Skip to content

Commit

Permalink
feat(cordis): use trapped method for services
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jun 8, 2024
1 parent cb0d85d commit 9909463
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 49 deletions.
4 changes: 2 additions & 2 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
Expand All @@ -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)

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/reflect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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<Context.Internal.Accessor, 'type'>) {
Expand Down
16 changes: 6 additions & 10 deletions packages/core/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,14 @@ export default class Registry<C extends Context = Context> {
plugin(plugin: Plugin<C>, 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
}
Expand All @@ -166,13 +162,13 @@ export default class Registry<C extends Context = Context> {
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)
}
}
2 changes: 2 additions & 0 deletions packages/core/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export abstract class Service<T = unknown, C extends Context = Context> {
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)
}
Expand Down
62 changes: 42 additions & 20 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(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<T>(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)
Expand Down
36 changes: 25 additions & 11 deletions packages/core/tests/associate.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('Association', () => {
associate: 'session',
}

constructor(private ctx: Context) {}
constructor(public ctx: Context) {}
}

class Foo extends Service {
Expand All @@ -70,7 +70,7 @@ describe('Association', () => {
}

createSession() {
return this.ctx.reflect.trace(new Session(this.ctx))
return new Session(this.ctx)
}
}

Expand All @@ -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)
})
})
8 changes: 4 additions & 4 deletions packages/core/tests/invoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('functional service', () => {
return result
}

reflect() {
invoke() {
return this()
}

Expand All @@ -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 })
})
})
36 changes: 35 additions & 1 deletion packages/core/tests/service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down Expand Up @@ -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) => {})
Expand Down

0 comments on commit 9909463

Please sign in to comment.