1
import { defineProperty } from '@deepseek-ai/cosmokit'2
import { Context } from './context.ts'3
import { createCallable, joinPrototype, symbols, type Tracker } from './utils.ts'5
/**6
* Base class for services that expose a named API on `ctx`.7
*8
* Subclasses call `super(ctx, name)` from their constructor. The service is9
* registered immediately and is automatically removed with the owning fiber.10
*/11
export abstract class Service<out T = never> {12
/** Symbol key of an instance method run after construction (class plugins). */13
static readonly init: unique symbol = symbols.init14
/** Symbol key of the availability predicate passed to `ctx.provide()`. */15
static readonly check: unique symbol = symbols.check16
/** Symbol key of the phantom intercept-config type parameter. */17
static readonly config: unique symbol = symbols.config18
/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */19
static readonly invoke: unique symbol = symbols.invoke20
/** Symbol key of the helper deriving an extended service instance. */21
static readonly extend: unique symbol = symbols.extend22
/** Symbol key of the tracker metadata used for context tracing. */23
static readonly tracker: unique symbol = symbols.tracker24
/** Symbol key of the intercept-config resolution helper below. */25
static readonly resolveConfig: unique symbol = symbols.resolveConfig27
declare [symbols.config]: T29
/** The service name this instance is registered under. */30
public name!: string32
/**33
* Register this instance as `name` in the current context.34
*35
* Calls `ctx.reflect.provide(name, this, this[Service.check])`, so the36
* service is unregistered automatically when the owning fiber unloads.37
* Services with a `[Service.invoke]` body return a callable instance.38
*39
* @param ctx — the context to register in (stored as `this.ctx`).40
* @param name — the service name; defaults to the static `provide` field.41
*/42
constructor(protected ctx: Context, name: string) {43
name ??= this.constructor['provide'] as string45
let self = this46
const tracker: Tracker = {47
associate: name,48
property: 'ctx',49
}50
if (self[symbols.invoke]) {51
self = createCallable(name, joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker)52
}53
self.ctx = ctx54
self.name = name55
defineProperty(self, symbols.tracker, tracker)57
self.ctx.reflect.provide(name, self, this[symbols.check])58
return self59
}61
protected [symbols.filter](ctx: Context) {62
return ctx[symbols.isolate][this.name] === this.ctx[symbols.isolate][this.name]63
}65
protected [symbols.extend](props?: any) {66
let self: any67
if (this[Service.invoke]) {68
self = createCallable(this.name, this, this[symbols.tracker])69
} else {70
self = Object.create(this)71
}72
return Object.assign(self, props)73
}75
/**76
* Merge intercept config from ancestors with optional base and head values.77
*78
* Entries added closer to the root apply first; `base` is prepended and79
* `head` appended. Uses `Config.merge` when the service declares one,80
* otherwise a shallow `Object.assign`.81
*82
* @param base — lowest-precedence config merged before all intercepts.83
* @param head — highest-precedence config merged after all intercepts.84
* @returns the merged config.85
*/86
[symbols.resolveConfig](base?: T, head?: T): T {87
let intercept = this.ctx[Context.intercept]88
const configs: any[] = []89
while (this.name in intercept) {90
if (Object.hasOwn(intercept, this.name)) {91
configs.unshift(intercept[this.name])92
}93
intercept = Object.getPrototypeOf(intercept)94
}95
if (base) configs.unshift(base)96
if (head) configs.push(head)97
if (this['Config']?.merge) {98
return this['Config'].merge(...configs)99
} else {100
return Object.assign({}, ...configs)101
}102
}104
static [Symbol.hasInstance](instance: any) {105
if (!instance) return false106
let constructor = instance.constructor107
while (constructor) {108
// constructor may be a proxy109
constructor = constructor.prototype?.constructor110
if (constructor === this) return true111
constructor &&= Object.getPrototypeOf(constructor)112
}113
return false114
}115
}