Skip to content

Commit a95c552

Browse files
committed
feat: waterfall internal/update
1 parent 3315005 commit a95c552

File tree

7 files changed

+58
-70
lines changed

7 files changed

+58
-70
lines changed

packages/core/src/events.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { deepEqual, defineProperty, Promisify } from 'cosmokit'
1+
import { defineProperty, Promisify } from 'cosmokit'
22
import { Context } from './context'
33
import { Fiber, FiberState } from './fiber'
4-
import { symbols } from './utils'
4+
import { DisposableList, symbols } from './utils'
55

66
export function isBailed(value: any) {
77
return value !== null && value !== false && value !== undefined
@@ -19,15 +19,15 @@ declare module './context' {
1919
/* eslint-disable max-len */
2020
[Context.events]: Events<this>
2121
parallel<K extends keyof GetEvents<this>>(name: K, ...args: Parameters<GetEvents<this>[K]>): Promise<void>
22-
parallel<K extends keyof GetEvents<this>>(thisArg: ThisType<GetEvents<this>[K]>, name: K, ...args: Parameters<GetEvents<this>[K]>): Promise<void>
22+
parallel<K extends keyof GetEvents<this>>(thisArg: NoInfer<ThisType<GetEvents<this>[K]>>, name: K, ...args: Parameters<GetEvents<this>[K]>): Promise<void>
2323
emit<K extends keyof GetEvents<this>>(name: K, ...args: Parameters<GetEvents<this>[K]>): void
24-
emit<K extends keyof GetEvents<this>>(thisArg: ThisType<GetEvents<this>[K]>, name: K, ...args: Parameters<GetEvents<this>[K]>): void
24+
emit<K extends keyof GetEvents<this>>(thisArg: NoInfer<ThisType<GetEvents<this>[K]>>, name: K, ...args: Parameters<GetEvents<this>[K]>): void
2525
serial<K extends keyof GetEvents<this>>(name: K, ...args: Parameters<GetEvents<this>[K]>): Promisify<ReturnType<GetEvents<this>[K]>>
26-
serial<K extends keyof GetEvents<this>>(thisArg: ThisType<GetEvents<this>[K]>, name: K, ...args: Parameters<GetEvents<this>[K]>): Promisify<ReturnType<GetEvents<this>[K]>>
26+
serial<K extends keyof GetEvents<this>>(thisArg: NoInfer<ThisType<GetEvents<this>[K]>>, name: K, ...args: Parameters<GetEvents<this>[K]>): Promisify<ReturnType<GetEvents<this>[K]>>
2727
bail<K extends keyof GetEvents<this>>(name: K, ...args: Parameters<GetEvents<this>[K]>): ReturnType<GetEvents<this>[K]>
28-
bail<K extends keyof GetEvents<this>>(thisArg: ThisType<GetEvents<this>[K]>, name: K, ...args: Parameters<GetEvents<this>[K]>): ReturnType<GetEvents<this>[K]>
28+
bail<K extends keyof GetEvents<this>>(thisArg: NoInfer<ThisType<GetEvents<this>[K]>>, name: K, ...args: Parameters<GetEvents<this>[K]>): ReturnType<GetEvents<this>[K]>
2929
waterfall<K extends keyof GetEvents<this>>(name: K, ...args: Parameters<GetEvents<this>[K]>): ReturnType<GetEvents<this>[K]>
30-
waterfall<K extends keyof GetEvents<this>>(thisArg: ThisType<GetEvents<this>[K]>, name: K, ...args: Parameters<GetEvents<this>[K]>): ReturnType<GetEvents<this>[K]>
30+
waterfall<K extends keyof GetEvents<this>>(thisArg: NoInfer<ThisType<GetEvents<this>[K]>>, name: K, ...args: Parameters<GetEvents<this>[K]>): ReturnType<GetEvents<this>[K]>
3131
on<K extends keyof GetEvents<this>>(name: K, listener: GetEvents<this>[K], options?: boolean | EventOptions): () => boolean
3232
once<K extends keyof GetEvents<this>>(name: K, listener: GetEvents<this>[K], options?: boolean | EventOptions): () => boolean
3333
/* eslint-enable max-len */
@@ -53,16 +53,11 @@ export class EventsService<C extends Context = Context> {
5353
noShadow: true,
5454
})
5555

56-
// TODO: deprecate these events
5756
this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) {
58-
if (name === 'ready') {
59-
Promise.resolve().then(listener)
60-
return () => false
61-
} else if (name === 'dispose') {
62-
defineProperty(listener, 'name', 'event <dispose>')
63-
return this.fiber._disposables.push(listener)
64-
} else if (name === 'internal/update' && !options.global) {
65-
return this.fiber.acceptors.push(listener)
57+
if (name === 'internal/update' && !options.global) {
58+
const hooks = this.fiber._hooks['internal/update'] ??= new DisposableList()
59+
const method = options.prepend ? 'unshift' : 'push'
60+
return hooks[method](listener)
6661
}
6762
})
6863

