Skip to content

Commit

Permalink
refa: remove fork impl
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Oct 23, 2024
1 parent 511a74e commit dff4e79
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 279 deletions.
2 changes: 1 addition & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ See: [Reusable plugins](#reusable-plugins-)

- runtime: `MainScope`

#### internal/fork(fork)
#### internal/plugin(fork)

- fork: `ForkScope`

Expand Down
13 changes: 8 additions & 5 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Lifecycle from './events.ts'
import ReflectService from './reflect.ts'
import Registry from './registry.ts'
import { getTraceable, resolveConfig, symbols } from './utils.ts'
import { EffectScope } from './index.ts'

export { Lifecycle, ReflectService, Registry }

Expand Down Expand Up @@ -93,6 +94,7 @@ export class Context {
this[symbols.intercept] = Object.create(null)
const self: Context = new Proxy(this, ReflectService.handler)
self.root = self
this.scope = new EffectScope(this, config, () => {})
self.reflect = new ReflectService(self)
self.registry = new Registry(self, config)
self.lifecycle = new Lifecycle(self)
Expand All @@ -116,11 +118,12 @@ export class Context {
}

get name() {
let runtime = this.runtime
while (runtime && !runtime.name) {
runtime = runtime.parent.runtime
}
return runtime?.name!
let scope = this.scope
do {
if (scope.meta?.name) return scope.meta.name
scope = scope.parent.scope
} while (scope)
return 'root'
}

get events() {
Expand Down
32 changes: 13 additions & 19 deletions packages/core/src/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Awaitable, defineProperty, Promisify, remove } from 'cosmokit'
import { Context } from './context.ts'
import { EffectScope, ForkScope, MainScope, ScopeStatus } from './scope.ts'
import { EffectScope, ScopeStatus } from './scope.ts'
import { getTraceable, symbols } from './index.ts'
import ReflectService from './reflect.ts'

Expand Down Expand Up @@ -54,6 +54,7 @@ class Lifecycle {
property: 'ctx',
})

// TODO: deprecate these events
ctx.scope.leak(this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) {
const method = options.prepend ? 'unshift' : 'push'
if (name === 'ready') {
Expand All @@ -63,9 +64,6 @@ class Lifecycle {
this.scope.disposables[method](listener as any)
defineProperty(listener, 'name', 'event <dispose>')
return () => remove(this.scope.disposables, listener)
} else if (name === 'fork') {
this.scope.runtime.forkables[method](listener as any)
return this.scope.collect('event <fork>', () => remove(this.scope.runtime.forkables, listener))
}
}))

