diff --git a/packages/core/README.md b/packages/core/README.md index cddabaa..83beaf0 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -484,7 +484,7 @@ class ListService extends Service { addItem(item) { this.data.push(item) // return a dispose function - return this[Context.current].collect('list-item', () => { + return this.ctx.collect('list-item', () => { return this.removeItem(item) }) } @@ -506,7 +506,7 @@ class ListService extends Service { - The `addItem` method adds an item to the list and returns a dispose function which can be used to remove the item from the list. When the caller context is disposed, the disposable function will be automatically called. - The `removeItem` method removes an item from the list and returns a boolean value indicating whether the item is successfully removed. -In the above example, `addItem` is implemented as disposable via `this[Context.current].collect()`. `caller` is a special property which always points to the last context which access the service. `ctx.collect()` accepts two parameters: the first is the name of disposable, the second is the callback function. +In the above example, `addItem` is implemented as disposable via `this.ctx.collect()`. `caller` is a special property which always points to the last context which access the service. `ctx.collect()` accepts two parameters: the first is the name of disposable, the second is the callback function. #### Service isolation [↑](#contents) diff --git a/packages/core/src/reflect.ts b/packages/core/src/reflect.ts index 144eff0..a4bcd7f 100644 --- a/packages/core/src/reflect.ts +++ b/packages/core/src/reflect.ts @@ -31,7 +31,7 @@ export default class ReflectService { if (typeof prop !== 'string') return Reflect.get(target, prop, ctx) if (Reflect.has(target, prop)) { - return getTraceable(ctx, Reflect.get(target, prop, ctx)) + return getTraceable(ctx, Reflect.get(target, prop, ctx), true) } const checkInject = (name: string) => { @@ -142,6 +142,10 @@ export default class ReflectService { internal[name] = { type: 'service', builtin } this.ctx.root[key] = value this.ctx.root[symbols.isolate][name] = key + isObject(value) && defineProperty(value, symbols.tracker, { + associate: name, + property: 'ctx', + }) } accessor(name: string, options: Omit) { diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index 40f6da5..dff1c90 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -146,18 +146,14 @@ export default class Registry { plugin(plugin: Plugin, config?: any) { // check if it's a valid plugin this.resolve(plugin, true) - - // magic: this.ctx[symbols.trace] === this - // Here we ignore the reference - const ctx: C = this.ctx === this.ctx.root ? this.ctx : Object.getPrototypeOf(this.ctx) - ctx.scope.assertActive() + this.ctx.scope.assertActive() // resolve plugin config let error: any try { config = resolveConfig(plugin, config) } catch (reason) { - this.context.emit(ctx, 'internal/error', reason) + this.context.emit(this.ctx, 'internal/error', reason) error = reason config = null } @@ -166,13 +162,13 @@ export default class Registry { let runtime = this.get(plugin) if (runtime) { if (!runtime.isForkable) { - this.context.emit(ctx, 'internal/warning', new Error(`duplicate plugin detected: ${plugin.name}`)) + this.context.emit(this.ctx, 'internal/warning', new Error(`duplicate plugin detected: ${plugin.name}`)) } - return runtime.fork(ctx, config, error) + return runtime.fork(this.ctx, config, error) } - runtime = new MainScope(ctx, plugin, config, error) + runtime = new MainScope(this.ctx, plugin, config, error) this.set(plugin, runtime) - return runtime.fork(ctx, config, error) + return runtime.fork(this.ctx, config, error) } } diff --git a/packages/core/src/service.ts b/packages/core/src/service.ts index 5b82b37..9720ab6 100644 --- a/packages/core/src/service.ts +++ b/packages/core/src/service.ts @@ -95,6 +95,8 @@ export abstract class Service { static [Symbol.hasInstance](instance: any) { let constructor = instance.constructor while (constructor) { + // constructor may be a proxy + constructor = constructor.prototype?.constructor if (constructor === this) return true constructor = Object.getPrototypeOf(constructor) } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 3170d12..116b3f1 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -64,43 +64,65 @@ export function isObject(value: any): value is {} { return value && (typeof value === 'object' || typeof value === 'function') } -function isTraceable(value: any): value is {} { - return isObject(value) && !isUnproxyable(value) && symbols.tracker in value +export function getTraceable(ctx: Context, value: T, noTrap?: boolean): T { + const tracker = value?.[symbols.tracker] + if (!tracker) return value + return createTraceable(ctx, value, tracker, noTrap) } -export function getTraceable(ctx: Context, value: T): T { - if (isTraceable(value)) { - return createTraceable(ctx, value, value[symbols.tracker]) - } else { - return value - } +function createTrapMethod(ctx: Context, value: any, property: string) { + return new Proxy(value, { + apply: (target, thisArg, args) => { + return getTraceable(ctx, Reflect.apply(target, new Proxy(thisArg, { + get: (target, prop, receiver) => { + if (prop === property) { + // FIXME Can I use target[prop]? + const origin = Reflect.getOwnPropertyDescriptor(target, prop)?.value + return ctx.extend({ [symbols.source]: origin }) + } + return Reflect.get(target, prop, receiver) + }, + set: (target, prop, value, receiver) => { + if (prop === property) return false + return Reflect.set(target, prop, value, receiver) + }, + }), args)) + }, + }) } -function createTraceable(ctx: Context, value: any, tracer: Tracker) { +function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: boolean) { + if (ctx[symbols.source]) { + ctx = Object.getPrototypeOf(ctx) + } const proxy = new Proxy(value, { get: (target, prop, receiver) => { + if (prop === tracker.property) return ctx if (typeof prop === 'symbol') { return Reflect.get(target, prop, receiver) } - if (prop === tracer.property) { - const origin = Reflect.getOwnPropertyDescriptor(target, tracer.property)?.value - return ctx.extend({ [symbols.source]: origin }) + if (tracker.associate && ctx[symbols.internal][`${tracker.associate}.${prop}`]) { + return Reflect.get(ctx, `${tracker.associate}.${prop}`) } - if (!tracer.associate || !ctx[symbols.internal][`${tracer.associate}.${prop}`]) { - return getTraceable(ctx, Reflect.get(target, prop, receiver)) + const value = Reflect.get(target, prop, receiver) + const innerTracker = value?.[symbols.tracker] + if (innerTracker) { + return createTraceable(ctx, value, innerTracker) + } else if (!noTrap && tracker.property && typeof value === 'function') { + return createTrapMethod(ctx, value, tracker.property) + } else { + return value } - return ctx[`${tracer.associate}.${prop}`] }, set: (target, prop, value, receiver) => { - if (prop === tracer.property) return false + if (prop === tracker.property) return false if (typeof prop === 'symbol') { return Reflect.set(target, prop, value, receiver) } - if (!tracer.associate || !ctx[symbols.internal][`${tracer.associate}.${prop}`]) { - return Reflect.set(target, prop, value, receiver) + if (tracker.associate && ctx[symbols.internal][`${tracker.associate}.${prop}`]) { + return Reflect.set(ctx, `${tracker.associate}.${prop}`, value) } - ctx[`${tracer.associate}.${prop}`] = value - return true + return Reflect.set(target, prop, value, receiver) }, apply: (target, thisArg, args) => { return applyTraceable(proxy, target, thisArg, args) diff --git a/packages/core/tests/associate.spec.ts b/packages/core/tests/associate.spec.ts index 5f7319e..adf6caa 100644 --- a/packages/core/tests/associate.spec.ts +++ b/packages/core/tests/associate.spec.ts @@ -1,6 +1,6 @@ import { Context, Service } from '../src' import { expect } from 'chai' -import {} from './utils' +import { checkError } from './utils' describe('Association', () => { it('service injection', async () => { @@ -61,7 +61,7 @@ describe('Association', () => { associate: 'session', } - constructor(private ctx: Context) {} + constructor(public ctx: Context) {} } class Foo extends Service { @@ -70,7 +70,7 @@ describe('Association', () => { } createSession() { - return this.ctx.reflect.trace(new Session(this.ctx)) + return new Session(this.ctx) } } @@ -79,22 +79,36 @@ describe('Association', () => { } class Bar extends Service { + inject = ['foo'] + constructor(ctx: Context) { super(ctx, 'bar', true) - ctx.set('session.bar', function (this: Session) { - return this + ctx.mixin('bar', { + getBar: 'session.bar', }) } + + getBar(this: Session) { + return this + } } const root = new Context() - root.plugin(Foo) - const session = root.foo.createSession() - expect(session).to.be.instanceof(Session) - expect(session.bar).to.be.undefined - root.plugin(Bar) - expect(session.bar()).to.be.instanceof(Session) + root.inject(['foo'], (ctx) => { + const session = ctx.foo.createSession() + expect(session).to.be.instanceof(Session) + expect(session.bar).to.be.undefined + + ctx.plugin(Bar) + ctx.inject(['bar'], (ctx) => { + const session = ctx.foo.createSession() + expect(session).to.be.instanceof(Session) + expect(session.bar()).to.be.instanceof(Bar) + }) + }) + + await checkError(root) }) }) diff --git a/packages/core/tests/invoke.spec.ts b/packages/core/tests/invoke.spec.ts index 8a0a359..7604f15 100644 --- a/packages/core/tests/invoke.spec.ts +++ b/packages/core/tests/invoke.spec.ts @@ -27,7 +27,7 @@ describe('functional service', () => { return result } - reflect() { + invoke() { return this() } @@ -54,9 +54,9 @@ describe('functional service', () => { 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 }) + expect(foo3.invoke()).to.deep.equal({ a: 1, b: 2, d: 4 }) - // context tracibility - expect(foo1.reflect()).to.deep.equal({ a: 1, b: 2 }) + // context traceability + expect(foo1.invoke()).to.deep.equal({ a: 1, b: 2 }) }) }) diff --git a/packages/core/tests/service.spec.ts b/packages/core/tests/service.spec.ts index 4ab931f..298841c 100644 --- a/packages/core/tests/service.spec.ts +++ b/packages/core/tests/service.spec.ts @@ -128,7 +128,7 @@ describe('Service', () => { expect(callback.mock.calls).to.have.length(1) }) - it('traceable effect (inject)', async () => { + it('traceable effect (with inject)', async () => { class Foo extends Service { static inject = ['counter'] @@ -161,6 +161,40 @@ describe('Service', () => { expect(warning.mock.calls).to.have.length(0) }) + it('traceable effect (without inject)', async () => { + class Foo extends Service { + constructor(ctx: Context) { + super(ctx, 'foo', true) + } + + count() { + this.ctx.counter.increse() + return this.ctx.counter.value + } + } + + const root = new Context() + const warning = mock.fn() + root.on('internal/warning', warning) + root.set('counter', new Counter(root)) + + root.plugin(Foo) + expect(root.foo.count()).to.equal(1) + expect(root.foo.count()).to.equal(2) + expect(warning.mock.calls).to.have.length(0) // access from root + + const fork = root.inject(['foo'], (ctx) => { + expect(ctx.foo.count()).to.equal(3) + expect(ctx.foo.count()).to.equal(4) + expect(warning.mock.calls).to.have.length(4) + }) + + fork.dispose() + expect(root.foo.count()).to.equal(3) + + await checkError(root) + }) + it('dependency update', async () => { const callback = mock.fn((foo: any) => {}) const dispose = mock.fn((foo: any) => {})