返回源码地图

vendor/cordis/src/reflect.ts

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

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

1import { defineProperty, isNullable } from '@deepseek-ai/cosmokit'
2import type { Dict } from '@deepseek-ai/cosmokit'
3import { Context } from './context.ts'
4import { getTraceable, symbols, withProps } from './utils.ts'
5import { Fiber, FiberState } from './fiber.ts'
6
7declare module './context.ts' {
8 interface Context {
9 /**
10 * Read a service from the store without the inject requirement.
11 *
12 * @param name — the service name.
13 * @param strict — when `true` (default), only return implementations
14 * whose providing fiber is currently active.
15 * @returns the service value, or `undefined` when not (yet) provided.
16 */
17 get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
18 /** Same as above for service names outside the typed `Context` surface. */
19 get(name: string, strict?: boolean): any
20 /**
21 * Overwrite a provided service's value.
22 *
23 * Only the fiber that provided the service may set it; setting an
24 * unprovided name throws.
25 *
26 * @param name — the service name.
27 * @param value — the new service value.
28 */
29 set<K extends string & keyof this>(name: K, value: undefined | this[K]): void
30 /** Same as above for service names outside the typed `Context` surface. */
31 set(name: string, value: any): void
32 /**
33 * Register a service implementation owned by the current fiber.
34 *
35 * The service becomes visible to dependents in the same isolation scope
36 * once the fiber is active; it is unregistered (waking dependents) when
37 * the returned disposer runs or the fiber unloads. Throws if the name is
38 * already provided in this scope or declared as an accessor.
39 *
40 * @param name — the service name.
41 * @param value — the service value.
42 * @returns a disposer that unregisters the service.
43 */
44 provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
45 /** Same as above for service names outside the typed `Context` surface. */
46 provide(name: string, value?: any): () => void
47 /**
48 * Define a computed context property backed by get/set hooks.
49 *
50 * The accessor is removed when the current fiber unloads. Throws if the
51 * name is already declared.
52 *
53 * @param name — the context property name.
54 * @param options — the `get` hook and optional `set` hook.
55 */
56 accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
57 /**
58 * Expose selected members of a service directly on `ctx`.
59 *
60 * Each mixed-in key becomes an accessor that forwards to the service
61 * (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`.
62 * Mixins are removed when the current fiber unloads.
63 *
64 * @param name — the context property holding the source service.
65 * @param mixins — keys to forward, or a source-key → ctx-key map.
66 */
67 mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void
68 /** Same as above with a source object instead of a context property name. */
69 mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): void
70 }
71}
72
73function enhanceError(error: Error) {
74 const lines = error.stack!.split('\n')
75 lines.splice(0, 2, `Error: ${error.message}`)
76 error.stack = lines.join('\n')
77 return error
78}
79
80const RESERVED_WORDS = ['prototype', 'then']
81
82// - is a symbol
83// - is a reserved word (prototype, then)
84// - is a number string (0, 1, 2, ...)
85// - starts with `_`
86function isSpecialProperty(prop: string | symbol): prop is symbol {
87 return typeof prop === 'symbol'
88 || RESERVED_WORDS.includes(prop)
89 || parseInt(prop).toString() === prop
90 || prop.startsWith('_')
91}
92
93/** Context property definition known by the reflection service. */
94export type Property = Property.Service | Property.Accessor
95
96/** Property definition variants understood by `ReflectService`. */
97export namespace Property {
98 /** Service property backed by a provided implementation. */
99 export interface Service {
100 /** Discriminator. */
101 type: 'service'
102 }
103
104 /** Computed context property backed by custom get/set hooks. */
105 export interface Accessor {
106 /** Discriminator. */
107 type: 'accessor'
108 /** Compute the property value; `error` carries the caller stack for diagnostics. */
109 get: (this: Context, receiver: any, error: Error) => any
110 /** Optional setter; return `false` to reject the write. */
111 set?: (this: Context, value: any, receiver: any, error: Error) => boolean
112 }
113}
114
115/** Concrete service implementation record stored in the root reflect service. */
116export interface Impl {
117 /** The service name. */
118 name: string
119 /** The fiber that provided the service (owns its lifetime). */
120 fiber: Fiber
121 /** The current service value. */
122 value?: any
123 /** Optional availability predicate consulted before dependents may load. */
124 check?: () => boolean
125}
126
127/**
128 * Reflection and service-resolution layer installed as `ctx.reflect`.
129 *
130 * This service powers the context proxy, service registration, accessors, and
131 * the mixins that expose core service methods directly on `ctx`.
132 */
133export class ReflectService {
134 /** Proxy traps implementing service resolution for every context object. */
135 static handler: ProxyHandler<Context> = {
136 get: (target, prop, ctx: Context) => {
137 if (isSpecialProperty(prop)) {
138 return Reflect.get(target, prop, ctx)
139 }
140 if (Reflect.has(target, prop)) {
141 return getTraceable(ctx, Reflect.get(target, prop, ctx))
142 }
143
144 const error = new Error(`cannot get property "${prop}" without inject`)
145
146 try {
147 const def = target.reflect.props[prop]
148 if (def?.type === 'accessor') {
149 return def.get.call(ctx, ctx[symbols.receiver], error)
150 }
151
152 if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false)
153 return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
154 const key = target[symbols.isolate][prop]
155 let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber
156 while (true) {
157 const impl = fiber.store?.[prop]
158 if (impl) return getTraceable(ctx, impl.value)
159 if (prop in fiber.inject) {
160 error.message = `cannot get required service "${prop}" in inactive context`
161 throw error
162 }
163 if (!fiber.runtime) throw error
164 if (fiber.parent[symbols.isolate][prop] !== key) throw error
165 fiber = fiber.parent.fiber
166 }
167 })
168 } catch (e: any) {
169 throw e === error ? enhanceError(e) : e
170 }
171 },
172
173 set: (target, prop, value, ctx: Context) => {
174 if (isSpecialProperty(prop)) {
175 return Reflect.set(target, prop, value, ctx)
176 }
177
178 const error = new Error(`cannot set property "${prop}" without provide`)
179 const def = target.reflect.props[prop]
180 if (!def) {
181 if (!ctx.fiber.runtime) return Reflect.set(target, prop, value, ctx)
182 throw enhanceError(error)
183 }
184
185 try {
186 if (def.type === 'accessor') {
187 if (!def.set) return false
188 return def.set.call(ctx, value, ctx[symbols.receiver], error)
189 }
190
191 return ctx.events.waterfall('internal/set', ctx, prop, value, error, () => {
192 return ctx.reflect.set(prop, value, error)
193 })
194 } catch (e: any) {
195 throw e === error ? enhanceError(e) : e
196 }
197 },
198
199 has: (target, prop) => {
200 if (isSpecialProperty(prop)) {
201 return Reflect.has(target, prop)
202 }
203 if (Reflect.has(target, prop)) return true
204 return !!target.reflect.props[prop]
205 },
206 }
207
208 /** Service implementations, keyed by isolation label. */
209 public store: Dict<Impl, symbol> = Object.create(null)
210 /** Declared context properties (services and accessors), by name. */
211 public props: Dict<Property> = Object.create(null)
212
213 constructor(public ctx: Context) {
214 defineProperty(this, symbols.tracker, {
215 property: 'ctx',
216 noShadow: true,
217 })
218
219 this.mixin('reflect', ['get', 'set', 'provide', 'accessor', 'mixin'])
220 this.mixin('fiber', ['runtime', 'effect'])
221 this.mixin('registry', ['inject', 'plugin'])
222 this.mixin('events', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'waterfall'])
223 }
224
225 /**
226 * Read a service from the store without the inject requirement.
227 *
228 * @param name — the service name.
229 * @param strict — when `true`, only return implementations whose providing
230 * fiber is currently active.
231 * @returns the service value, or `undefined` when not (yet) provided.
232 */
233 get(name: string, strict = true) {
234 return getTraceable(this.ctx, this._getImpl(name, strict)?.value)
235 }
236
237 _getImpl(name: string, strict = true) {
238 const key = this.ctx[symbols.isolate][name]
239 const impl = key && this.store[key]
240 if (!impl) return
241 if (strict && impl.fiber.state !== FiberState.ACTIVE) return
242 return impl
243 }
244
245 /**
246 * Overwrite a provided service's value.
247 *
248 * @param name — the service name.
249 * @param value — the new service value.
250 * @param error — carrier for the caller stack in diagnostics.
251 * @returns `true` on success.
252 * @throws when `name` was never provided, or was provided by another fiber.
253 */
254 set(name: string, value: any, error?: Error) {
255 const key = this.ctx[symbols.isolate][name]
256 const impl = this.store[key]
257 if (!impl) {
258 throw new Error(`cannot set property "${name}" without provide`)
259 }
260 if (impl.fiber !== this.ctx.fiber) {
261 throw new Error(`cannot set property "${name}" in multiple fibers`)
262 }
263 impl.value = value
264 return true
265 }
266
267 /**
268 * Register a service implementation owned by the current fiber.
269 *
270 * See the `ctx.provide()` overload above for the full contract.
271 *
272 * @param name — the service name.
273 * @param value — the service value.
274 * @param check — optional availability predicate for dependents.
275 * @returns a disposer that unregisters the service.
276 */
277 provide(name: string, value?: any, check?: () => boolean) {
278 return this.ctx.fiber.effect(() => {
279 if (!this.props[name]) {
280 this.props[name] ??= { type: 'service' }
281 } else if (this.props[name].type !== 'service') {
282 throw new Error(`property "${name}" is already declared as ${this.props[name].type}`)
283 }
284 this.props[name] = { type: 'service' }
285
286 this.ctx.root[symbols.isolate][name] ??= Symbol(name)
287 const key = this.ctx[symbols.isolate][name]
288 const impl: Impl = { name, value, fiber: this.ctx.fiber, check }
289 if (this.store[key]) {
290 throw new Error(`service "${name}" has been registered at <${this.store[key].fiber.name}>`)
291 }
292 this.store[key] = impl
293 this.ctx.fiber.store![name] = impl
294 if (this.ctx.fiber.state === FiberState.ACTIVE) {
295 this.notify([name])
296 }
297 return async () => {
298 delete this.store[key]
299 const fibers = this.notify([name])
300 await Promise.allSettled(fibers.map(fiber => fiber.await()))
301 // ensure self access before dependencies cleanup
302 delete this.ctx.fiber.store![name]
303 }
304 }, `ctx.provide(${JSON.stringify(name)})`)
305 }
306
307 /**
308 * Re-evaluate every fiber that requires one of the given services.
309 *
310 * @param names — the service names that changed.
311 * @param filter — restricts notification to matching isolation scopes.
312 * @returns the fibers whose dependency state was refreshed.
313 */
314 notify(names: string[], filter = (ctx: Context, name: string) => ctx[symbols.isolate][name] === this.ctx[symbols.isolate][name]) {
315 const fibers: Fiber[] = []
316 for (const runtime of this.ctx.registry.values()) {
317 for (const fiber of runtime.fibers) {
318 let hasUpdate = false
319 for (const name of names) {
320 if (!(name in fiber.inject)) continue
321 if (!filter(fiber.ctx, name)) continue
322 hasUpdate = true
323 fiber._checkImpl(name)
324 }
325 if (!hasUpdate) continue
326 fiber._refresh()
327 fibers.push(fiber)
328 }
329 }
330 for (const name of names) {
331 const self: Context = Object.create(this.ctx)
332 self[symbols.filter] = (target: Context) => filter(target, name)
333 this.ctx.events.emit(self, 'internal/service', name, this._getImpl(name, false)?.value)
334 }
335 return fibers
336 }
337
338 /**
339 * Define a computed context property backed by get/set hooks.
340 *
341 * @param name — the context property name.
342 * @param options — the `get` hook and optional `set` hook.
343 * @returns a disposer that removes the accessor.
344 */
345 accessor(name: string, options: Omit<Property.Accessor, 'type'>) {
346 return this.ctx.fiber.effect(() => {
347 if (name in this.props) {
348 throw new Error(`property "${name}" is already declared as ${this.props[name].type}`)
349 }
350 this.props[name] = { type: 'accessor', ...options }
351 return () => delete this.props[name]
352 }, `ctx.accessor(${JSON.stringify(name)})`)
353 }
354
355 /**
356 * Expose selected members of a service directly on `ctx`.
357 *
358 * See the `ctx.mixin()` overload above for the full contract.
359 *
360 * @param source — a context property name or a source object.
361 * @param mixins — keys to forward, or a source-key → ctx-key map.
362 * @returns a disposer that removes all created accessors.
363 */
364 mixin(source: any, mixins: string[] | Dict<string>) {
365 const self = this
366 return this.ctx.fiber.effect(function* () {
367 const entries = Array.isArray(mixins) ? mixins.map(key => [key, key]) : Object.entries(mixins)
368 const getTarget = (ctx: Context, error: Error) => {
369 // TODO enhance error message
370 return ctx[source]
371 }
372 for (const [key, value] of entries) {
373 yield self.accessor(value, {
374 get(receiver, error) {
375 const service = getTarget(this, error)
376 if (isNullable(service)) return service
377 const mixin = receiver ? withProps(receiver, service) : service
378 const value = Reflect.get(service, key, mixin)
379 if (typeof value !== 'function') return value
380 return value.bind(mixin ?? service)
381 },
382 set(value, receiver, error) {
383 const service = getTarget(this, error)
384 const mixin = receiver ? withProps(receiver, service) : service
385 return Reflect.set(service, key, value, mixin)
386 },
387 })
388 }
389 }, `ctx.mixin(${JSON.stringify(source)})`)
390 }
391
392 /**
393 * Attach this context's tracing wrapper to a value.
394 *
395 * @param value — the value to wrap.
396 * @returns the traceable wrapper (or the value itself when not applicable).
397 */
398 trace<T>(value: T) {
399 return getTraceable(this.ctx, value)
400 }
401
402 /**
403 * Wrap a callback so calls trace `this` and arguments to this context.
404 *
405 * @param callback — the function to wrap.
406 * @returns a proxy delegating to `callback` with traced values.
407 */
408 bind<T extends Function>(callback: T) {
409 return new Proxy(callback, {
410 apply: (target, thisArg, args) => {
411 return Reflect.apply(target, this.trace(thisArg), args.map(arg => this.trace(arg)))
412 },
413 construct: (target, args, newTarget) => {
414 return Reflect.construct(target, args.map(arg => this.trace(arg)), newTarget)
415 },
416 })
417 }
418}