Expand All @@ -79,10 +77,9 @@ class Lifecycle {

// non-reusable plugin forks are not responsive to isolated service changes
ctx.scope.leak(this.on('internal/before-service', function (this: Context, name) {
for (const runtime of this.registry.values()) {
if (!runtime.inject[name]?.required) continue
const scopes = runtime.isReusable ? runtime.children : [runtime]
for (const scope of scopes) {
for (const meta of this.registry.values()) {
if (!meta.inject[name]?.required) continue
for (const scope of meta.scopes) {
if (!this[symbols.filter](scope.ctx)) continue
scope.updateStatus()
scope.reset()
Expand All @@ -91,10 +88,9 @@ class Lifecycle {
}, { global: true }))

ctx.scope.leak(this.on('internal/service', function (this: Context, name) {
for (const runtime of this.registry.values()) {
if (!runtime.inject[name]?.required) continue
const scopes = runtime.isReusable ? runtime.children : [runtime]
for (const scope of scopes) {
for (const meta of this.registry.values()) {
if (!meta.inject[name]?.required) continue
for (const scope of meta.scopes) {
if (!this[symbols.filter](scope.ctx)) continue
scope.start()
}
Expand All @@ -114,8 +110,8 @@ class Lifecycle {

// inject in ancestor contexts
const checkInject = (scope: EffectScope, name: string) => {
if (!scope.runtime.plugin) return false
for (const key in scope.runtime.inject) {
if (!scope.meta) return false
for (const key in scope.meta.inject) {
if (name === ReflectService.resolveInject(scope.ctx, key)[0]) return true
}
return checkInject(scope.parent.scope, name)
Expand Down Expand Up @@ -218,19 +214,17 @@ class Lifecycle {
export default Lifecycle

export interface Events<in C extends Context = Context> {
'fork'(ctx: C, config: C['config']): void
'ready'(): Awaitable<void>
'dispose'(): Awaitable<void>
'internal/fork'(fork: ForkScope<C>): void
'internal/runtime'(runtime: MainScope<C>): void
'internal/plugin'(fork: EffectScope<C>): void
'internal/status'(scope: EffectScope<C>, oldValue: ScopeStatus): void
'internal/info'(this: C, format: any, ...param: any[]): void
'internal/error'(this: C, format: any, ...param: any[]): void
'internal/warning'(this: C, format: any, ...param: any[]): void
'internal/before-service'(this: C, name: string, value: any): void
'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/before-update'(fork: EffectScope<C>, config: any): void
'internal/update'(fork: EffectScope<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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/reflect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ReflectService {
// Case 2: `$` or `_` prefix
if (name[0] === '$' || name[0] === '_') return
// Case 3: access directly from root
if (!ctx.runtime.plugin) return
if (!ctx.scope.meta) return
// Case 4: custom inject checks
if (ctx.bail(ctx, 'internal/inject', name)) return
const lines = error.stack!.split('\n')
Expand Down
121 changes: 77 additions & 44 deletions packages/core/src/registry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineProperty, Dict } from 'cosmokit'
import { Context } from './context.ts'
import { ForkScope, MainScope, ScopeStatus } from './scope.ts'
import { resolveConfig, symbols, withProps } from './utils.ts'
import { EffectScope } from './scope.ts'
import { isConstructor, resolveConfig, symbols, withProps } from './utils.ts'

function isApplicable(object: Plugin) {
return object && typeof object === 'object' && typeof object.apply === 'function'
Expand All @@ -10,11 +10,11 @@ function isApplicable(object: Plugin) {
export type Inject = string[] | Dict<Inject.Meta>

export function Inject(inject: Inject) {
return function (value: any, ctx: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) {
if (ctx.kind === 'class') {
return function (value: any, decorator: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) {
if (decorator.kind === 'class') {
value.inject = inject
} else if (ctx.kind === 'method') {
ctx.addInitializer(function () {
} else if (decorator.kind === 'method') {
decorator.addInitializer(function () {
const property = this[symbols.tracker]?.property
if (!property) throw new Error('missing context tracker')
;(this[symbols.initHooks] ??= []).push(() => {
Expand Down Expand Up @@ -72,15 +72,33 @@ export namespace Plugin {
}

export interface Function<C extends Context = Context, T = any> extends Base<T> {
(ctx: C, config: T): void
(ctx: C, config: T): void | Promise<void>
}

export interface Constructor<C extends Context = Context, T = any> extends Base<T> {
new (ctx: C, config: T): any
}

export interface Object<C extends Context = Context, T = any> extends Base<T> {
apply: (ctx: C, config: T) => void
apply: (ctx: C, config: T) => void | Promise<void>
}

export interface Meta<C extends Context = Context> {
name?: string
schema: any
inject: Dict<Inject.Meta>
isReactive?: boolean
scopes: EffectScope<C>[]
plugin: Plugin<C>
}

export function resolve<C extends Context = Context>(plugin: Plugin<C>): Meta<C> {
let name = plugin.name
if (name === 'apply') name = undefined
const schema = plugin['Config'] || plugin['schema']
const inject = Inject.resolve(plugin['using'] || plugin['inject'])
const isReactive = plugin['reactive']
return { name, schema, inject, isReactive, plugin, scopes: [] }
}
}

Expand All @@ -89,20 +107,20 @@ export type Spread<T> = undefined extends T ? [config?: T] : [config: T]
declare module './context.ts' {
export interface Context {
/** @deprecated use `ctx.inject()` instead */
using(deps: Inject, callback: Plugin.Function<this, void>): ForkScope<this>
inject(deps: Inject, callback: Plugin.Function<this, void>): ForkScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Function<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): ForkScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Constructor<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): ForkScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Object<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): ForkScope<this>
plugin<T = undefined>(plugin: Plugin.Function<this, T>, ...args: Spread<T>): ForkScope<this>
plugin<T = undefined>(plugin: Plugin.Constructor<this, T>, ...args: Spread<T>): ForkScope<this>
plugin<T = undefined>(plugin: Plugin.Object<this, T>, ...args: Spread<T>): ForkScope<this>
using(deps: Inject, callback: Plugin.Function<this, void>): EffectScope<this>
inject(deps: Inject, callback: Plugin.Function<this, void>): EffectScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Function<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): EffectScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Constructor<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): EffectScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Object<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): EffectScope<this>
plugin<T = undefined>(plugin: Plugin.Function<this, T>, ...args: Spread<T>): EffectScope<this>
plugin<T = undefined>(plugin: Plugin.Constructor<this, T>, ...args: Spread<T>): EffectScope<this>
plugin<T = undefined>(plugin: Plugin.Object<this, T>, ...args: Spread<T>): EffectScope<this>
}
}

class Registry<C extends Context = Context> {
private _counter = 0
private _internal = new Map<Function, MainScope<C>>()
private _internal = new Map<Function, Plugin.Meta<C>>()
protected context: Context

constructor(public ctx: C, config: any) {
Expand All @@ -112,11 +130,6 @@ class Registry<C extends Context = Context> {
})

this.context = ctx
const runtime = new MainScope(ctx, null!, config)
ctx.scope = runtime
runtime.ctx = ctx
runtime.status = ScopeStatus.ACTIVE
this.set(null!, runtime)
}

get counter() {
Expand All @@ -127,9 +140,9 @@ class Registry<C extends Context = Context> {
return this._internal.size
}

resolve(plugin: Plugin, assert: true): Function
resolve(plugin: Plugin, assert?: boolean): Function | undefined
resolve(plugin: Plugin, assert = false): Function | undefined {
// Allow `null` as a special case.
if (plugin === null) return plugin
if (typeof plugin === 'function') return plugin
if (isApplicable(plugin)) return plugin.apply
if (assert) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin)
Expand All @@ -145,18 +158,12 @@ class Registry<C extends Context = Context> {
return !!key && this._internal.has(key)
}

set(plugin: Plugin, state: MainScope<C>) {
const key = this.resolve(plugin)
this._internal.set(key!, state)
}

delete(plugin: Plugin) {
const key = this.resolve(plugin)
const runtime = key && this._internal.get(key)
if (!runtime) return
const meta = key && this._internal.get(key)
if (!meta) return
this._internal.delete(key)
runtime.dispose()
return runtime
return meta
}

keys() {
Expand All @@ -171,7 +178,7 @@ class Registry<C extends Context = Context> {
return this._internal.entries()
}

forEach(callback: (value: MainScope<C>, key: Function, map: Map<Plugin, MainScope<C>>) => void) {
forEach(callback: (value: Plugin.Meta<C>, key: Function) => void) {
return this._internal.forEach(callback)
}

Expand All @@ -185,7 +192,7 @@ class Registry<C extends Context = Context> {

plugin(plugin: Plugin<C>, config?: any, error?: any) {
// check if it's a valid plugin
this.resolve(plugin, true)
const key = this.resolve(plugin, true)
this.ctx.scope.assertActive()

// resolve plugin config
Expand All @@ -199,18 +206,44 @@ class Registry<C extends Context = Context> {
}
}

// check duplication
let runtime = this.get(plugin)
if (runtime) {
if (!runtime.isForkable) {
this.context.emit(this.ctx, 'internal/warning', new Error(`duplicate plugin detected: ${plugin.name}`))
const meta = Plugin.resolve<C>(plugin)
this._internal.set(key!, meta)

const scope = new EffectScope(this.ctx, config, async (ctx, config) => {
if (typeof plugin !== 'function') {
await plugin.apply(ctx, config)
} else if (isConstructor(plugin)) {
// eslint-disable-next-line new-cap
const instance = new plugin(ctx, config)
for (const hook of instance?.[symbols.initHooks] ?? []) {
hook()
}
await instance?.[symbols.setup]?.()
} else {
await plugin(ctx, config)
}
return runtime.fork(this.ctx, config, error)
}, meta)
if (!config) {
scope.cancel(error)
} else {
scope.start()
}
return scope
}

runtime = new MainScope(this.ctx, plugin, config, error)
this.set(plugin, runtime)
return runtime.fork(this.ctx, config, error)
private async apply(plugin: Plugin, context: C, config: any) {
if (typeof plugin !== 'function') {
await plugin.apply(context, config)
} else if (isConstructor(plugin)) {
// eslint-disable-next-line new-cap
const instance = new plugin(context, config)
for (const hook of instance?.[symbols.initHooks] ?? []) {
hook()
}
await instance?.[symbols.setup]?.()
} else {
await plugin(context, config)
}
}
}

Expand Down
Loading

0 comments on commit dff4e79

Please sign in to comment.