Skip to content

Commit

Permalink
feat(cordis): support shadow in service accessors
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jun 11, 2024
1 parent 49f0cf8 commit d373e08
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 74 deletions.
18 changes: 9 additions & 9 deletions packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,16 @@ export default class Lifecycle {
}, { global: true }), Context.static, ctx.scope)

// inject in ancestor contexts
defineProperty(this.on('internal/inject', function (this: Context, name) {
let ctx = this
while (ctx !== ctx.root) {
if (Reflect.ownKeys(ctx).includes('scope')) {
for (const key of ctx.runtime.inject) {
if (name === ReflectService.resolveInject(ctx, key)[0]) return true
}
}
ctx = Object.getPrototypeOf(ctx)
const checkInject = (scope: EffectScope, name: string) => {
if (!scope.runtime.plugin) return false
for (const key of scope.runtime.inject) {
if (name === ReflectService.resolveInject(scope.ctx, key)[0]) return true
}
return checkInject(scope.parent.scope, name)
}

defineProperty(this.on('internal/inject', function (this: Context, name) {
return checkInject(this.scope, name)
}, { global: true }), Context.static, ctx.scope)
}

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/reflect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineProperty, Dict, isNullable } from 'cosmokit'
import { Context } from './context'
import { createMixin, getTraceable, isObject, isUnproxyable, symbols } from './utils'
import { getTraceable, isObject, isUnproxyable, symbols, withProps } from './utils'

declare module './context' {
interface Context {
Expand Down Expand Up @@ -168,14 +168,14 @@ export default class ReflectService {
get(receiver) {
const service = getTarget(this)
if (isNullable(service)) return service
const mixin = createMixin(service, receiver)
const mixin = receiver ? withProps(receiver, service) : service
const value = Reflect.get(service, key, mixin)
if (typeof value !== 'function') return value
return value.bind(mixin ?? service)
},
set(value, receiver) {
const service = getTarget(this)
const mixin = createMixin(service, receiver)
const mixin = receiver ? withProps(receiver, service) : service
return Reflect.set(service, key, value, mixin)
},
})
Expand Down
82 changes: 38 additions & 44 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,39 @@ export function getTraceable<T>(ctx: Context, value: T, noTrap?: boolean): T {
return createTraceable(ctx, value, tracker, noTrap)
}

function createShadowMethod(ctx: Context, value: any, outer: any, property: string) {
export function withProps(target: any, props?: {}) {
if (!props) return target
return new Proxy(target, {
get: (target, prop, receiver) => {
if (prop in props) return Reflect.get(props, prop, receiver)
return Reflect.get(target, prop, receiver)
},
set: (target, prop, value, receiver) => {
if (prop in props) return Reflect.set(props, prop, value, receiver)
return Reflect.set(target, prop, value, receiver)
},
})
}

function withProp(target: any, prop: string | symbol, value: any) {
return withProps(target, Object.defineProperty(Object.create(null), prop, {
value,
writable: false,
}))
}

function createShadow(ctx: Context, target: any, property: string | undefined, receiver: any) {
if (!property) return receiver
const origin = Reflect.getOwnPropertyDescriptor(target, property)?.value
if (!origin) return receiver
return withProp(receiver, property, ctx.extend({ [symbols.shadow]: origin }))
}

function createShadowMethod(ctx: Context, value: any, outer: any, shadow: {}) {
return new Proxy(value, {
apply: (target, thisArg, args) => {
const isBound = thisArg === outer

// contravariant
thisArg = new Proxy(thisArg, {
get: (target, prop, receiver) => {
if (prop === property && isBound) {
const origin = Reflect.getOwnPropertyDescriptor(target, prop)?.value
return ctx.extend({ [symbols.shadow]: origin })
}
return Reflect.get(target, prop, receiver)
},
set: (target, prop, value, receiver) => {
if (prop === property) return false
return Reflect.set(target, prop, value, receiver)
},
})

if (thisArg === outer) thisArg = shadow
// contravariant
args = args.map((arg) => {
if (typeof arg !== 'function') return arg
Expand All @@ -108,14 +121,12 @@ function createShadowMethod(ctx: Context, value: any, outer: any, property: stri
},
})
})

// covariant
return getTraceable(ctx, Reflect.apply(target, thisArg, args))
},
})
}

// covariant
function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: boolean) {
if (ctx[symbols.shadow]) {
ctx = Object.getPrototypeOf(ctx)
Expand All @@ -127,19 +138,15 @@ function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: bo
return Reflect.get(target, prop, receiver)
}
if (tracker.associate && ctx[symbols.internal][`${tracker.associate}.${prop}`]) {
return Reflect.get(ctx, `${tracker.associate}.${prop}`, new Proxy(ctx, {
get: (target2, prop2, receiver2) => {
if (prop2 === symbols.receiver) return receiver
return Reflect.get(target2, prop2, receiver2)
},
}))
return Reflect.get(ctx, `${tracker.associate}.${prop}`, withProp(ctx, symbols.receiver, receiver))
}
const innerValue = Reflect.get(target, prop, receiver)
const shadow = createShadow(ctx, target, tracker.property, receiver)
const innerValue = Reflect.get(target, prop, shadow)
const innerTracker = innerValue?.[symbols.tracker]
if (innerTracker) {
return createTraceable(ctx, innerValue, innerTracker)
} else if (!noTrap && tracker.property && typeof innerValue === 'function') {
return createShadowMethod(ctx, innerValue, receiver, tracker.property)
} else if (!noTrap && typeof innerValue === 'function') {
return createShadowMethod(ctx, innerValue, receiver, shadow)
} else {
return innerValue
}
Expand All @@ -150,14 +157,10 @@ function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: bo
return Reflect.set(target, prop, value, receiver)
}
if (tracker.associate && ctx[symbols.internal][`${tracker.associate}.${prop}`]) {
return Reflect.set(ctx, `${tracker.associate}.${prop}`, value, new Proxy(ctx, {
get: (target2, prop2, receiver2) => {
if (prop2 === symbols.receiver) return receiver
return Reflect.get(target2, prop2, receiver2)
},
}))
return Reflect.set(ctx, `${tracker.associate}.${prop}`, value, withProp(ctx, symbols.receiver, receiver))
}
return Reflect.set(target, prop, value, receiver)
const shadow = createShadow(ctx, target, tracker.property, receiver)
return Reflect.set(target, prop, value, shadow)
},
apply: (target, thisArg, args) => {
return applyTraceable(proxy, target, thisArg, args)
Expand All @@ -179,12 +182,3 @@ export function createCallable(name: string, proto: {}, tracker: Tracker) {
defineProperty(self, 'name', name)
return Object.setPrototypeOf(self, proto)
}

export function createMixin(service: {}, receiver: any) {
return receiver ? new Proxy(receiver, {
get: (target, prop, receiver) => {
if (prop in service) return Reflect.get(service, prop, receiver)
return Reflect.get(target, prop, receiver)
},
}) : service
}
44 changes: 27 additions & 17 deletions packages/core/tests/service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,13 @@ describe('Service', () => {
super(ctx, 'foo', true)
}

count() {
this.ctx.counter.increse()
get value() {
return this.ctx.counter.value
}

increase() {
return this.ctx.counter.increase()
}
}

const root = new Context()
Expand All @@ -148,19 +151,22 @@ describe('Service', () => {
root.set('counter', new Counter(root))

root.plugin(Foo)
expect(root.foo.count()).to.equal(1)
expect(root.foo.count()).to.equal(2)
root.foo.increase()
expect(root.foo.value).to.equal(1)
expect(warning.mock.calls).to.have.length(0)

const fork = root.inject(['foo'], (ctx) => {
expect(ctx.foo.count()).to.equal(3)
expect(ctx.foo.count()).to.equal(4)
root.foo.increase()
expect(ctx.foo.value).to.equal(2)
expect(warning.mock.calls).to.have.length(0)
})

fork.dispose()
expect(root.foo.count()).to.equal(3)
root.foo.increase()
expect(root.foo.value).to.equal(3)
expect(warning.mock.calls).to.have.length(0)

await checkError(root)
})

it('traceable effect (without inject)', async () => {
Expand All @@ -169,10 +175,13 @@ describe('Service', () => {
super(ctx, 'foo', true)
}

count() {
this.ctx.counter.increse()
get value() {
return this.ctx.counter.value
}

increase() {
return this.ctx.counter.increase()
}
}

const root = new Context()
Expand All @@ -181,19 +190,20 @@ describe('Service', () => {
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(4)
root.foo.increase()
expect(root.foo.value).to.equal(1)
expect(warning.mock.calls).to.have.length(2)

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(8)
root.foo.increase()
expect(root.foo.value).to.equal(2)
expect(warning.mock.calls).to.have.length(4)
})

fork.dispose()
expect(root.foo.count()).to.equal(3)
expect(warning.mock.calls).to.have.length(10)
root.foo.increase()
expect(root.foo.value).to.equal(3)
expect(warning.mock.calls).to.have.length(6)

await checkError(root)
})
Expand Down
2 changes: 1 addition & 1 deletion packages/core/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class Counter {

constructor(public ctx: Context) {}

increse() {
increase() {
return this.ctx.effect(() => {
this.value++
return () => this.value--
Expand Down

0 comments on commit d373e08

Please sign in to comment.