Skip to content

Commit

Permalink
feat(cordis): support traceable object
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jun 7, 2024
1 parent c9be0aa commit 90257b2
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 114 deletions.
60 changes: 25 additions & 35 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineProperty, Dict, isNullable } from 'cosmokit'
import { Lifecycle } from './events.ts'
import { Registry } from './registry.ts'
import { createTraceable, isUnproxyable, resolveConfig, symbols } from './utils.ts'
import { createTraceable, getTraceable, isObject, isUnproxyable, resolveConfig, symbols } from './utils.ts'

export namespace Context {
export type Parameterized<C, T = any> = C & { config: T }
Expand Down Expand Up @@ -50,16 +50,16 @@ export interface Context {
}

export class Context {
static readonly origin: unique symbol = symbols.origin as any
static readonly source: unique symbol = symbols.source as any
static readonly events: unique symbol = symbols.events as any
static readonly static: unique symbol = symbols.static as any
static readonly filter: unique symbol = symbols.filter as any
static readonly expose: unique symbol = symbols.expose as any
static readonly isolate: unique symbol = symbols.isolate as any
static readonly internal: unique symbol = symbols.internal as any
static readonly intercept: unique symbol = symbols.intercept as any
/** @deprecated use `Context.origin` instead */
static readonly current: typeof Context.origin = Context.origin
static readonly origin = 'ctx'
static readonly current = 'ctx'

static is<C extends Context>(value: any): value is C {
return !!value?.[Context.is as any]
Expand Down Expand Up @@ -92,6 +92,10 @@ export class Context {
get(target, prop, ctx: Context) {
if (typeof prop !== 'string') return Reflect.get(target, prop, ctx)

if (Reflect.has(target, prop)) {
return getTraceable(ctx, Reflect.get(target, prop, ctx))
}

const checkInject = (name: string) => {
// Case 1: a normal property defined on context
if (Reflect.has(target, name)) return
Expand Down Expand Up @@ -125,7 +129,10 @@ export class Context {
if (typeof prop !== 'string') return Reflect.set(target, prop, value, ctx)

const [name, internal] = Context.resolveInject(ctx, prop)
if (!internal) return Reflect.set(target, name, value, ctx)
if (!internal) {
// TODO
return Reflect.set(target, name, value, ctx)
}
if (internal.type === 'accessor') {
if (!internal.set) return false
return internal.set.call(ctx, value)
Expand All @@ -137,35 +144,22 @@ export class Context {
},
}

/** @deprecated use `Service.traceable` instead */
static associate<T extends {}>(object: T, name: string) {
return new Proxy(object, {
get(target, key, receiver) {
if (typeof key === 'symbol') return Reflect.get(target, key, receiver)
const caller: Context = receiver[symbols.origin]
if (!caller?.[symbols.internal][`${name}.${key}`]) return Reflect.get(target, key, receiver)
return caller[`${name}.${key}`]
},
set(target, key, value, receiver) {
if (typeof key === 'symbol') return Reflect.set(target, key, value, receiver)
const caller: Context = receiver[symbols.origin]
if (!caller?.[symbols.internal][`${name}.${key}`]) return Reflect.set(target, key, value, receiver)
caller[`${name}.${key}`] = value
return true
},
})
return object
}

constructor(config?: any) {
const self: Context = new Proxy(this, Context.handler)
config = resolveConfig(this.constructor, config)
self[symbols.isolate] = Object.create(null)
self[symbols.intercept] = Object.create(null)
this[symbols.isolate] = Object.create(null)
this[symbols.intercept] = Object.create(null)
const self: Context = new Proxy(this, Context.handler)
self.root = self
self.registry = new Registry(self, config)
self.lifecycle = new Lifecycle(self)
self.mixin('scope', ['config', 'runtime', 'effect', 'collect', 'accept', 'decline'])
self.mixin('registry', ['using', 'inject', 'plugin', 'dispose'])
self.mixin('lifecycle', ['on', 'once', 'off', 'after', 'parallel', 'emit', 'serial', 'bail', 'start', 'stop'])
self.provide('registry', new Registry(self, config!), true)
self.provide('lifecycle', new Lifecycle(self), true)

const attach = (internal: Context[typeof symbols.internal]) => {
if (!internal) return
Expand All @@ -174,7 +168,7 @@ export class Context {
const constructor = internal[key]['prototype']?.constructor
if (!constructor) continue
self[internal[key]['key']] = new constructor(self, config)
defineProperty(self[internal[key]['key']], symbols.origin, self)
defineProperty(self[internal[key]['key']], 'ctx', self)
}
}
attach(this[symbols.internal])
Expand Down Expand Up @@ -208,12 +202,8 @@ export class Context {
const internal = this[symbols.internal][name]
if (internal?.type !== 'service') return
const value = this.root[this[symbols.isolate][name]]
if (!value || typeof value !== 'object' && typeof value !== 'function') return value
if (isUnproxyable(value)) {
defineProperty(value, symbols.origin, this)
return value
}
return createTraceable(this, value)
if (!isObject(value) || isUnproxyable(value)) return value
return createTraceable(this, value, name)
}

set<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
Expand Down Expand Up @@ -248,9 +238,9 @@ export class Context {

ctx.emit(self, 'internal/before-service', name, value)
ctx.root[key] = value
if (value instanceof Object) {
defineProperty(value, symbols.origin, ctx)
}
// if (value instanceof Object) {
// defineProperty(value, symbols.origin, ctx)
// }
ctx.emit(self, 'internal/service', name, oldValue)
return dispose
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class Lifecycle {
_hooks: Record<keyof any, Hook[]> = {}

constructor(private ctx: Context) {
defineProperty(this, Context.origin, ctx)
defineProperty(this, symbols.trace, 'lifecycle')

defineProperty(this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) {
const method = options.prepend ? 'unshift' : 'push'
Expand Down Expand Up @@ -105,7 +105,7 @@ export class Lifecycle {
if (name === Context.resolveInject(ctx, key)[0]) return true
}
}
ctx = ctx[symbols.trace] ?? Object.getPrototypeOf(ctx)
ctx = ctx[symbols.source] ?? Object.getPrototypeOf(ctx)
}
}, { global: true }), Context.static, ctx.scope)
}
Expand Down
6 changes: 3 additions & 3 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 } from './scope.ts'
import { resolveConfig } from './utils.ts'
import { resolveConfig, symbols } from './utils.ts'

export function isApplicable(object: Plugin) {
return object && typeof object === 'object' && typeof object.apply === 'function'
Expand Down Expand Up @@ -67,7 +67,7 @@ export class Registry<C extends Context = Context> {
protected context: Context

constructor(public ctx: C, config: any) {
defineProperty(this, Context.origin, ctx)
defineProperty(this, symbols.trace, 'registry')
this.context = ctx
const runtime = new MainScope(ctx, null!, config)
ctx.scope = runtime
Expand Down Expand Up @@ -145,7 +145,7 @@ export class Registry<C extends Context = Context> {

// magic: this.ctx[symbols.trace] === this
// Here we ignore the reference
const ctx: C = Object.getPrototypeOf(this.ctx)
const ctx: C = this.ctx === this.ctx.root ? this.ctx : Object.getPrototypeOf(this.ctx)
ctx.scope.assertActive()

// resolve plugin config
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ import { Spread } from './registry.ts'

export abstract class Service<T = unknown, C extends Context = Context> {
static readonly setup: unique symbol = symbols.setup as any
static readonly trace: unique symbol = symbols.trace as any
static readonly invoke: unique symbol = symbols.invoke as any
static readonly extend: unique symbol = symbols.extend as any
static readonly provide: unique symbol = symbols.provide as any
static readonly immediate: unique symbol = symbols.immediate as any
static readonly trace: unique symbol = symbols.trace as any

protected start(): Awaitable<void> {}
protected stop(): Awaitable<void> {}
protected fork?(ctx: C, config: any): void

protected ctx!: C
protected [symbols.origin]!: C

public name!: string
public config!: T
Expand Down Expand Up @@ -51,7 +50,7 @@ export abstract class Service<T = unknown, C extends Context = Context> {
}
self.name = name
self.config = config
defineProperty(self, symbols.origin, self.ctx)
defineProperty(self, symbols.trace, name)

self.ctx.provide(name)
self.ctx.runtime.name = name
Expand All @@ -68,7 +67,7 @@ export abstract class Service<T = unknown, C extends Context = Context> {
})

self.ctx.on('dispose', () => self.stop())
return Context.associate(self, name)
return self
}

protected [symbols.filter](ctx: Context) {
Expand All @@ -86,8 +85,9 @@ export abstract class Service<T = unknown, C extends Context = Context> {
} else {
self = Object.create(this)
}
defineProperty(self, symbols.origin, this.ctx)
return Context.associate<this>(Object.assign(self, props), this.name)
defineProperty(self, symbols.trace, this.name)
// defineProperty(self, symbols.origin, this.ctx)
return Object.assign(self, props)
}

static [Symbol.hasInstance](instance: any) {
Expand Down
53 changes: 44 additions & 9 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Context, Service } from './index.ts'

export const symbols = {
// context symbols
origin: Symbol.for('cordis.origin') as typeof Context.origin,
source: Symbol.for('cordis.source') as typeof Context.source,
events: Symbol.for('cordis.events') as typeof Context.events,
static: Symbol.for('cordis.static') as typeof Context.static,
filter: Symbol.for('cordis.filter') as typeof Context.filter,
Expand All @@ -13,8 +13,8 @@ export const symbols = {
intercept: Symbol.for('cordis.intercept') as typeof Context.intercept,

// service symbols
trace: Symbol.for('cordis.traceable') as typeof Service.trace,
setup: Symbol.for('cordis.setup') as typeof Service.setup,
trace: Symbol.for('cordis.trace') as typeof Service.trace,
invoke: Symbol.for('cordis.invoke') as typeof Service.invoke,
extend: Symbol.for('cordis.extend') as typeof Service.extend,
provide: Symbol.for('cordis.provide') as typeof Service.provide,
Expand Down Expand Up @@ -55,14 +55,49 @@ export function joinPrototype(proto1: {}, proto2: {}) {
return result
}

export function createTraceable(ctx: any, value: any) {
export function isObject(value: any): value is {} {
return value && (typeof value === 'object' || typeof value === 'function')
}

export function getTraceable(ctx: any, value: any) {
if (isObject(value) && symbols.trace in value) {
return createTraceable(ctx, value, value[symbols.trace] as any)
} else {
return value
}
}

export function createTraceable(ctx: any, value: any, name: string) {
const proxy = new Proxy(value, {
get: (target, name, receiver) => {
if (name === symbols.origin || name === 'ctx') {
const origin = Reflect.getOwnPropertyDescriptor(target, symbols.origin)?.value
return ctx.extend({ [symbols.trace]: origin })
get: (target, prop, receiver) => {
if (prop === 'ctx') {
const origin = Reflect.getOwnPropertyDescriptor(target, 'ctx')?.value
return ctx.extend({ [symbols.source]: origin })
}

if (typeof prop === 'symbol') {
return Reflect.get(target, prop, receiver)
}

if (!ctx[symbols.internal][`${name}.${prop}`]) {
return getTraceable(ctx, Reflect.get(target, prop, receiver))
}

return ctx[`${name}.${prop}`]
},
set: (target, prop, value, receiver) => {
if (prop === 'ctx') return false

if (typeof prop === 'symbol') {
return Reflect.set(target, prop, value, receiver)
}

if (!ctx[symbols.internal][`${name}.${prop}`]) {
return Reflect.set(target, prop, value, receiver)
}
return Reflect.get(target, name, receiver)

ctx[`${name}.${prop}`] = value
return true
},
apply: (target, thisArg, args) => {
return applyTraceable(proxy, target, thisArg, args)
Expand All @@ -78,7 +113,7 @@ export function applyTraceable(proxy: any, value: any, thisArg: any, args: any[]

export function createCallable(name: string, proto: {}) {
const self = function (...args: any[]) {
const proxy = createTraceable(self[symbols.origin], self)
const proxy = createTraceable(self['ctx'], self, name)
return applyTraceable(proxy, self, this, args)
}
defineProperty(self, 'name', name)
Expand Down
34 changes: 24 additions & 10 deletions packages/core/tests/associate.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context, Service } from '../src'
import { Context, getTraceable, Service } from '../src'
import { expect } from 'chai'
import {} from './utils'

Expand Down Expand Up @@ -55,20 +55,30 @@ describe('Association', () => {
})

it('associated type', async () => {
interface Session {
bar(): this
class Session {
[Service.trace] = 'session'
constructor(ctx: Context) {}
}

class Session {
class Foo extends Service {
session: any = Session.prototype

constructor(ctx: Context) {
this[Context.origin] = ctx
return Context.associate(this, 'session')
super(ctx, 'foo', true)
}

createSession() {
return getTraceable(this.ctx, new this.session.constructor(this.ctx))
}
}

class Foo extends Service {
interface Session {
bar(): this
}

class Bar extends Service {
constructor(ctx: Context) {
super(ctx, 'foo', true)
super(ctx, 'bar', true)
ctx.provide('session.bar')
ctx['session.bar'] = function (this: Session) {
return this
Expand All @@ -77,9 +87,13 @@ describe('Association', () => {
}

const root = new Context()
const session = new Session(root)

root.plugin(Foo)
expect(root.foo).to.be.instanceof(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)
})
})
Loading

0 comments on commit 90257b2

Please sign in to comment.