Skip to content

Commit

Permalink
feat(cordis): support Serivce.tracker for tracibility
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jun 7, 2024
1 parent 90257b2 commit e06b3f3
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 41 deletions.
11 changes: 5 additions & 6 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, getTraceable, isObject, isUnproxyable, resolveConfig, symbols } from './utils.ts'
import { getTraceable, isObject, isUnproxyable, resolveConfig, symbols } from './utils.ts'

export namespace Context {
export type Parameterized<C, T = any> = C & { config: T }
Expand Down Expand Up @@ -202,8 +202,7 @@ export class Context {
const internal = this[symbols.internal][name]
if (internal?.type !== 'service') return
const value = this.root[this[symbols.isolate][name]]
if (!isObject(value) || isUnproxyable(value)) return value
return createTraceable(this, value, name)
return getTraceable(this, value)
}

set<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
Expand Down Expand Up @@ -238,9 +237,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 (isObject(value)) {
defineProperty(value, symbols.source, ctx)
}
ctx.emit(self, 'internal/service', name, oldValue)
return dispose
}
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ export class Lifecycle {
_hooks: Record<keyof any, Hook[]> = {}

constructor(private ctx: Context) {
defineProperty(this, symbols.trace, 'lifecycle')
defineProperty(this, symbols.tracker, {
associate: 'lifecycle',
property: 'ctx',
})

defineProperty(this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) {
const method = options.prepend ? 'unshift' : 'push'
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ export class Registry<C extends Context = Context> {
protected context: Context

constructor(public ctx: C, config: any) {
defineProperty(this, symbols.trace, 'registry')
defineProperty(this, symbols.tracker, {
associate: 'registry',
property: 'ctx',
})

this.context = ctx
const runtime = new MainScope(ctx, null!, config)
ctx.scope = runtime
Expand Down
16 changes: 9 additions & 7 deletions packages/core/src/service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Awaitable, defineProperty } from 'cosmokit'
import { Context } from './context.ts'
import { createCallable, joinPrototype, symbols } from './utils.ts'
import { createCallable, joinPrototype, symbols, Tracker } from './utils.ts'
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 invoke: unique symbol = symbols.invoke as any
static readonly extend: unique symbol = symbols.extend as any
static readonly tracker: unique symbol = symbols.tracker 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> {}
Expand Down Expand Up @@ -40,8 +40,12 @@ export abstract class Service<T = unknown, C extends Context = Context> {
immediate ??= this.constructor[symbols.immediate]

let self = this
const tracker: Tracker = {
associate: name,
property: 'ctx',
}
if (self[symbols.invoke]) {
self = createCallable(name, joinPrototype(Object.getPrototypeOf(this), Function.prototype))
self = createCallable(name, joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker)
}
if (_ctx) {
self.ctx = _ctx
Expand All @@ -50,7 +54,7 @@ export abstract class Service<T = unknown, C extends Context = Context> {
}
self.name = name
self.config = config
defineProperty(self, symbols.trace, name)
defineProperty(self, symbols.tracker, tracker)

self.ctx.provide(name)
self.ctx.runtime.name = name
Expand Down Expand Up @@ -81,12 +85,10 @@ export abstract class Service<T = unknown, C extends Context = Context> {
protected [symbols.extend](props?: any) {
let self: any
if (this[Service.invoke]) {
self = createCallable(this.name, this)
self = createCallable(this.name, this, this[symbols.tracker])
} else {
self = Object.create(this)
}
defineProperty(self, symbols.trace, this.name)
// defineProperty(self, symbols.origin, this.ctx)
return Object.assign(self, props)
}

Expand Down
47 changes: 25 additions & 22 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { defineProperty } from 'cosmokit'
import type { Context, Service } from './index.ts'

export interface Tracker {
associate?: string
property?: string
}

export const symbols = {
// context symbols
source: Symbol.for('cordis.source') as typeof Context.source,
Expand All @@ -13,10 +18,10 @@ 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,
invoke: Symbol.for('cordis.invoke') as typeof Service.invoke,
extend: Symbol.for('cordis.extend') as typeof Service.extend,
tracker: Symbol.for('cordis.tracker') as typeof Service.tracker,
provide: Symbol.for('cordis.provide') as typeof Service.provide,
immediate: Symbol.for('cordis.immediate') as typeof Service.immediate,
}
Expand Down Expand Up @@ -59,44 +64,42 @@ 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: any, value: any) {
if (isObject(value) && symbols.trace in value) {
return createTraceable(ctx, value, value[symbols.trace] as any)
if (isTraceable(value)) {
return createTraceable(ctx, value, value[symbols.tracker])
} else {
return value
}
}

export function createTraceable(ctx: any, value: any, name: string) {
function createTraceable(ctx: any, value: any, tracer: Tracker) {
const proxy = new Proxy(value, {
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}`]) {
if (prop === tracer.property) {
const origin = Reflect.getOwnPropertyDescriptor(target, tracer.property)?.value
return ctx.extend({ [symbols.source]: origin })
}
if (!tracer.associate || !ctx[symbols.internal][`${tracer.associate}.${prop}`]) {
return getTraceable(ctx, Reflect.get(target, prop, receiver))
}

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

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

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

ctx[`${name}.${prop}`] = value
ctx[`${tracer.associate}.${prop}`] = value
return true
},
apply: (target, thisArg, args) => {
Expand All @@ -106,14 +109,14 @@ export function createTraceable(ctx: any, value: any, name: string) {
return proxy
}

export function applyTraceable(proxy: any, value: any, thisArg: any, args: any[]) {
function applyTraceable(proxy: any, value: any, thisArg: any, args: any[]) {
if (!value[symbols.invoke]) return Reflect.apply(value, thisArg, args)
return value[symbols.invoke].apply(proxy, args)
}

export function createCallable(name: string, proto: {}) {
export function createCallable(name: string, proto: {}, tracker: Tracker) {
const self = function (...args: any[]) {
const proxy = createTraceable(self['ctx'], self, name)
const proxy = createTraceable(self['ctx'], self, tracker)
return applyTraceable(proxy, self, this, args)
}
defineProperty(self, 'name', name)
Expand Down
8 changes: 6 additions & 2 deletions packages/core/tests/associate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ describe('Association', () => {

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

constructor(private ctx: Context) {}
}

class Foo extends Service {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { use } from 'chai'
import { Context } from '../src'
import { Context, Service } from '../src'
import promised from 'chai-as-promised'
import { Dict } from 'cosmokit'

Expand Down Expand Up @@ -44,6 +44,11 @@ declare module '../src/events' {
}

export class Counter {
[Service.tracker] = {
associate: 'counter',
property: 'ctx',
}

value = 0

constructor(public ctx: Context) {}
Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/isolate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function apply(ctx: Context) {
for (const symbol of [oldMap[key], this.newMap[key]]) {
const value = symbol && entry.ctx[symbol]
if (!(value instanceof Object)) continue
const source = Reflect.getOwnPropertyDescriptor(value, Context.origin)?.value
const source = Reflect.getOwnPropertyDescriptor(value, Context.source)?.value
if (!source) {
entry.ctx.emit('internal/warning', new Error(`expected service ${key} to be implemented`))
continue
Expand Down

0 comments on commit e06b3f3

Please sign in to comment.