返回源码地图

vendor/cordis/src/registry.ts

main snapshot · da00f7f5358f · 正文引用章节 03 / 03;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1import { defineProperty } from '@deepseek-ai/cosmokit'
2import type { Dict } from '@deepseek-ai/cosmokit'
3import type { StandardSchemaV1 } from '@standard-schema/spec'
4import { Context } from './context.ts'
5import { Fiber } from './fiber.ts'
6import { buildOuterStack, DisposableList, symbols, withProps } from './utils.ts'
7
8function isApplicable(object: Plugin) {
9 return object && typeof object === 'object' && typeof object.apply === 'function'
10}
11
12/**
13 * Service dependency declaration accepted by plugins and the `@Inject`
14 * decorator.
15 *
16 * Array form requests services without intercept config. Object form maps each
17 * service name to optional intercept config for the plugin context.
18 */
19export type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
20
21/** Context keys that correspond to services with typed intercept config. */
22export type InjectKey = keyof {
23 [K in keyof Context & string as Context[K] extends { [symbols.config]: any } ? K : never]: any
24}
25
26/**
27 * Decorator for declaring service dependencies on classes or class methods.
28 *
29 * On classes it contributes to the plugin's static `inject` map. On methods it
30 * delays the method call until the declared services are available.
31 */
32/**
33 * @param name — the required service name.
34 * @param config — optional intercept config applied for that service.
35 * @returns the class or method decorator.
36 */
37export function Inject<K extends InjectKey>(name: K, config?: Context[K] extends { [symbols.config]: infer T } ? T : never) {
38 return function (value: any, decorator: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) {
39 if (decorator.kind === 'class') {
40 if (!Object.hasOwn(value, 'inject')) {
41 defineProperty(value, 'inject', Object.create(Object.getPrototypeOf(value).inject ?? null))
42 defineProperty(value.inject, symbols.checkProto, true)
43 }
44 value.inject[name] = config
45 } else if (decorator.kind === 'method') {
46 const inject = (value[symbols.metadata] ??= {}).inject ??= Object.create(null)
47 inject[name] = config
48 decorator.addInitializer(function () {
49 const property = this[symbols.tracker]?.property
50 ;(this[symbols.initHooks] ??= []).push(() => {
51 (this.ctx as Context).inject(inject, (ctx) => {
52 return value.call(property ? withProps(this, { [property]: ctx }) : this)
53 })
54 })
55 })
56 } else {
57 throw new Error('@Inject() can only be used on class or class methods')
58 }
59 }
60}
61
62/** Utilities for normalizing plugin dependency declarations. */
63export namespace Inject {
64 /**
65 * Convert array/object/class-inherited inject metadata into a plain map.
66 *
67 * @param inject — the declaration to normalize; `null`/`undefined` add nothing.
68 * @param result — the map to fill (service name → intercept config or `null`).
69 * @returns `result`.
70 */
71 export function resolve(inject: Inject | null | undefined, result: Dict = Object.create(null)) {
72 if (!inject) return result
73 if (Array.isArray(inject)) {
74 for (const name of inject) {
75 result[name] = null
76 }
77 } else if (Reflect.has(inject, symbols.checkProto)) {
78 Object.assign(result, resolve(Object.getPrototypeOf(inject)))
79 for (const name of Object.keys(inject)) {
80 result[name] = inject[name] ?? null
81 }
82 } else {
83 for (const name of Object.keys(inject)) {
84 result[name] = inject[name] ?? null
85 }
86 }
87 return result
88 }
89}
90
91/** Supported plugin entrypoint shapes. */
92export type Plugin<T = any> =
93 | Plugin.Function<T>
94 | Plugin.Constructor<T>
95 | Plugin.Object<T>
96
97/** Types associated with plugin entrypoints and runtime records. */
98export namespace Plugin {
99 /** Shared metadata understood by the plugin registry and related tooling. */
100 export interface Base<T = any> {
101 /** Display name used for fiber diagnostics and logger names. */
102 name?: string
103 /** Standard-schema validator applied to config before the plugin starts. */
104 Config?: StandardSchemaV1<any, T>
105 /** Services the plugin requires; it only loads while all are available. */
106 inject?: Inject
107 /** Service name(s) the plugin provides (read by `Service` and by loaders). */
108 provide?: string | string[]
109 /** Service names whose intercept config the plugin declares it consumes. */
110 intercept?: Dict<boolean>
111 }
112
113 export interface Transform<S, T> {
114 /** Marks the transform object as a schema/config transform. */
115 schema?: true
116 /** Convert user-facing config to runtime config. */
117 Config: (config: S) => T
118 }
119
120 /** Function plugin called with `(ctx, config)`. */
121 export interface Function<T = any> extends Base<T> {
122 (ctx: Context, config: T): any
123 }
124
125 /** Class plugin constructed with `(ctx, config)`. */
126 export interface Constructor<T = any> extends Base<T> {
127 new (ctx: Context, config: T): any
128 }
129
130 /** Object plugin with an `apply(ctx, config)` method. */
131 export interface Object<T = any> extends Base<T> {
132 apply(ctx: Context, config: T): any
133 }
134
135 /** Mutable registry record shared by all fibers of one plugin callback. */
136 export interface Runtime {
137 /** Display name copied from the first registered plugin shape. */
138 name?: string
139 /** Every live fiber of this plugin (one per `ctx.plugin()` call). */
140 fibers: DisposableList<Fiber>
141 /** The executable entrypoint all fibers share (registry identity key). */
142 callback: globalThis.Function
143 /** Standard-schema validator applied to each fiber's config. */
144 Config?: StandardSchemaV1
145 }
146}
147
148type Spread<T> = undefined extends T ? [config?: T] : [config: T]
149
150type GetPluginParameters<P> =
151 | P extends (ctx: Context, ...args: infer R) => any
152 ? R
153 : P extends new (ctx: Context, ...args: infer R) => any
154 ? R
155 : P extends { apply(ctx: Context, ...args: infer R): any }
156 ? R
157 : never
158
159type GetPluginConfig<P> =
160 | P extends Plugin.Transform<infer S, any>
161 ? S
162 : GetPluginParameters<P>[0]
163
164declare module './context.ts' {
165 export interface Context {
166 /**
167 * Run a callback once the requested services are available.
168 *
169 * Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback
170 * is unloaded and re-run whenever a required service changes.
171 *
172 * @param deps — required services, as an array or a name → config map.
173 * @param callback — plugin body called with `(ctx, config)`.
174 * @returns the fiber; awaiting it settles once loading finished.
175 */
176 inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
177 /**
178 * Load a plugin in the current context.
179 *
180 * @param plugin — a function, class, or `{ apply }` object plugin.
181 * @param args — the plugin config, validated against its `Config` schema.
182 * @returns the fiber; awaiting it settles once loading finished
183 * (rejecting on config or startup errors).
184 */
185 plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
186 }
187}
188
189/**
190 * Plugin registry installed as `ctx.registry` and mixed into every context.
191 *
192 * It normalizes plugin shapes, tracks plugin runtimes, starts fibers, and
193 * exposes map-like inspection over active plugin callbacks.
194 */
195export class RegistryService {
196 private _counter = 0
197 private _internal = new Map<Function, Plugin.Runtime>()
198
199 constructor(public ctx: Context) {
200 defineProperty(this, symbols.tracker, {
201 property: 'ctx',
202 noShadow: true,
203 })
204 }
205
206 /** Allocate the next fiber uid (increments on every read). */
207 get counter() {
208 return ++this._counter
209 }
210
211 /** Number of registered plugin runtimes. */
212 get size() {
213 return this._internal.size
214 }
215
216 /**
217 * Resolve a supported plugin shape to its executable callback.
218 *
219 * @param plugin — a function, class, or `{ apply }` object plugin.
220 * @returns the callback identifying the plugin, or `undefined` if invalid.
221 */
222 resolve(plugin: Plugin): Function | undefined {
223 // plugin.apply may throw
224 try {
225 if (typeof plugin === 'function') return plugin
226 if (isApplicable(plugin)) return plugin.apply
227 } catch {}
228 }
229
230 /**
231 * Look up the runtime record for a plugin.
232 *
233 * @param plugin — any supported plugin shape.
234 * @returns the runtime, or `undefined` when the plugin is not registered.
235 */
236 get(plugin: Plugin) {
237 const key = this.resolve(plugin)
238 return key && this._internal.get(key)
239 }
240
241 /**
242 * Check whether a plugin has a registered runtime.
243 *
244 * @param plugin — any supported plugin shape.
245 * @returns `true` when at least one fiber of the plugin exists.
246 */
247 has(plugin: Plugin) {
248 const key = this.resolve(plugin)
249 return !!key && this._internal.has(key)
250 }
251
252 /**
253 * Dispose every running fiber for a plugin and remove its runtime record.
254 *
255 * @param plugin — any supported plugin shape.
256 * @returns the removed runtime, or `undefined` when none was registered.
257 */
258 delete(plugin: Plugin) {
259 const key = this.resolve(plugin)
260 const runtime = key && this._internal.get(key)
261 if (!runtime) return
262 this._internal.delete(key)
263 for (const fiber of runtime.fibers) {
264 fiber.dispose()
265 }
266 return runtime
267 }
268
269 /** Iterate the registered plugin callbacks. */
270 keys() {
271 return this._internal.keys()
272 }
273
274 /** Iterate the registered plugin runtimes. */
275 values() {
276 return this._internal.values()
277 }
278
279 /** Iterate `[callback, runtime]` pairs. */
280 entries() {
281 return this._internal.entries()
282 }
283
284 /**
285 * Visit every registered runtime.
286 *
287 * @param callback — receives each runtime and its identifying callback.
288 */
289 forEach(callback: (value: Plugin.Runtime, key: Function) => void) {
290 return this._internal.forEach(callback)
291 }
292
293 /**
294 * Start a callback once the requested dependencies are available.
295 *
296 * @param inject — required services, as an array or a name → config map.
297 * @param callback — plugin body called with `(ctx, config)`.
298 * @returns the fiber; awaiting it settles once loading finished.
299 */
300 inject(inject: Inject, callback: Plugin.Function<void>) {
301 return this.plugin({ inject, apply: callback, name: callback.name })
302 }
303
304 /**
305 * Start a plugin in the current context and return its fiber.
306 *
307 * Creates (or reuses) the plugin's runtime record, then starts a new fiber
308 * under the current context. Throws if `plugin` is not a supported shape or
309 * if the current fiber is already disposed.
310 *
311 * @param plugin — a function, class, or `{ apply }` object plugin.
312 * @param config — the plugin config, validated against its `Config` schema.
313 * @param getOuterStack — captures the caller stack for effect diagnostics.
314 * @returns the fiber; awaiting it settles once loading finished.
315 */
316 plugin(plugin: Plugin, config?: any, getOuterStack = buildOuterStack()) {
317 // check if it's a valid plugin
318 const callback = this.resolve(plugin)
319 if (!callback) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin)
320 this.ctx.fiber.assertActive()
321
322 let runtime = this._internal.get(callback)
323 if (!runtime) {
324 let name = plugin.name
325 if (name === 'apply') name = undefined
326 runtime = { name, callback, fibers: new DisposableList(), Config: plugin.Config }
327 this._internal.set(callback, runtime)
328 }
329
330 const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), runtime, getOuterStack)
331 const wrapped = Object.create(fiber) as Fiber & PromiseLike<Fiber>
332 wrapped.then = (onFulfilled, onRejected) => {
333 return fiber.await().then(onFulfilled, onRejected)
334 }
335 return wrapped
336 }
337}