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 bced90d
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 64 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 () {}
13 changes: 12 additions & 1 deletion 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 Down Expand Up @@ -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)
},
})
}
Expand Down Expand Up @@ -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)
83 changes: 38 additions & 45 deletions packages/core/src/service.ts
Original file line number Diff line number Diff line change
@@ -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<C extends Context = Context> {
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 = (...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)
Expand All @@ -35,46 +70,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 bced90d

Please sign in to comment.