@@ -74,12 +69,14 @@ export class EventsService<C extends Context = Context> {
7469
})
7570
}
7671

77-
this.on('internal/update', (fiber: Fiber, config) => {
78-
for (const acceptor of fiber.acceptors) {
79-
if (acceptor.call(fiber, config)) return true
72+
this.on('internal/update', function (config, _next) {
73+
const cbs = [...this._hooks['internal/update'] || []]
74+
const next = () => {
75+
const cb = cbs.shift() ?? _next
76+
return cb.call(this, config, next)
8077
}
81-
return deepEqual(fiber.config, config)
82-
}, { global: true })
78+
return next()
79+
}, { global: true, prepend: true })
8380
}
8481

8582
dispatch(type: string, args: any[]) {
@@ -175,7 +172,7 @@ export interface Events<in C extends Context = Context> {
175172
'internal/error'(this: C, format: any, ...param: any[]): void
176173
'internal/warn'(this: C, format: any, ...param: any[]): void
177174
'internal/service'(this: C, name: string, value: any): void
178-
'internal/update'(fiber: Fiber<C>, config: any): boolean | void
175+
'internal/update'(this: Fiber<C>, config: any, next: () => void): void
179176
'internal/get'(ctx: C, name: string, error: Error, next: () => any): any
180177
'internal/set'(ctx: C, name: string, value: any, error: Error, next: () => boolean): boolean
181178
'internal/listener'(this: C, name: string, listener: any, prepend: boolean): void

packages/core/src/fiber.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,12 @@ export class Fiber<out C extends Context = Context> {
6969
public uid: number | null
7070
public ctx: C
7171
public config: any
72-
public acceptors = new DisposableList<(config: any) => boolean>()
7372
public state = FiberState.PENDING
7473
public dispose: () => Promise<void>
7574
public store: Dict<Impl<C>> | undefined
7675
public inertia: Promise<void> | undefined
7776

77+
public _hooks: Dict<DisposableList<Function>> = Object.create(null)
7878
public _disposables = new DisposableList<Disposable>()
7979

8080
// Same as `this.ctx`, but with a more specific type.
@@ -432,16 +432,11 @@ export class Fiber<out C extends Context = Context> {
432432

433433
update(config: any) {
434434
this.assertActive()
435-
if (this.context.bail(this, 'internal/update', this, config)) return
436-
try {
437-
this.config = resolveConfig(this.runtime!, config)
438-
} catch (error) {
439-
this.context.emit('internal/error', error)
440-
this._error = error
441-
this._setEpoch(INACTIVE)
442-
return
443-
}
444-
this._error = undefined
445-
return this.restart()
435+
config = resolveConfig(this.runtime!, config)
436+
this.context.waterfall(this, 'internal/update', config, () => {
437+
this.config = config
438+
this._error = undefined
439+
return this.restart()
440+
})
446441
}
447442
}

packages/loader/src/config/entry.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Context, Fiber, Inject } from '@cordisjs/core'
2-
import { isNullable } from 'cosmokit'
2+
import { deepEqual, isNullable } from 'cosmokit'
33
import { Loader } from '../loader.ts'
44
import { EntryGroup } from './group.ts'
55
import { EntryTree } from './tree.ts'
@@ -38,7 +38,8 @@ export class Entry<C extends Context = Context> {
3838
public fiber?: Fiber<C>
3939
public suspend = false
4040
public parent!: EntryGroup<C>
41-
public options!: EntryOptions
41+
// safety: call `entry.update()` immediately after creating an entry
42+
public options = {} as EntryOptions
4243
public subgroup?: EntryGroup<C>
4344
public subtree?: EntryTree<C>
4445

@@ -81,23 +82,13 @@ export class Entry<C extends Context = Context> {
8182
return interpolate(this.ctx, this.options.config)
8283
}
8384

84-
private _patchContext(options: Partial<EntryOptions> = {}) {
85+
private _patchContext(diff: string[]) {
8586
this.context.waterfall('loader/patch-context', this, () => {
86-
// step 1: set prototype for transferred context
8787
Object.setPrototypeOf(this.ctx, this.parent.ctx)
8888

89-
if (this.fiber && 'config' in options) {
90-
// step 2: update fork (when options.config is updated)
89+
if (this.fiber?.uid && (diff.includes('config') || this.options.group)) {
9190
this.suspend = true
92-
this.fiber.update(this._resolveConfig(this.fiber.runtime?.callback))
93-
} else if (this.subgroup && 'disabled' in options) {
94-
// step 3: check children (when options.disabled is updated)
95-
const tree = this.subtree ?? this.parent.tree
96-
for (const options of this.subgroup.data) {
97-
tree.store[options.id].update({
98-
disabled: options.disabled,
99-
})
100-
}
91+
this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback))
10192
}
10293
})
10394
}
@@ -108,11 +99,11 @@ export class Entry<C extends Context = Context> {
10899
await this.init()
109100
}
110101

111-
async update(options: Partial<EntryOptions>, override = false) {
102+
async update(options: Partial<EntryOptions>, create = false, force = false) {
112103
const legacy = { ...this.options }
113104

114105
// step 1: update options
115-
if (override) {
106+
if (create) {
116107
this.options = options as EntryOptions
117108
} else {
118109
for (const [key, value] of Object.entries(options)) {
@@ -126,15 +117,19 @@ export class Entry<C extends Context = Context> {
126117
sortKeys(this.options)
127118

128119
// step 2: execute
129-
// this._check() is only a init-time optimization
130120
if (this.disabled) {
131121
this.fiber?.dispose()
132122
return
133123
}
134124

125+
// step 3: check if options are changed
135126
if (this.fiber?.uid) {
127+
const diff = Object
128+
.keys({ ...this.options, ...legacy })
129+
.filter(key => !deepEqual(this.options[key], legacy[key]))
130+
if (!diff.length && !force) return
136131
this.context.emit('loader/partial-dispose', this, legacy, true)
137-
this._patchContext(options)
132+
this._patchContext(diff)
138133
} else {
139134
await this.init()
140135
}
@@ -173,7 +168,7 @@ export class Entry<C extends Context = Context> {
173168
this._initTask = undefined
174169
}
175170
const plugin = this.loader.unwrapExports(exports)
176-
this._patchContext()
171+
this._patchContext([])
177172
this.loader.showLog(this, 'apply')
178173
this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
179174
}

packages/loader/src/config/group.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export class EntryGroup<C extends Context = Context> {
2222
// Entry may be moved from another group,
2323
// so we need to update the parent reference.
2424
entry.parent = this
25-
await entry.update(options, true)
25+
// Use `create: true` to replace existing entry.options.
26+
await entry.update(options, true, true)
2627
return entry.id
2728
}
2829

@@ -73,9 +74,8 @@ export class Group extends EntryGroup {
7374

7475
constructor(public ctx: Context, public config: EntryOptions[]) {
7576
super(ctx, ctx.fiber.entry!.parent.tree)
76-
ctx.on('internal/update', (_, config) => {
77+
ctx.on('internal/update', (config) => {
7778
this.update(config)
78-
return true
7979
})
8080
}
8181

packages/loader/src/config/tree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export abstract class EntryTree<C extends Context = Context> {
9595
entry.parent = target
9696
}
9797
source.tree.write()
98-
return entry.update(options)
98+
return entry.update(options, false, true)
9999
}
100100

101101
import(name: string, getOuterStack?: () => string[], useInternal = false) {

packages/loader/src/loader.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export abstract class Loader<C extends Context = Context> extends ImportTree<C>
6767

6868
constructor(public ctx: C, public config: Loader.Config) {
6969
super(ctx)
70+
const self = this
7071

7172
defineProperty(this, Service.tracker, {
7273
associate: 'loader',
@@ -76,19 +77,19 @@ export abstract class Loader<C extends Context = Context> extends ImportTree<C>
7677

7778
ctx.reflect.provide('loader', this, this[Service.check])
7879

79-
ctx.on('internal/update', (fiber, config) => {
80-
if (!fiber.entry) return
81-
this.showLog(fiber.entry, 'reload')
80+
ctx.on('internal/update', function (config, next) {
81+
if (!this.entry) return next()
82+
if (this.entry.suspend) {
83+
this.entry.suspend = false
84+
} else {
85+
const unparse = this.runtime?.Config?.['simplify']
86+
this.entry.options.config = unparse ? unparse(config) : config
87+
this.entry.parent.tree.write()
88+
}
89+
self.showLog(this.entry, 'reload')
90+
return next()
8291
}, { global: true })
8392

84-
ctx.on('internal/update', (fiber, config) => {
85-
if (!fiber.entry) return
86-
if (fiber.entry.suspend) return fiber.entry.suspend = false
87-
const unparse = fiber.runtime?.Config?.['simplify']
88-
fiber.entry.options.config = unparse ? unparse(config) : config
89-
fiber.entry.parent.tree.write()
90-
}, { global: true, prepend: true })
91-
9293
ctx.on('internal/plugin', (fiber) => {
9394
// 1. set `fiber.entry`
9495
if (fiber.parent[Entry.key] && !fiber.entry) {

packages/loader/tests/isolate.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('Service Isolation: basic', () => {
1818
foo = Object.assign(loader.mock('foo', () => dispose), {
1919
inject: ['bar'],
2020
})
21-
21+
2222
bar = loader.mock('bar', class Bar extends Service {
2323
constructor(ctx: Context) {
2424
super(ctx, 'bar')

0 commit comments

Comments
 (0)