diff --git a/packages/cordis/src/index.ts b/packages/cordis/src/index.ts index c00bc0e..8027710 100644 --- a/packages/cordis/src/index.ts +++ b/packages/cordis/src/index.ts @@ -42,15 +42,4 @@ export abstract class Service extends core.Service< } } -export abstract class FunctionalService extends core.FunctionalService { - 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 () {} diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts index a62d26d..1953159 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/context.ts @@ -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') @@ -261,7 +262,7 @@ export class Context { return Reflect.get(target, name, receiver) }, apply: receiver ? undefined : (target, thisArg, args) => { - return target.call(this, ...args) + return makeCall(target, this, ...args) }, }) } @@ -323,4 +324,14 @@ export class Context { } } +export function makeCall(self: any, thisArg: any, ...args: any[]) { + if (!self[Context.invoke]) return Reflect.apply(self, thisArg, args) + return self[Context.invoke].call(new Proxy(self, { + get: (target, name, receiver) => { + if (name === Context.current) return thisArg + return Reflect.get(target, name, receiver) + }, + }), ...args) +} + Context.prototype[Context.internal] = Object.create(null) diff --git a/packages/core/src/service.ts b/packages/core/src/service.ts index e8268a9..ee59abd 100644 --- a/packages/core/src/service.ts +++ b/packages/core/src/service.ts @@ -1,19 +1,54 @@ import { Awaitable, defineProperty } from 'cosmokit' -import { Context } from './context.ts' +import { Context, makeCall } 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 { static Context = Context - public [kSetup](ctx: C | undefined, name: string, options?: boolean | Service.Options) { + protected start(): Awaitable {} + protected stop(): Awaitable {} + 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 = (...args: any[]) => makeCall(self, ctx, ...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) @@ -35,46 +70,4 @@ export abstract class Service { return Context.associate(this, name) } - - protected start(): Awaitable {} - protected stop(): Awaitable {} - 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 -} - -export abstract class FunctionalService extends Function { - static Context = Context - - abstract call(ctx: C, ...args: any[]): any - - protected start(): Awaitable {} - protected stop(): Awaitable {} - 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 - } } diff --git a/packages/core/tests/service.spec.ts b/packages/core/tests/service.spec.ts index d3a1574..a8ebff3 100644 --- a/packages/core/tests/service.spec.ts +++ b/packages/core/tests/service.spec.ts @@ -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' @@ -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) @@ -294,6 +299,10 @@ describe('Service', () => { return result } + reflect() { + return this() + } + extend(config?: Config) { return new Foo(this[Context.current], { ...this.config, ...config }, true) } @@ -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 }) }) })