Skip to content

Commit

Permalink
feat(core): support Context.invoke for functional service
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 14, 2024
1 parent f692eb8 commit 4fbf8bf
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 74 deletions.
11 changes: 0 additions & 11 deletions packages/cordis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,4 @@ export abstract class Service<C extends Context = Context> extends core.Service<
}
}

export abstract class FunctionalService<C extends Context = Context> extends core.FunctionalService<C> {
static Context = Context

public logger: logger.Logger

constructor(ctx: C | undefined, name: string, options?: boolean | core.Service.Options) {
super(ctx, name, options)
this.logger = this.ctx.logger(name)
}
}

export default function () {}
35 changes: 23 additions & 12 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface Context {
}

export class Context {
static readonly invoke = Symbol.for('cordis.invoke')
static readonly config = Symbol.for('cordis.config')
static readonly events = Symbol.for('cordis.events')
static readonly static = Symbol.for('cordis.static')
Expand All @@ -65,6 +66,24 @@ export class Context {
static readonly internal = Symbol.for('cordis.internal')
static readonly intercept = Symbol.for('cordis.intercept')

static createProxy(ctx: any, value: any) {
const proxy = new Proxy(value, {
get: (target, name, receiver) => {
if (name === Context.current || name === 'caller') return ctx
return Reflect.get(target, name, receiver)
},
apply: (target, thisArg, args) => {
return Context.applyProxy(proxy, target, thisArg, args)
},
})
return proxy
}

static applyProxy(proxy: any, value: any, thisArg: any, args: any[]) {
if (!value[Context.invoke]) return Reflect.apply(value, thisArg, args)
return value[Context.invoke].apply(proxy, args)
}

private static ensureInternal(): Context[typeof Context.internal] {
const ctx = this.prototype || this
if (Object.prototype.hasOwnProperty.call(ctx, Context.internal)) {
Expand Down Expand Up @@ -183,7 +202,7 @@ export class Context {
if (typeof key === 'symbol' || key in target) return Reflect.get(target, key, receiver)
const caller: Context = receiver[Context.current]
if (!caller?.[Context.internal][`${name}.${key}`]) return Reflect.get(target, key, receiver)
return caller.get(`${name}.${key}`, receiver)
return caller.get(`${name}.${key}`)
},
set(target, key, value, receiver) {
if (typeof key === 'symbol' || key in target) return Reflect.set(target, key, value, receiver)
Expand Down Expand Up @@ -244,8 +263,8 @@ export class Context {
}

get<K extends string & keyof this>(name: K): undefined | this[K]
get(name: string, receiver?: any): any
get(name: string, receiver?: any) {
get(name: string): any
get(name: string) {
const internal = this[Context.internal][name]
if (internal?.type !== 'service') return
const key: symbol = this[Context.shadow][name] || internal.key
Expand All @@ -255,15 +274,7 @@ export class Context {
defineProperty(value, Context.current, this)
return value
}
return new Proxy(value, {
get: (target, name, receiver) => {
if (name === Context.current || name === 'caller') return this
return Reflect.get(target, name, receiver)
},
apply: receiver ? undefined : (target, thisArg, args) => {
return target.call(this, ...args)
},
})
return Context.createProxy(this, value)
}

provide(name: string, value?: any, builtin?: boolean) {
Expand Down
85 changes: 41 additions & 44 deletions packages/core/src/service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
import { Awaitable, defineProperty } from 'cosmokit'
import { Context } from './context.ts'

const kSetup = Symbol('cordis.service.setup')
const kSetup = Symbol.for('cordis.service.setup')

export namespace Service {
export interface Options {
name?: string
immediate?: boolean
standalone?: boolean
}
}

function makeFunctional(proto: {}) {
if (proto === Object.prototype) return Function.prototype
const result = Object.create(makeFunctional(Object.getPrototypeOf(proto)))
for (const key of Object.getOwnPropertyNames(proto)) {
Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(proto, key)!)
}
for (const key of Object.getOwnPropertySymbols(proto)) {
Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(proto, key)!)
}
return result
}

export abstract class Service<C extends Context = Context> {
static immediate = false
static Context = Context

public [kSetup](ctx: C | undefined, name: string, options?: boolean | Service.Options) {
protected start(): Awaitable<void> {}
protected stop(): Awaitable<void> {}
protected fork?(ctx: C, config: any): void

protected ctx!: C
protected [Context.current]!: C

constructor(ctx: C | undefined, public readonly name: string, options?: boolean | Service.Options) {
let self: any = this
if (self[Context.invoke]) {
// functional service
self = function (...args: any[]) {
const proxy = Context.createProxy(ctx, self)
return Context.applyProxy(proxy, self, this, args)
}
defineProperty(self, 'name', name)
Object.setPrototypeOf(self, makeFunctional(Object.getPrototypeOf(this)))
}
return self[kSetup](ctx, name, options)
}

[Context.filter](ctx: Context) {
return ctx[Context.shadow][this.name] === this.ctx[Context.shadow][this.name]
}

[kSetup](ctx: C | undefined, name: string, options?: boolean | Service.Options) {
this.ctx = ctx ?? new (this.constructor as any).Context()
this.ctx.provide(name)
defineProperty(this, Context.current, ctx)
Expand All @@ -35,46 +74,4 @@ export abstract class Service<C extends Context = Context> {

return Context.associate(this, name)
}

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

protected ctx!: C
protected [Context.current]!: C

constructor(ctx: C | undefined, public readonly name: string, options?: boolean | Service.Options) {
return this[kSetup](ctx, name, options)
}

[Context.filter](ctx: Context) {
return ctx[Context.shadow][this.name] === this.ctx[Context.shadow][this.name]
}
}

export interface FunctionalService {
(...args: this['call'] extends (thisArg: any, ...rest: infer R) => any ? R : never): ReturnType<this['call']>
}

export abstract class FunctionalService<C extends Context = Context> extends Function {
static Context = Context

abstract call(ctx: C, ...args: any[]): any

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

protected ctx!: C
protected [Context.current]!: C

constructor(ctx: C | undefined, name: string, options?: boolean | Service.Options) {
super()
const self = function (this: C, ...args: any[]) {
return self.call(ctx, ...args)
}
defineProperty(self, 'name', name)
Object.setPrototypeOf(self, Object.getPrototypeOf(this))
return Service.prototype[kSetup].call(self, ctx, name, options) as any
}
}
23 changes: 16 additions & 7 deletions packages/core/tests/service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context, FunctionalService, Service } from '../src'
import { Context, Service } from '../src'
import { defineProperty, noop } from 'cosmokit'
import { expect } from 'chai'
import { describe, mock, test } from 'node:test'
Expand Down Expand Up @@ -277,15 +277,20 @@ describe('Service', () => {
test('functional service', async () => {
interface Config {}

class Foo extends FunctionalService {
interface Foo {
(init?: Config): Config
}

class Foo extends Service {
constructor(ctx: Context, public config?: Config, standalone?: boolean) {
super(ctx, 'foo', { immediate: true, standalone })
}

call(ctx: Context, init?: Config) {
expect(ctx).to.be.instanceof(Context)
[Context.invoke](init?: Config) {
const caller = this[Context.current]
expect(caller).to.be.instanceof(Context)
let result = { ...this.config }
let intercept = ctx[Context.intercept]
let intercept = caller[Context.intercept]
while (intercept) {
Object.assign(result, intercept.foo)
intercept = Object.getPrototypeOf(intercept)
Expand All @@ -294,6 +299,10 @@ describe('Service', () => {
return result
}

reflect() {
return this()
}

extend(config?: Config) {
return new Foo(this[Context.current], { ...this.config, ...config }, true)
}
Expand All @@ -312,9 +321,9 @@ describe('Service', () => {
const foo2 = root.foo.extend({ c: 3 })
expect(foo2()).to.deep.equal({ a: 1, c: 3 })
const foo3 = foo1.extend({ d: 4 })
expect(foo3()).to.deep.equal({ a: 1, b: 2, d: 4 })
expect(foo3.reflect()).to.deep.equal({ a: 1, b: 2, d: 4 })

// context tracibility
expect(foo1()).to.deep.equal({ a: 1, b: 2 })
expect(foo1.reflect()).to.deep.equal({ a: 1, b: 2 })
})
})

0 comments on commit 4fbf8bf

Please sign in to comment.