import { defineProperty, Dict } from 'cosmokit' import { StandardSchemaV1 } from '@standard-schema/spec' import { Context } from './context.ts' import { Fiber } from './fiber.ts' import { buildOuterStack, DisposableList, symbols, withProps } from './utils.ts' function isApplicable(object: Plugin) { return object && typeof object === 'object' && typeof object.apply === 'function' } /** * Service dependency declaration accepted by plugins and the `@Inject` * decorator. * * Array form requests services without intercept config. Object form maps each * service name to optional intercept config for the plugin context. */ export type Inject = (keyof M)[] | { [K in keyof M]?: M[K] } /** Context keys that correspond to services with typed intercept config. */ export type InjectKey = keyof { [K in keyof Context & string as Context[K] extends { [symbols.config]: any } ? K : never]: any } /** * Decorator for declaring service dependencies on classes or class methods. * * On classes it contributes to the plugin's static `inject` map. On methods it * delays the method call until the declared services are available. */ export function Inject(name: K, config?: Context[K] extends { [symbols.config]: infer T } ? T : never) { return function (value: any, decorator: ClassDecoratorContext | ClassMethodDecoratorContext) { if (decorator.kind === 'class') { if (!Object.hasOwn(value, 'inject')) { defineProperty(value, 'inject', Object.create(Object.getPrototypeOf(value).inject ?? null)) defineProperty(value.inject, symbols.checkProto, true) } value.inject[name] = config } else if (decorator.kind === 'method') { const inject = (value[symbols.metadata] ??= {}).inject ??= Object.create(null) inject[name] = config decorator.addInitializer(function () { const property = this[symbols.tracker]?.property ;(this[symbols.initHooks] ??= []).push(() => { (this.ctx as Context).inject(inject, (ctx) => { return value.call(property ? withProps(this, { [property]: ctx }) : this) }) }) }) } else { throw new Error('@Inject() can only be used on class or class methods') } } } /** Utilities for normalizing plugin dependency declarations. */ export namespace Inject { /** Convert array/object/class-inherited inject metadata into a plain map. */ export function resolve(inject: Inject | null | undefined, result: Dict = Object.create(null)) { if (!inject) return result if (Array.isArray(inject)) { for (const name of inject) { result[name] = null } } else if (Reflect.has(inject, symbols.checkProto)) { Object.assign(result, resolve(Object.getPrototypeOf(inject))) for (const name of Object.keys(inject)) { result[name] = inject[name] ?? null } } else { for (const name of Object.keys(inject)) { result[name] = inject[name] ?? null } } return result } } /** Supported plugin entrypoint shapes. */ export type Plugin = | Plugin.Function | Plugin.Constructor | Plugin.Object /** Types associated with plugin entrypoints and runtime records. */ export namespace Plugin { /** Shared metadata understood by the plugin registry and related tooling. */ export interface Base { name?: string Config?: StandardSchemaV1 inject?: Inject provide?: string | string[] intercept?: Dict } export interface Transform { /** Marks the transform object as a schema/config transform. */ schema?: true /** Convert user-facing config to runtime config. */ Config: (config: S) => T } /** Function plugin called with `(ctx, config)`. */ export interface Function extends Base { (ctx: Context, config: T): any } /** Class plugin constructed with `(ctx, config)`. */ export interface Constructor extends Base { new (ctx: Context, config: T): any } /** Object plugin with an `apply(ctx, config)` method. */ export interface Object extends Base { apply(ctx: Context, config: T): any } /** Mutable registry record shared by all fibers of one plugin callback. */ export interface Runtime { name?: string fibers: DisposableList callback: globalThis.Function Config?: StandardSchemaV1 } } type Spread = undefined extends T ? [config?: T] : [config: T] type GetPluginParameters

= | P extends (ctx: Context, ...args: infer R) => any ? R : P extends new (ctx: Context, ...args: infer R) => any ? R : P extends { apply(ctx: Context, ...args: infer R): any } ? R : never type GetPluginConfig

= | P extends Plugin.Transform ? S : GetPluginParameters

[0] declare module './context.ts' { export interface Context { inject(deps: Inject, callback: Plugin.Function): Fiber & PromiseLike plugin

(plugin: P, ...args: Spread>): Fiber & PromiseLike } } /** * Plugin registry installed as `ctx.registry` and mixed into every context. * * It normalizes plugin shapes, tracks plugin runtimes, starts fibers, and * exposes map-like inspection over active plugin callbacks. */ export class RegistryService { private _counter = 0 private _internal = new Map() constructor(public ctx: Context) { defineProperty(this, symbols.tracker, { property: 'ctx', noShadow: true, }) } get counter() { return ++this._counter } get size() { return this._internal.size } /** Resolve a supported plugin shape to its executable callback. */ resolve(plugin: Plugin): Function | undefined { // plugin.apply may throw try { if (typeof plugin === 'function') return plugin if (isApplicable(plugin)) return plugin.apply } catch {} } get(plugin: Plugin) { const key = this.resolve(plugin) return key && this._internal.get(key) } has(plugin: Plugin) { const key = this.resolve(plugin) return !!key && this._internal.has(key) } /** Dispose every running fiber for a plugin and remove its runtime record. */ delete(plugin: Plugin) { const key = this.resolve(plugin) const runtime = key && this._internal.get(key) if (!runtime) return this._internal.delete(key) for (const fiber of runtime.fibers) { fiber.dispose() } return runtime } keys() { return this._internal.keys() } values() { return this._internal.values() } entries() { return this._internal.entries() } forEach(callback: (value: Plugin.Runtime, key: Function) => void) { return this._internal.forEach(callback) } /** Start a callback once the requested dependencies are available. */ inject(inject: Inject, callback: Plugin.Function) { return this.plugin({ inject, apply: callback, name: callback.name }) } /** Start a plugin in the current context and return its fiber. */ plugin(plugin: Plugin, config?: any, getOuterStack = buildOuterStack()) { // check if it's a valid plugin const callback = this.resolve(plugin) if (!callback) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin) this.ctx.fiber.assertActive() let runtime = this._internal.get(callback) if (!runtime) { let name = plugin.name if (name === 'apply') name = undefined runtime = { name, callback, fibers: new DisposableList(), Config: plugin.Config } this._internal.set(callback, runtime) } const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), runtime, getOuterStack) const wrapped = Object.create(fiber) as Fiber & PromiseLike wrapped.then = (onFulfilled, onRejected) => { return fiber.await().then(onFulfilled, onRejected) } return wrapped } }