diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts index 9de0b29..a327f0f 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/context.ts @@ -254,6 +254,7 @@ export class Context { return dispose } + /** @deprecated use `ctx.set()` instead */ provide(name: string, value?: any, builtin?: boolean) { const internal = Context.ensureInternal.call(this.root) if (name in internal) return diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts index e224727..e115049 100644 --- a/packages/core/src/events.ts +++ b/packages/core/src/events.ts @@ -45,8 +45,8 @@ export class Lifecycle { _tasks = new Set>() _hooks: Record = {} - constructor(private root: Context) { - defineProperty(this, Context.origin, root) + constructor(private ctx: Context) { + defineProperty(this, Context.origin, ctx) defineProperty(this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) { const method = options.prepend ? 'unshift' : 'push' @@ -62,14 +62,14 @@ export class Lifecycle { this.scope.runtime.forkables[method](listener as any) return this.scope.collect('event ', () => remove(this.scope.runtime.forkables, listener)) } - }), Context.static, root.scope) + }), Context.static, ctx.scope) for (const level of ['info', 'error', 'warning']) { defineProperty(this.on(`internal/${level}`, (format, ...param) => { if (this._hooks[`internal/${level}`].length > 1) return // eslint-disable-next-line no-console console.info(format, ...param) - }), Context.static, root.scope) + }), Context.static, ctx.scope) } // non-reusable plugin forks are not responsive to isolated service changes @@ -83,7 +83,7 @@ export class Lifecycle { scope.reset() } } - }, { global: true }), Context.static, root.scope) + }, { global: true }), Context.static, ctx.scope) defineProperty(this.on('internal/service', function (this: Context, name) { for (const runtime of this.registry.values()) { @@ -94,7 +94,7 @@ export class Lifecycle { scope.start() } } - }, { global: true }), Context.static, root.scope) + }, { global: true }), Context.static, ctx.scope) // inject in ancestor contexts defineProperty(this.on('internal/inject', function (name) { @@ -105,7 +105,7 @@ export class Lifecycle { } parent = parent.scope.parent } - }, { global: true }), Context.static, root.scope) + }, { global: true }), Context.static, ctx.scope) } async flush() { @@ -128,7 +128,7 @@ export class Lifecycle { if (name !== 'internal/event') { this.emit('internal/event', type, name, args, thisArg) } - return [this.getHooks(name, thisArg), thisArg ?? this[Context.origin]] as const + return [this.getHooks(name, thisArg), thisArg ?? this.ctx] as const } async parallel(...args: any[]) { @@ -162,10 +162,9 @@ export class Lifecycle { } register(label: string, hooks: Hook[], listener: any, options: EventOptions) { - const caller = this[Context.origin] const method = options.prepend ? 'unshift' : 'push' - hooks[method]([caller, listener, options]) - return caller.state.collect(label, () => this.unregister(hooks, listener)) + hooks[method]([this.ctx, listener, options]) + return this.ctx.state.collect(label, () => this.unregister(hooks, listener)) } unregister(hooks: Hook[], listener: any) { @@ -182,9 +181,8 @@ export class Lifecycle { } // handle special events - const caller: Context = this[Context.origin] - caller.scope.assertActive() - const result = this.bail(caller, 'internal/listener', name, listener, options) + this.ctx.scope.assertActive() + const result = this.bail(this.ctx, 'internal/listener', name, listener, options) if (result) return result const hooks = this._hooks[name] ||= [] @@ -217,7 +215,7 @@ export class Lifecycle { async stop() { this.isActive = false // `dispose` event is handled by state.disposables - this.root.scope.reset() + this.ctx.scope.reset() } } diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index e672e68..0c363b5 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -65,10 +65,10 @@ export class Registry { private _counter = 0 private _internal = new Map>() - constructor(private root: Context, config: any) { - defineProperty(this, Context.origin, root) - root.scope = new MainScope(this, null!, config) - root.scope.runtime.isReactive = true + constructor(private ctx: Context, config: any) { + defineProperty(this, Context.origin, ctx) + ctx.scope = new MainScope(this, null!, config) + ctx.scope.runtime.isReactive = true } get counter() { @@ -139,7 +139,7 @@ export class Registry { // check if it's a valid plugin this.resolve(plugin, true) - const context: Context = this[Context.origin] + const context: Context = this.ctx context.scope.assertActive() // resolve plugin config diff --git a/packages/core/src/service.ts b/packages/core/src/service.ts index b183f7c..73a1dfc 100644 --- a/packages/core/src/service.ts +++ b/packages/core/src/service.ts @@ -79,14 +79,13 @@ export abstract class Service { } protected [symbols.extend](props?: any) { - const caller = this[symbols.origin] let self: any if (this[Service.invoke]) { self = createCallable(this.name, this) } else { self = Object.create(this) } - defineProperty(self, symbols.origin, caller) + defineProperty(self, symbols.origin, this.ctx) return Context.associate(Object.assign(self, props), this.name) } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 62c49a8..540d7e7 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -57,7 +57,7 @@ export function joinPrototype(proto1: {}, proto2: {}) { export function createTraceable(ctx: any, value: any) { const proxy = new Proxy(value, { get: (target, name, receiver) => { - if (name === symbols.origin) return ctx + if (name === symbols.origin || name === 'ctx') return ctx return Reflect.get(target, name, receiver) }, apply: (target, thisArg, args) => { diff --git a/packages/core/tests/dispose.spec.ts b/packages/core/tests/dispose.spec.ts index 67b7941..4617f3e 100644 --- a/packages/core/tests/dispose.spec.ts +++ b/packages/core/tests/dispose.spec.ts @@ -75,7 +75,7 @@ describe('Disposables', () => { expect(dispose.mock.calls).to.have.length(1) }) - it('dispose event', async () => { + it('dispose event error', async () => { const root = new Context() const error = mock.fn() const dispose = mock.fn(() => { diff --git a/packages/core/tests/invoke.spec.ts b/packages/core/tests/invoke.spec.ts new file mode 100644 index 0000000..87e2774 --- /dev/null +++ b/packages/core/tests/invoke.spec.ts @@ -0,0 +1,61 @@ +import { expect } from 'chai' +import { Context, Service } from '../src' + +describe('functional service', () => { + it('functional service', async () => { + interface Config {} + + interface Foo { + (init?: Config): Config + } + + class Foo extends Service { + static [Service.provide] = 'foo' + static [Service.immediate] = true + + protected [Service.invoke](init?: Config) { + const caller = this[Context.origin] + expect(caller).to.be.instanceof(Context) + let result = { ...this.config } + let intercept = caller[Context.intercept] + while (intercept) { + Object.assign(result, intercept.foo) + intercept = Object.getPrototypeOf(intercept) + } + Object.assign(result, init) + return result + } + + reflect() { + return this() + } + + extend(config?: Config) { + return this[Service.extend]({ + config: { ...this.config, ...config }, + }) + } + } + + const root = new Context() + root.plugin(Foo, { a: 1 }) + + // access from context + expect(root.foo()).to.deep.equal({ a: 1 }) + const ctx1 = root.intercept('foo', { b: 2 }) + expect(ctx1.foo()).to.deep.equal({ a: 1, b: 2 }) + const foo1 = ctx1.foo + expect(foo1).to.be.instanceof(Foo) + + // create extension + const foo2 = root.foo.extend({ c: 3 }) + expect(foo2).to.be.instanceof(Foo) + expect(foo2()).to.deep.equal({ a: 1, c: 3 }) + const foo3 = foo1.extend({ d: 4 }) + expect(foo3).to.be.instanceof(Foo) + expect(foo3.reflect()).to.deep.equal({ a: 1, b: 2, d: 4 }) + + // context tracibility + expect(foo1.reflect()).to.deep.equal({ a: 1, b: 2 }) + }) +}) diff --git a/packages/core/tests/service.spec.ts b/packages/core/tests/service.spec.ts index 02aba5a..009944e 100644 --- a/packages/core/tests/service.spec.ts +++ b/packages/core/tests/service.spec.ts @@ -128,19 +128,34 @@ describe('Service', () => { expect(callback.mock.calls).to.have.length(1) }) - it('Context.origin', async () => { + it('traceable effect', async () => { class Foo extends Service { + size = 0 + constructor(ctx: Context) { super(ctx, 'foo', true) } + + increse() { + return this.ctx.effect(() => { + this.size++ + return () => this.size-- + }) + } } const root = new Context() root.plugin(Foo) - expect(root.foo[Context.origin]).to.equal(root) + root.foo.increse() + expect(root.foo.size).to.equal(1) + + const fork = root.plugin((ctx) => { + ctx.foo.increse() + expect(ctx.foo.size).to.equal(2) + }) - const ctx = root.extend() - expect(ctx.foo[Context.origin]).to.equal(ctx) + fork.dispose() + expect(root.foo.size).to.equal(1) }) it('dependency update', async () => { @@ -279,61 +294,4 @@ describe('Service', () => { expect(bar.mock.calls).to.have.length(1) expect(qux.mock.calls).to.have.length(1) }) - - it('functional service', async () => { - interface Config {} - - interface Foo { - (init?: Config): Config - } - - class Foo extends Service { - static [Service.provide] = 'foo' - static [Service.immediate] = true - - protected [Service.invoke](init?: Config) { - const caller = this[Context.origin] - expect(caller).to.be.instanceof(Context) - let result = { ...this.config } - let intercept = caller[Context.intercept] - while (intercept) { - Object.assign(result, intercept.foo) - intercept = Object.getPrototypeOf(intercept) - } - Object.assign(result, init) - return result - } - - reflect() { - return this() - } - - extend(config?: Config) { - return this[Service.extend]({ - config: { ...this.config, ...config }, - }) - } - } - - const root = new Context() - root.plugin(Foo, { a: 1 }) - - // access from context - expect(root.foo()).to.deep.equal({ a: 1 }) - const ctx1 = root.intercept('foo', { b: 2 }) - expect(ctx1.foo()).to.deep.equal({ a: 1, b: 2 }) - const foo1 = ctx1.foo - expect(foo1).to.be.instanceof(Foo) - - // create extension - const foo2 = root.foo.extend({ c: 3 }) - expect(foo2).to.be.instanceof(Foo) - expect(foo2()).to.deep.equal({ a: 1, c: 3 }) - const foo3 = foo1.extend({ d: 4 }) - expect(foo3).to.be.instanceof(Foo) - expect(foo3.reflect()).to.deep.equal({ a: 1, b: 2, d: 4 }) - - // context tracibility - expect(foo1.reflect()).to.deep.equal({ a: 1, b: 2 }) - }) })