@qilin-agent/kylin 0.0.0-stage → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /** Core context type and root context implementation. */
2
+ export * from './context.ts'
3
+ /** Event bus, dispatch modes, and event augmentation types. */
4
+ export * from './events.ts'
5
+ /** Plugin fiber lifecycle, effects, and config validation helpers. */
6
+ export * from './fiber.ts'
7
+ /** Logger facade, logger service, message, exporter, and formatting types. */
8
+ export * from './logger.ts'
9
+ /** Plugin registry, dependency injection, and plugin entrypoint types. */
10
+ export * from './registry.ts'
11
+ /** Base service class and service lifecycle symbols. */
12
+ export * from './service.ts'
13
+ /** Shared internal helpers used by context, services, and plugin fibers. */
14
+ export * from './utils.ts'
15
+ /** Readonly references returned by volatile config schemas. */
16
+ export type { Volatile, VolatileSnapshot } from '@qilin-agent/cosmokit'
package/src/logger.ts ADDED
@@ -0,0 +1,271 @@
1
+ import { defineProperty, hyphenate } from '@qilin-agent/cosmokit'
2
+ import { Context } from './context.ts'
3
+ import { Fiber } from './fiber.ts'
4
+ import { createCallable, joinPrototype, symbols, type Tracker } from './utils.ts'
5
+
6
+ declare module './context.ts' {
7
+ interface Intercept {
8
+ logger: LoggerService.Intercept
9
+ }
10
+ }
11
+
12
+ /** Logger method name and severity category. */
13
+ export type LoggerType = 'error' | 'info' | 'warn' | 'debug'
14
+
15
+ /** Callable shape for one logger severity method. */
16
+ export type LoggerMethod = (format: any, ...param: any[]) => void
17
+
18
+ /** Formatter used to resolve a printf-style placeholder. */
19
+ export type Formatter = (value: any, exporter: Exporter, message: Message) => any
20
+
21
+ /** Numeric severity used when exporters decide whether to emit a message. */
22
+ export const enum LoggerLevel {
23
+ ERROR = 0,
24
+ INFO = 1,
25
+ WARN = 2,
26
+ DEBUG = 3,
27
+ }
28
+
29
+ /** Structured log record delivered to exporters. */
30
+ export interface Message {
31
+ sn: number
32
+ ts: number
33
+ name: string
34
+ type: LoggerType
35
+ level: number
36
+ args: any[]
37
+ fiber?: WeakRef<Fiber>
38
+ }
39
+
40
+ /** Sink that receives structured log messages. */
41
+ export interface Exporter {
42
+ colors?: number | false
43
+ maxLength?: number
44
+ levels?: Record<string, number>
45
+ formatters?: Record<string, Formatter>
46
+ export(message: Message): void
47
+ }
48
+
49
+ /** Built-in placeholder formatters used by `Logger.format()`. */
50
+ export const defaultFormatters: Record<string, Formatter> = {
51
+ s: (value) => String(value),
52
+ d: (value) => Math.trunc(Number(value)),
53
+ i: (value) => Math.trunc(Number(value)),
54
+ f: (value) => Number(value),
55
+ o: (value) => JSON.stringify(value),
56
+ O: (value) => JSON.stringify(value),
57
+ c: () => '',
58
+ C: (value, exporter, message) => {
59
+ return Logger.color(exporter, Logger.code(message.name, exporter.colors), value)
60
+ },
61
+ }
62
+
63
+ /** Options used when creating a named logger facade. */
64
+ export interface LoggerOptions {
65
+ /** The logger name shown with each message. */
66
+ name: string
67
+ /** Message fields merged into every record from this logger. */
68
+ meta?: Partial<Message>
69
+ /** Default maximum level exported when an exporter has no own threshold. */
70
+ level?: number
71
+ }
72
+
73
+ /** Logger facade identity, inherited message metadata, and optional minimum level. */
74
+ export interface Logger extends LoggerOptions {}
75
+ /** Logger facade severity methods. */
76
+ export interface Logger extends Record<LoggerType, LoggerMethod> {}
77
+
78
+ function isAggregateError(error: any): error is Error & { errors: Error[] } {
79
+ return error instanceof Error && Array.isArray(error['errors'])
80
+ }
81
+
82
+ /** Logger facade for one named subsystem. */
83
+ export class Logger {
84
+ static color(exporter: Exporter, code: number, value: any, decoration = '') {
85
+ if (!exporter.colors) return '' + value
86
+ return `\u001b[3${code < 8 ? code : '8;5;' + code}${exporter.colors >= 2 ? decoration : ''}m${value}\u001b[0m`
87
+ }
88
+
89
+ static code(name: string, level?: false | number) {
90
+ let hash = 0
91
+ for (let i = 0; i < name.length; i++) {
92
+ hash = ((hash << 3) - hash) + name.charCodeAt(i) + 13
93
+ hash |= 0
94
+ }
95
+ const colors = !level ? [] : level >= 2 ? c256 : c16
96
+ return colors[Math.abs(hash) % colors.length]
97
+ }
98
+
99
+ static format(exporter: Exporter, message: Message): string {
100
+ const args = message.args.slice()
101
+ if (args[0] instanceof Error) {
102
+ args[0] = args[0].stack || args[0].message
103
+ args.unshift('%s')
104
+ } else if (typeof args[0] !== 'string') {
105
+ args.unshift('%o')
106
+ }
107
+
108
+ let format: string = args.shift()
109
+ format = format.replace(/%([a-zA-Z%])/g, (match, char) => {
110
+ if (match === '%%') return '%'
111
+ const formatter = exporter.formatters?.[char] ?? defaultFormatters[char]
112
+ if (typeof formatter === 'function') {
113
+ const value = args.shift()
114
+ return formatter(value, exporter, message)
115
+ }
116
+ return match
117
+ })
118
+
119
+ const oFormatter = exporter.formatters?.o ?? defaultFormatters.o
120
+ for (let arg of args) {
121
+ if (typeof arg === 'object' && arg) {
122
+ arg = oFormatter(arg, exporter, message)
123
+ }
124
+ format += ' ' + arg
125
+ }
126
+
127
+ const { maxLength = 10240 } = exporter
128
+ return format.split(/\r?\n/g).map(line => {
129
+ return line.slice(0, maxLength) + (line.length > maxLength ? '...' : '')
130
+ }).join('\n')
131
+ }
132
+
133
+ constructor(options: LoggerOptions, private service: LoggerService) {
134
+ Object.assign(this, options)
135
+ this.error = this._method('error', LoggerLevel.ERROR)
136
+ this.info = this._method('info', LoggerLevel.INFO)
137
+ this.warn = this._method('warn', LoggerLevel.WARN)
138
+ this.debug = this._method('debug', LoggerLevel.DEBUG)
139
+ }
140
+
141
+ private _method(type: LoggerType, level: number): LoggerMethod {
142
+ return (...args: any[]) => {
143
+ if (args.length === 1 && args[0] instanceof Error) {
144
+ if (args[0].cause) {
145
+ this[type](args[0].cause)
146
+ } else if (isAggregateError(args[0])) {
147
+ args[0].errors.forEach(error => this[type](error))
148
+ return
149
+ }
150
+ }
151
+
152
+ const sn = ++this.service._snMessage
153
+ const ts = Date.now()
154
+ for (const exporter of this.service.exporters.values()) {
155
+ const targetLevel = exporter.levels?.[this.name] ?? exporter.levels?.default ?? this.level ?? LoggerLevel.INFO
156
+ if (targetLevel < level) continue
157
+ const message: Message = { sn, ts, type, level, name: this.name, ...this.meta, args }
158
+ exporter.export(message)
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ /** ANSI 16-color palette indexes used for logger name coloring. */
165
+ export const c16 = [6, 2, 3, 4, 5, 1]
166
+ /** ANSI 256-color palette indexes used for logger name coloring. */
167
+ export const c256 = [
168
+ 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
169
+ 63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
170
+ 129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168,
171
+ 169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200,
172
+ 201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
173
+ ]
174
+
175
+ /** Logger service configuration merged from context intercepts. */
176
+ export namespace LoggerService {
177
+ export interface Intercept {
178
+ name?: string
179
+ level?: number
180
+ }
181
+ }
182
+
183
+ /** Callable `ctx.logger` service shape. */
184
+ export interface LoggerService extends Record<LoggerType, LoggerMethod> {
185
+ (name?: string): Logger
186
+ }
187
+
188
+ /**
189
+ * Built-in logging service.
190
+ *
191
+ * Call `ctx.logger()` to create a named logger, or call `ctx.logger.info()`
192
+ * directly to log with the current fiber-derived name.
193
+ */
194
+ export class LoggerService {
195
+ bufferSize = 1000
196
+ buffer: Message[] = []
197
+ ctx!: Context
198
+
199
+ _snMessage = 0
200
+ _snExporter = 0
201
+ exporters = new Map<number, Exporter>()
202
+
203
+ constructor(ctx: Context) {
204
+ const tracker: Tracker = {
205
+ property: 'ctx',
206
+ noShadow: true,
207
+ }
208
+ const self = createCallable('logger', joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker) as unknown as LoggerService
209
+ Object.assign(self, this)
210
+ self.ctx = ctx
211
+ defineProperty(self, symbols.tracker, tracker)
212
+
213
+ self.exporter({
214
+ colors: 3,
215
+ export: (message) => {
216
+ self.buffer.push(message)
217
+ if (self.buffer.length > self.bufferSize) {
218
+ self.buffer = self.buffer.slice(-self.bufferSize)
219
+ }
220
+ },
221
+ })
222
+
223
+ return self
224
+ }
225
+
226
+ /**
227
+ * Register an exporter and dispose it with the current fiber.
228
+ *
229
+ * @param exporter — the sink that receives structured log messages.
230
+ * @returns a disposer that removes the exporter.
231
+ */
232
+ exporter(exporter: Exporter) {
233
+ return this.ctx.effect(() => {
234
+ const id = ++this._snExporter
235
+ this.exporters.set(id, exporter)
236
+ return () => this.exporters.delete(id)
237
+ }, 'ctx.logger.exporter()')
238
+ }
239
+
240
+ private _resolveConfig(): LoggerService.Intercept {
241
+ let intercept = this.ctx[symbols.intercept]
242
+ const configs: LoggerService.Intercept[] = []
243
+ while ('logger' in intercept) {
244
+ if (Object.hasOwn(intercept, 'logger')) {
245
+ configs.unshift(intercept['logger'])
246
+ }
247
+ intercept = Object.getPrototypeOf(intercept)
248
+ }
249
+ return Object.assign({}, ...configs)
250
+ }
251
+
252
+ [symbols.invoke](name?: string): Logger {
253
+ const config = this._resolveConfig()
254
+ const fiber = ((this.ctx as any)[symbols.shadow] ?? this.ctx).fiber
255
+ name ??= config.name
256
+ name ??= hyphenate(fiber.name)
257
+ return new Logger({
258
+ name,
259
+ level: config.level,
260
+ meta: { fiber: new WeakRef(fiber) },
261
+ }, this)
262
+ }
263
+
264
+ static {
265
+ for (const type of ['error', 'info', 'warn', 'debug'] as const) {
266
+ ;(LoggerService.prototype as any)[type] = function (this: LoggerService, ...args: any[]) {
267
+ return (this as any)()[type](...args)
268
+ }
269
+ }
270
+ }
271
+ }
package/src/reflect.ts ADDED
@@ -0,0 +1,418 @@
1
+ import { defineProperty, isNullable } from '@qilin-agent/cosmokit'
2
+ import type { Dict } from '@qilin-agent/cosmokit'
3
+ import { Context } from './context.ts'
4
+ import { getTraceable, symbols, withProps } from './utils.ts'
5
+ import { Fiber, FiberState } from './fiber.ts'
6
+
7
+ declare 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
+
73
+ function 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
+
80
+ const 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 `_`
86
+ function 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. */
94
+ export type Property = Property.Service | Property.Accessor
95
+
96
+ /** Property definition variants understood by `ReflectService`. */
97
+ export 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. */
116
+ export 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
+ */
133
+ export 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
+ }