Skip to content

Commit

Permalink
feat(cordis): support custom inject checker, check entry injections
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 28, 2024
1 parent 717b11d commit b78fd8e
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 31 deletions.
12 changes: 2 additions & 10 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export class Context {
static readonly static: unique symbol = symbols.static as any
static readonly filter: unique symbol = symbols.filter as any
static readonly expose: unique symbol = symbols.expose as any
static readonly inject: unique symbol = symbols.inject as any
static readonly isolate: unique symbol = symbols.isolate as any
static readonly internal: unique symbol = symbols.internal as any
static readonly intercept: unique symbol = symbols.intercept as any
Expand Down Expand Up @@ -104,14 +103,8 @@ export class Context {
if (name[0] === '$' || name[0] === '_') return
// Case 4: access directly from root
if (!ctx.runtime.plugin) return
// Case 5: inject in ancestor contexts
let parent = ctx
while (parent.runtime.plugin) {
for (const key of parent.runtime.inject) {
if (name === Context.resolveInject(parent, key)[0]) return
}
parent = parent.scope.parent
}
// Case 5: custom inject checks
if (ctx.bail('internal/inject', name)) return
ctx.emit('internal/warning', new Error(`property ${name} is not registered, declare it as \`inject\` to suppress this warning`))
}

Expand Down Expand Up @@ -164,7 +157,6 @@ export class Context {
constructor(config?: any) {
const self: Context = new Proxy(this, Context.handler)
config = resolveConfig(this.constructor, config)
self[symbols.inject] = {}
self[symbols.isolate] = Object.create(null)
self[symbols.intercept] = Object.create(null)
self.root = self
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ export class Lifecycle {
}
}
}, { global: true }), Context.static, root.scope)

// inject in ancestor contexts
defineProperty(this.on('internal/inject', function (name) {
let parent = this
while (parent.runtime.plugin) {
for (const key of parent.runtime.inject) {
if (name === Context.resolveInject(parent, key)[0]) return true
}
parent = parent.scope.parent
}
}, { global: true }), Context.static, root.scope)
}

async flush() {
Expand Down Expand Up @@ -224,6 +235,7 @@ export interface Events<in C extends Context = Context> {
'internal/service'(this: C, name: string, value: any): void
'internal/before-update'(fork: ForkScope<C>, config: any): void
'internal/update'(fork: ForkScope<C>, oldConfig: any): void
'internal/inject'(this: C, name: string): boolean | undefined
'internal/listener'(this: C, name: string, listener: any, prepend: boolean): void
'internal/event'(type: 'emit' | 'parallel' | 'serial' | 'bail', name: string, args: any[], thisArg: any): void
}
2 changes: 0 additions & 2 deletions packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export abstract class EffectScope<C extends Context = Context> {
constructor(public parent: C, public config: C['config']) {
this.uid = parent.registry ? parent.registry.counter : 0
this.ctx = this.context = parent.extend({ scope: this })
this.ctx[Context.inject] = {}
this.proxy = new Proxy({}, {
get: (target, key) => Reflect.get(this.config, key),
})
Expand Down Expand Up @@ -360,7 +359,6 @@ export class MainScope<C extends Context = Context> extends EffectScope<C> {
if (name && name !== 'apply') this.name = name
this.schema = this.plugin['Config'] || this.plugin['schema']
this.setInject(this.plugin['using'] || this.plugin['inject'])
this.setInject(this.parent[Context.inject])
this.isReusable = this.plugin['reusable']
this.isReactive = this.plugin['reactive']
this.context.emit('internal/runtime', this)
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const symbols = {
static: Symbol.for('cordis.static') as typeof Context.static,
filter: Symbol.for('cordis.filter') as typeof Context.filter,
expose: Symbol.for('cordis.expose') as typeof Context.expose,
inject: Symbol.for('cordis.inject') as typeof Context.inject,
isolate: Symbol.for('cordis.isolate') as typeof Context.isolate,
internal: Symbol.for('cordis.internal') as typeof Context.internal,
intercept: Symbol.for('cordis.intercept') as typeof Context.intercept,
Expand Down
74 changes: 58 additions & 16 deletions packages/loader/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ export class Entry {
}

patch(ctx: Context, ref: Context = ctx) {
ctx[Context.inject] = this.options.inject

// part 1: prepare isolate map
const newMap: Dict<symbol> = Object.create(Object.getPrototypeOf(ref[Context.isolate]))
for (const [key, label] of Object.entries(this.options.isolate ?? {})) {
Expand Down Expand Up @@ -144,11 +142,51 @@ export class Entry {
})
}

get requiredInjects() {
return Array.isArray(this.options.inject)
? this.options.inject
: this.options.inject?.required ?? []
}

get optionalInjects() {
return Array.isArray(this.options.inject)
? this.options.inject
: [
...this.options.inject?.required ?? [],
...this.options.inject?.optional ?? [],
]
}

_check() {
if (!this.loader.isTruthyLike(this.options.when)) return false
if (this.options.disabled) return false
for (const name of this.requiredInjects) {
let key = this.parent.ctx[Context.isolate][name]
const label = this.options.isolate?.[name]
if (label) {
const realm = this.resolveRealm(label)
key = (this.loader.realms[realm] ?? Object.create(null))[name] ?? Symbol(`${name}${realm}`)
}
if (!key || isNullable(this.parent.ctx[key])) return false
}
return true
}

async checkService(name: string) {
if (!this.requiredInjects.includes(name)) return
const ready = this._check()
if (ready && !this.fork) {
await this.start()
} else if (!ready && this.fork) {
await this.stop()
}
}

async update(options: Entry.Options) {
const legacy = this.options
this.options = sortKeys(options)
if (!this.loader.isTruthyLike(options.when) || options.disabled) {
this.stop()
if (!this._check()) {
await this.stop()
} else if (this.fork) {
this.suspend = true
for (const [key, label] of Object.entries(legacy.isolate ?? {})) {
Expand All @@ -158,21 +196,25 @@ export class Entry {
}
this.patch(this.fork.parent)
} else {
const ctx = this.createContext()
const exports = await this.loader.import(this.options.name).catch((error: any) => {
ctx.emit('internal/error', new Error(`Cannot find package "${this.options.name}"`))
ctx.emit('internal/error', error)
})
if (!exports) return
const plugin = this.loader.unwrapExports(exports)
this.patch(ctx)
ctx[Entry.key] = this
this.fork = ctx.plugin(plugin, this.options.config)
ctx.emit('loader/entry', 'apply', this)
await this.start()
}
}

stop() {
async start() {
const ctx = this.createContext()
const exports = await this.loader.import(this.options.name, this.parent.url).catch((error: any) => {
ctx.emit('internal/error', new Error(`Cannot find package "${this.options.name}"`))
ctx.emit('internal/error', error)
})
if (!exports) return
const plugin = this.loader.unwrapExports(exports)
this.patch(ctx)
ctx[Entry.key] = this
this.fork = ctx.plugin(plugin, this.options.config)
ctx.emit('loader/entry', 'apply', this)
}

async stop() {
this.fork?.dispose()
this.fork = undefined

Expand Down
31 changes: 29 additions & 2 deletions packages/loader/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export abstract class Loader extends BaseLoader {

constructor(public ctx: Context, public config: Loader.Config) {
super(ctx)

const self = this
this.ctx.set('loader', this)
this.realms['#'] = ctx.root[Context.isolate]

Expand Down Expand Up @@ -98,6 +100,31 @@ export abstract class Loader extends BaseLoader {
fork.entry.stop()
fork.entry.parent.write()
})

this.ctx.on('internal/before-service', function (name) {
for (const entry of Object.values(self.entries)) {
entry.checkService(name)
}
}, { global: true })

this.ctx.on('internal/service', function (name) {
for (const entry of Object.values(self.entries)) {
entry.checkService(name)
}
}, { global: true })

const checkInject = (scope: EffectScope, name: string) => {
if (!scope.runtime.plugin) return false
if (scope.runtime === scope) {
return scope.runtime.children.every(fork => checkInject(fork, name))
}
if (scope.entry?.optionalInjects.includes(name)) return true
return checkInject(scope.parent.scope, name)
}

this.ctx.on('internal/inject', function (this, name) {
return checkInject(this.scope, name)
})
}

async start() {
Expand Down Expand Up @@ -188,11 +215,11 @@ export abstract class Loader extends BaseLoader {

_locate(scope: EffectScope): Entry[] {
// root scope
if (scope === scope.parent.scope) return []
if (!scope.runtime.plugin) return []

// runtime scope
if (scope.runtime === scope) {
return ([] as Entry[]).concat(...scope.runtime.children.map(child => this._locate(child)))
return scope.runtime.children.flatMap(child => this._locate(child))
}

if (scope.entry) return [scope.entry]
Expand Down

0 comments on commit b78fd8e

Please sign in to comment.