vendor(cordis): document the full plugin-author surface (@param/@returns everywhere)
Comment-only enrichment across cordis/src/*.ts — Context, EventsService (+ the ctx merges), Fiber, RegistryService, ReflectService, Service, logger — so the website API generator can render a complete reference and hard-error on any future undocumented member (vendor sync included). Logged as local modification 6 in vendor/README.md; retire it when upstreamed to the fork. INHERITED_SERVICES/EVENTS source pointers refreshed for the shifted lines; cordis catalogs regenerated.
This commit is contained in:
@@ -427,14 +427,14 @@ Source: [`packages/workflow/workflow/src/index.ts:62`](../../packages/workflow/w
|
||||
|
||||
The framework events every plugin also sees, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of the event bus, without elevating framework internals to the harness tier's prominence.
|
||||
|
||||
- `internal/plugin` — A plugin fiber was created. ([`vendor/cordis/src/events.ts:197`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/status` — A fiber changed lifecycle state. ([`vendor/cordis/src/events.ts:198`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/service` — Interception hook for a service binding (no core producer). ([`vendor/cordis/src/events.ts:199`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/update` — Waterfall: a fiber config update is being applied. ([`vendor/cordis/src/events.ts:200`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/get` — Waterfall: a service is being read from the store. ([`vendor/cordis/src/events.ts:201`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/set` — Waterfall: a service is being written to the store. ([`vendor/cordis/src/events.ts:202`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:203`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:204`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/plugin` — A plugin fiber was created. ([`vendor/cordis/src/events.ts:328`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/status` — A fiber changed lifecycle state. ([`vendor/cordis/src/events.ts:330`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/service` — Interception hook for a service binding (no core producer). ([`vendor/cordis/src/events.ts:332`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/update` — Waterfall: a fiber config update is being applied. ([`vendor/cordis/src/events.ts:334`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/get` — Waterfall: a service is being read from the store. ([`vendor/cordis/src/events.ts:336`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/set` — Waterfall: a service is being written to the store. ([`vendor/cordis/src/events.ts:338`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:340`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:342`](../../vendor/cordis/src/events.ts))
|
||||
- `hmr/change` — A watched source file changed on disk. ([`vendor/hmr/src/index.ts:20`](../../vendor/hmr/src/index.ts))
|
||||
- `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:21`](../../vendor/hmr/src/index.ts))
|
||||
- `exit` — The process is exiting on a signal. ([`vendor/loader/src/index.ts:23`](../../vendor/loader/src/index.ts))
|
||||
|
||||
@@ -282,12 +282,12 @@ Source: [`packages/workflow/workflow/src/index.ts:210`](../../packages/workflow/
|
||||
|
||||
The framework `ctx` surface every plugin also sees, beyond the harness services above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of what `ctx` offers, without elevating framework internals to the harness tier's prominence.
|
||||
|
||||
- `ctx.on / ctx.once` — Register an event listener (disposable). ([`vendor/cordis/src/events.ts:29`](../../vendor/cordis/src/events.ts))
|
||||
- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-bail / veto-chain). ([`vendor/cordis/src/events.ts:29`](../../vendor/cordis/src/events.ts))
|
||||
- `ctx.plugin / ctx.inject` — Load a plugin / declare required services. ([`vendor/cordis/src/registry.ts:144`](../../vendor/cordis/src/registry.ts))
|
||||
- `ctx.on / ctx.once` — Register an event listener (disposable). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts))
|
||||
- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-bail / veto-chain). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts))
|
||||
- `ctx.plugin / ctx.inject` — Load a plugin / declare required services. ([`vendor/cordis/src/registry.ts:164`](../../vendor/cordis/src/registry.ts))
|
||||
- `ctx.effect` — Register a disposable side effect tied to the fiber. ([`vendor/cordis/src/fiber.ts:9`](../../vendor/cordis/src/fiber.ts))
|
||||
- `ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin` — Low-level service-store access and binding. ([`vendor/cordis/src/reflect.ts:7`](../../vendor/cordis/src/reflect.ts))
|
||||
- `ctx.extend / ctx.isolate / ctx.intercept` — Derive a child context (scoped services / isolation / interception). ([`vendor/cordis/src/context.ts:35`](../../vendor/cordis/src/context.ts))
|
||||
- `ctx.extend / ctx.isolate / ctx.intercept` — Derive a child context (scoped services / isolation / interception). ([`vendor/cordis/src/context.ts:42`](../../vendor/cordis/src/context.ts))
|
||||
- `ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger` — Ambient handles onto the running context graph. ([`vendor/cordis/src/context.ts:16`](../../vendor/cordis/src/context.ts))
|
||||
- `ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)` — Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick). ([`vendor/timer/src/index.ts:4`](../../vendor/timer/src/index.ts))
|
||||
- `ctx.loader` — The config Loader that booted the app (present under the loader). ([`vendor/loader/src/index.ts:30`](../../vendor/loader/src/index.ts))
|
||||
|
||||
@@ -310,14 +310,14 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] {
|
||||
* sibling check is N/A; keep them current on a vendor bump.
|
||||
*/
|
||||
const INHERITED_EVENTS: InheritedEntry[] = [
|
||||
{ name: 'internal/plugin', summary: 'A plugin fiber was created.', source: 'vendor/cordis/src/events.ts:197' },
|
||||
{ name: 'internal/status', summary: 'A fiber changed lifecycle state.', source: 'vendor/cordis/src/events.ts:198' },
|
||||
{ name: 'internal/service', summary: 'Interception hook for a service binding (no core producer).', source: 'vendor/cordis/src/events.ts:199' },
|
||||
{ name: 'internal/update', summary: 'Waterfall: a fiber config update is being applied.', source: 'vendor/cordis/src/events.ts:200' },
|
||||
{ name: 'internal/get', summary: 'Waterfall: a service is being read from the store.', source: 'vendor/cordis/src/events.ts:201' },
|
||||
{ name: 'internal/set', summary: 'Waterfall: a service is being written to the store.', source: 'vendor/cordis/src/events.ts:202' },
|
||||
{ name: 'internal/listener', summary: 'A listener was registered.', source: 'vendor/cordis/src/events.ts:203' },
|
||||
{ name: 'internal/dispatch', summary: 'An event is being dispatched to listeners.', source: 'vendor/cordis/src/events.ts:204' },
|
||||
{ name: 'internal/plugin', summary: 'A plugin fiber was created.', source: 'vendor/cordis/src/events.ts:328' },
|
||||
{ name: 'internal/status', summary: 'A fiber changed lifecycle state.', source: 'vendor/cordis/src/events.ts:330' },
|
||||
{ name: 'internal/service', summary: 'Interception hook for a service binding (no core producer).', source: 'vendor/cordis/src/events.ts:332' },
|
||||
{ name: 'internal/update', summary: 'Waterfall: a fiber config update is being applied.', source: 'vendor/cordis/src/events.ts:334' },
|
||||
{ name: 'internal/get', summary: 'Waterfall: a service is being read from the store.', source: 'vendor/cordis/src/events.ts:336' },
|
||||
{ name: 'internal/set', summary: 'Waterfall: a service is being written to the store.', source: 'vendor/cordis/src/events.ts:338' },
|
||||
{ name: 'internal/listener', summary: 'A listener was registered.', source: 'vendor/cordis/src/events.ts:340' },
|
||||
{ name: 'internal/dispatch', summary: 'An event is being dispatched to listeners.', source: 'vendor/cordis/src/events.ts:342' },
|
||||
{ name: 'hmr/change', summary: 'A watched source file changed on disk.', source: 'vendor/hmr/src/index.ts:20' },
|
||||
{ name: 'hmr/reload', summary: 'Plugins are being reloaded after a change.', source: 'vendor/hmr/src/index.ts:21' },
|
||||
{ name: 'exit', summary: 'The process is exiting on a signal.', source: 'vendor/loader/src/index.ts:23' },
|
||||
@@ -328,12 +328,12 @@ const INHERITED_EVENTS: InheritedEntry[] = [
|
||||
]
|
||||
|
||||
export const INHERITED_SERVICES: InheritedEntry[] = [
|
||||
{ name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:29' },
|
||||
{ name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).', source: 'vendor/cordis/src/events.ts:29' },
|
||||
{ name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:144' },
|
||||
{ name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:34' },
|
||||
{ name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).', source: 'vendor/cordis/src/events.ts:34' },
|
||||
{ name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:164' },
|
||||
{ name: 'ctx.effect', summary: 'Register a disposable side effect tied to the fiber.', source: 'vendor/cordis/src/fiber.ts:9' },
|
||||
{ name: 'ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin', summary: 'Low-level service-store access and binding.', source: 'vendor/cordis/src/reflect.ts:7' },
|
||||
{ name: 'ctx.extend / ctx.isolate / ctx.intercept', summary: 'Derive a child context (scoped services / isolation / interception).', source: 'vendor/cordis/src/context.ts:35' },
|
||||
{ name: 'ctx.extend / ctx.isolate / ctx.intercept', summary: 'Derive a child context (scoped services / isolation / interception).', source: 'vendor/cordis/src/context.ts:42' },
|
||||
{ name: 'ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger', summary: 'Ambient handles onto the running context graph.', source: 'vendor/cordis/src/context.ts:16' },
|
||||
{ name: 'ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)', summary: 'Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick).', source: 'vendor/timer/src/index.ts:4' },
|
||||
{ name: 'ctx.loader', summary: 'The config Loader that booted the app (present under the loader).', source: 'vendor/loader/src/index.ts:30' },
|
||||
|
||||
Vendored
+1
@@ -35,6 +35,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
|
||||
3. **All `tsconfig.json` files**: regenerated to extend the repo-root `tsconfig.base.json`, emit TypeScript intermediates to `lib/types`, and declare project references.
|
||||
4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from upstream's specifier shape to explicit `.ts` specifiers so TypeScript rewrites emitted JS to `.js` while declarations keep explicit, NodeNext-safe `.ts` specifiers. This includes `loader/src/config/isolate.ts` using `declare module './entry.ts'`.
|
||||
5. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted under `lib/types` and then write the publish runtime entries under `lib/`. Like the regenerated tsconfigs, they are not part of the upstream sync surface.
|
||||
6. **`cordis/src/*.ts` JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context`, `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork.
|
||||
|
||||
## Sync procedure
|
||||
|
||||
|
||||
Vendored
+53
-4
@@ -14,14 +14,21 @@ import { Fiber } from './fiber.ts'
|
||||
* be read from `ctx`.
|
||||
*/
|
||||
export interface Context {
|
||||
/** Isolation map: service name → scope label. Lookups for a name resolve within its label. */
|
||||
[symbols.isolate]: Dict<symbol>
|
||||
/** Intercept map: service name → config merged into that service's per-plugin config. */
|
||||
[symbols.intercept]: Dict
|
||||
/** @experimental */
|
||||
root: this
|
||||
/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */
|
||||
baseUrl?: string
|
||||
/** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */
|
||||
events: EventsService
|
||||
/** The logging service. Call `ctx.logger(name)` for a named logger. */
|
||||
logger: LoggerService
|
||||
/** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */
|
||||
reflect: ReflectService
|
||||
/** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */
|
||||
registry: RegistryService
|
||||
}
|
||||
|
||||
@@ -33,12 +40,24 @@ export interface Context {
|
||||
* contexts without mutating their parent.
|
||||
*/
|
||||
export class Context {
|
||||
/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */
|
||||
static readonly effect: unique symbol = symbols.effect
|
||||
/** Symbol key for a context's listener filter, consulted on every event dispatch. */
|
||||
static readonly filter: unique symbol = symbols.filter
|
||||
/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */
|
||||
static readonly isolate: unique symbol = symbols.isolate
|
||||
/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */
|
||||
static readonly intercept: unique symbol = symbols.intercept
|
||||
|
||||
/** Returns true for Cordis context proxies and context prototypes. */
|
||||
/**
|
||||
* Returns true for Cordis context proxies and context prototypes.
|
||||
*
|
||||
* Works across realms and across multiple copies of cordis, because the
|
||||
* brand is keyed by a global symbol rather than by `instanceof`.
|
||||
*
|
||||
* @param value — the value to test.
|
||||
* @returns `true` if `value` is a Cordis context, narrowing its type.
|
||||
*/
|
||||
static is(value: any): value is Context {
|
||||
return !!value?.[Context.is as any]
|
||||
}
|
||||
@@ -68,7 +87,15 @@ export class Context {
|
||||
return `Context <${this.fiber.name}>`
|
||||
}
|
||||
|
||||
/** Create a child context with extra metadata on top of the current scope. */
|
||||
/**
|
||||
* Create a child context with extra metadata on top of the current scope.
|
||||
*
|
||||
* The child prototypally inherits every property of this context; own
|
||||
* properties of `meta` shadow the inherited ones. The parent is not mutated.
|
||||
*
|
||||
* @param meta — own properties (including symbol keys) to define on the child.
|
||||
* @returns a child context inheriting from this one.
|
||||
*/
|
||||
extend(meta = {}): this {
|
||||
const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
|
||||
const self = Object.create(getTraceable(this, this))
|
||||
@@ -79,14 +106,36 @@ export class Context {
|
||||
return Object.assign(Object.create(self), { [symbols.shadow]: shadow })
|
||||
}
|
||||
|
||||
/** Create a child context with an independent service scope for `name`. */
|
||||
/**
|
||||
* Create a child context with an independent service scope for `name`.
|
||||
*
|
||||
* Below the returned context, reads and writes of the service `name`
|
||||
* resolve against the new label instead of the parent's, so a different
|
||||
* implementation can be provided without affecting the parent scope.
|
||||
* Passing the same `label` to two `isolate()` calls joins their scopes.
|
||||
*
|
||||
* @param name — the service name to isolate.
|
||||
* @param label — scope label to join; defaults to a fresh unique symbol.
|
||||
* @returns a child context whose `name` service resolves in the new scope.
|
||||
*/
|
||||
isolate(name: string, label?: symbol) {
|
||||
const shadow = Object.create(this[symbols.isolate])
|
||||
shadow[name] = label ?? Symbol(name)
|
||||
return this.extend({ [symbols.isolate]: shadow })
|
||||
}
|
||||
|
||||
/** Add service-specific intercept config for plugins started below this context. */
|
||||
/**
|
||||
* Add service-specific intercept config for plugins started below this
|
||||
* context.
|
||||
*
|
||||
* Plugins loaded under the returned context see `config` merged into the
|
||||
* service's resolved config (ancestor entries first; see
|
||||
* `Service[symbols.resolveConfig]`). The parent context is not affected.
|
||||
*
|
||||
* @param name — the service name whose config to intercept.
|
||||
* @param config — the intercept config to merge for that service.
|
||||
* @returns a child context carrying the additional intercept entry.
|
||||
*/
|
||||
intercept<K extends InjectKey>(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this
|
||||
intercept(name: string, config: any): this
|
||||
intercept(name: string, config: any) {
|
||||
|
||||
Vendored
+147
-9
@@ -3,7 +3,12 @@ import { Context } from './context.ts'
|
||||
import { Fiber, FiberState } from './fiber.ts'
|
||||
import { DisposableList, symbols } from './utils.ts'
|
||||
|
||||
/** Return whether an event result should stop a bail-style dispatch. */
|
||||
/**
|
||||
* Return whether an event result should stop a bail-style dispatch.
|
||||
*
|
||||
* @param value — a listener's return value.
|
||||
* @returns `true` unless `value` is `null`, `false`, or `undefined`.
|
||||
*/
|
||||
export function isBailed(value: any) {
|
||||
return value !== null && value !== false && value !== undefined
|
||||
}
|
||||
@@ -28,17 +33,75 @@ export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
|
||||
declare module './context.ts' {
|
||||
export interface Context {
|
||||
/* eslint-disable max-len */
|
||||
/**
|
||||
* Dispatch an event, running all listeners concurrently.
|
||||
*
|
||||
* @param name — the event name.
|
||||
* @param args — arguments passed to every listener.
|
||||
* @returns a promise resolving once every listener has settled.
|
||||
*/
|
||||
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
|
||||
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
||||
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
|
||||
/**
|
||||
* Dispatch an event synchronously, ignoring listener return values.
|
||||
*
|
||||
* @param name — the event name.
|
||||
* @param args — arguments passed to every listener.
|
||||
*/
|
||||
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
|
||||
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
||||
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
|
||||
/**
|
||||
* Dispatch an event, awaiting listeners in order until one bails.
|
||||
*
|
||||
* @param name — the event name.
|
||||
* @param args — arguments passed to each listener.
|
||||
* @returns the first bail value (non-null, non-false, non-undefined), if any.
|
||||
*/
|
||||
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
||||
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
||||
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
||||
/**
|
||||
* Dispatch an event, calling listeners in order until one bails.
|
||||
*
|
||||
* @param name — the event name.
|
||||
* @param args — arguments passed to each listener.
|
||||
* @returns the first bail value (non-null, non-false, non-undefined), if any.
|
||||
*/
|
||||
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
||||
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
/**
|
||||
* Dispatch an event whose last argument is a `next` continuation.
|
||||
*
|
||||
* Each listener wraps the rest of the chain: calling `next()` invokes the
|
||||
* next listener (finally the built-in behavior); not calling it vetoes.
|
||||
*
|
||||
* @param name — the event name.
|
||||
* @param args — listener arguments; the final one is the innermost `next`.
|
||||
* @returns the outermost listener's return value.
|
||||
*/
|
||||
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
||||
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
/**
|
||||
* Register an event listener owned by the current fiber.
|
||||
*
|
||||
* @param name — the event name to listen for.
|
||||
* @param listener — called with the dispatch arguments.
|
||||
* @param options — listener options; a boolean is shorthand for `prepend`.
|
||||
* @returns a disposer removing the listener; `true` if it was still registered.
|
||||
*/
|
||||
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
||||
/**
|
||||
* Same as `on()`, but the listener disposes itself after its first call.
|
||||
*
|
||||
* @param name — the event name to listen for.
|
||||
* @param listener — called at most once with the dispatch arguments.
|
||||
* @param options — listener options; a boolean is shorthand for `prepend`.
|
||||
* @returns a disposer removing the listener; `true` if it was still registered.
|
||||
*/
|
||||
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
@@ -91,7 +154,13 @@ export class EventsService {
|
||||
}, { global: true, prepend: true })
|
||||
}
|
||||
|
||||
/** Resolve listeners for one dispatch and apply context filtering. */
|
||||
/**
|
||||
* Resolve listeners for one dispatch and apply context filtering.
|
||||
*
|
||||
* @param type — the dispatch mode, reported on `internal/dispatch`.
|
||||
* @param args — the raw dispatch arguments; consumed up to the event name.
|
||||
* @returns the matching listener callbacks, bound to the dispatch `this`.
|
||||
*/
|
||||
dispatch(type: string, args: any[]) {
|
||||
const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
|
||||
const name: string = args.shift()
|
||||
@@ -104,17 +173,31 @@ export class EventsService {
|
||||
.map(hook => hook.callback.bind(thisArg))
|
||||
}
|
||||
|
||||
/** Run listeners concurrently and wait for all of them. */
|
||||
/**
|
||||
* Run listeners concurrently and wait for all of them.
|
||||
*
|
||||
* @param args — optional `this`, the event name, then listener arguments.
|
||||
* @returns a promise resolving once every listener has settled.
|
||||
*/
|
||||
async parallel(...args: any[]) {
|
||||
await Promise.all(this.dispatch('emit', args).map(cb => cb(...args)))
|
||||
}
|
||||
|
||||
/** Run listeners synchronously without waiting for returned promises. */
|
||||
/**
|
||||
* Run listeners synchronously without waiting for returned promises.
|
||||
*
|
||||
* @param args — optional `this`, the event name, then listener arguments.
|
||||
*/
|
||||
emit(...args: any[]) {
|
||||
this.dispatch('emit', args).map(cb => cb(...args))
|
||||
}
|
||||
|
||||
/** Run listeners in order until one returns a bail value. */
|
||||
/**
|
||||
* Run listeners in order, awaiting each, until one returns a bail value.
|
||||
*
|
||||
* @param args — optional `this`, the event name, then listener arguments.
|
||||
* @returns the first bail value (see {@link isBailed}), if any.
|
||||
*/
|
||||
async serial(...args: any[]) {
|
||||
for (const cb of this.dispatch('serial', args)) {
|
||||
const result = await cb(...args)
|
||||
@@ -122,7 +205,12 @@ export class EventsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Run listeners synchronously until one returns a bail value. */
|
||||
/**
|
||||
* Run listeners synchronously until one returns a bail value.
|
||||
*
|
||||
* @param args — optional `this`, the event name, then listener arguments.
|
||||
* @returns the first bail value (see {@link isBailed}), if any.
|
||||
*/
|
||||
bail(...args: any[]) {
|
||||
for (const cb of this.dispatch('bail', args)) {
|
||||
const result = cb(...args)
|
||||
@@ -130,7 +218,16 @@ export class EventsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Compose listeners around the final `next` callback. */
|
||||
/**
|
||||
* Compose listeners around the final `next` callback.
|
||||
*
|
||||
* The last dispatch argument is treated as the innermost `next`. Listeners
|
||||
* run outermost-first; a listener that does not call `next()` vetoes the
|
||||
* rest of the chain, including the built-in behavior.
|
||||
*
|
||||
* @param args — optional `this`, the event name, listener arguments, then `next`.
|
||||
* @returns the outermost listener's return value.
|
||||
*/
|
||||
waterfall(...args: any[]) {
|
||||
const cbs = this.dispatch('waterfall', args)
|
||||
const inner = args.pop()
|
||||
@@ -142,6 +239,15 @@ export class EventsService {
|
||||
return next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a listener record as an effect on the current fiber.
|
||||
*
|
||||
* @param label — effect label shown in fiber diagnostics.
|
||||
* @param hooks — the listener list for one event.
|
||||
* @param callback — the listener to store.
|
||||
* @param options — placement and filtering options.
|
||||
* @returns a disposer that unregisters the listener.
|
||||
*/
|
||||
register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
|
||||
const method = options.prepend ? 'unshift' : 'push'
|
||||
return this.ctx.fiber.effect(() => {
|
||||
@@ -150,6 +256,13 @@ export class EventsService {
|
||||
}, label)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a stored listener record.
|
||||
*
|
||||
* @param hooks — the listener list for one event.
|
||||
* @param callback — the listener to remove.
|
||||
* @returns `true` if the listener was found and removed.
|
||||
*/
|
||||
unregister(hooks: Hook[], callback: any) {
|
||||
const index = hooks.findIndex(hook => hook.callback === callback)
|
||||
if (index >= 0) {
|
||||
@@ -158,7 +271,17 @@ export class EventsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Register an event listener owned by the current fiber. */
|
||||
/**
|
||||
* Register an event listener owned by the current fiber.
|
||||
*
|
||||
* The listener is removed automatically when the fiber unloads. Throws
|
||||
* `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed.
|
||||
*
|
||||
* @param name — the event name to listen for.
|
||||
* @param listener — called with the dispatch arguments.
|
||||
* @param options — listener options; a boolean is shorthand for `prepend`.
|
||||
* @returns a disposer removing the listener; `true` if it was still registered.
|
||||
*/
|
||||
on(name: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
||||
if (typeof options !== 'object') {
|
||||
options = { prepend: options }
|
||||
@@ -175,7 +298,14 @@ export class EventsService {
|
||||
return this.register(label, hooks, listener, options)
|
||||
}
|
||||
|
||||
/** Register an event listener that disposes itself after the first call. */
|
||||
/**
|
||||
* Register an event listener that disposes itself after the first call.
|
||||
*
|
||||
* @param name — the event name to listen for.
|
||||
* @param listener — called at most once with the dispatch arguments.
|
||||
* @param options — listener options; a boolean is shorthand for `prepend`.
|
||||
* @returns a disposer removing the listener; `true` if it was still registered.
|
||||
*/
|
||||
once(name: string, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
||||
const dispose = this.on(name, function (...args: any[]) {
|
||||
dispose()
|
||||
@@ -194,12 +324,20 @@ export class EventsService {
|
||||
* diagnostics before public events are delivered.
|
||||
*/
|
||||
export interface Events {
|
||||
/** A plugin fiber was created or its uid was cleared on disposal. */
|
||||
'internal/plugin'(fiber: Fiber): void
|
||||
/** A fiber changed lifecycle state; receives the fiber and its previous state. */
|
||||
'internal/status'(fiber: Fiber, oldValue: FiberState): void
|
||||
/** Interception hook for a service binding (no core producer). */
|
||||
'internal/service'(this: Context, name: string, value: any): void
|
||||
/** Waterfall: a fiber config update is being applied; skip `next()` to veto. */
|
||||
'internal/update'(this: Fiber, config: any, noSave: boolean, next: () => void): void
|
||||
/** Waterfall: a service is being read through the context proxy. */
|
||||
'internal/get'(ctx: Context, name: string, error: Error, next: () => any): any
|
||||
/** Waterfall: a service is being written through the context proxy. */
|
||||
'internal/set'(ctx: Context, name: string, value: any, error: Error, next: () => boolean): boolean
|
||||
/** Bail: a listener is being registered; a non-null result replaces registration. */
|
||||
'internal/listener'(this: Context, name: string, listener: any, prepend: boolean): void
|
||||
/** An event is being dispatched to listeners (fired for non-internal events only). */
|
||||
'internal/dispatch'(mode: DispatchMode, name: string, args: any[], thisArg: any): void
|
||||
}
|
||||
Vendored
+107
-10
@@ -7,6 +7,7 @@ import { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
|
||||
declare module './context.ts' {
|
||||
export interface Context extends Pick<Fiber, 'effect'> {
|
||||
/** The fiber (plugin runtime instance) that owns this context. */
|
||||
fiber: Fiber
|
||||
}
|
||||
}
|
||||
@@ -17,6 +18,11 @@ const kValidationError = Symbol.for('ValidationError')
|
||||
export class ValidationError extends TypeError {
|
||||
name = 'ValidationError'
|
||||
|
||||
/**
|
||||
* Build the aggregated message from schema issues.
|
||||
*
|
||||
* @param issues — the standard-schema issues, one message line each.
|
||||
*/
|
||||
constructor(issues: readonly StandardSchemaV1.Issue[]) {
|
||||
super(`invalid config:\n` + issues.map(issue => {
|
||||
if (issue.path) {
|
||||
@@ -32,7 +38,14 @@ Object.defineProperty(ValidationError.prototype, kValidationError, {
|
||||
value: true,
|
||||
})
|
||||
|
||||
/** Validate and normalize config for a plugin runtime before it starts. */
|
||||
/**
|
||||
* Validate and normalize config for a plugin runtime before it starts.
|
||||
*
|
||||
* @param runtime — the plugin runtime whose `Config` schema to apply.
|
||||
* @param config — the raw user config.
|
||||
* @returns the validated config, or `config` unchanged if the runtime has no schema.
|
||||
* @throws {ValidationError} when validation reports issues.
|
||||
*/
|
||||
export function resolveConfig(runtime: Plugin.Runtime, config: any) {
|
||||
if (!runtime.Config) return config
|
||||
// TODO: async validation
|
||||
@@ -51,10 +64,21 @@ interface AsyncDisposable<T extends Awaitable<void> = Awaitable<void>> extends P
|
||||
(): T
|
||||
}
|
||||
|
||||
/** Function returned by an effect to release resources during disposal. */
|
||||
/**
|
||||
* Function returned by an effect to release resources during disposal.
|
||||
*
|
||||
* Disposers run in reverse registration order when the owning fiber unloads;
|
||||
* they may be async, in which case unloading awaits them.
|
||||
*/
|
||||
export type Disposable<T = any> = () => T
|
||||
|
||||
/** Effect body result accepted by `ctx.effect()` and plugin startup. */
|
||||
/**
|
||||
* Effect body result accepted by `ctx.effect()` and plugin startup.
|
||||
*
|
||||
* Either a single disposer, a promise of one, or a (possibly async) iterable
|
||||
* yielding several — generator effects register each yielded disposer as it
|
||||
* is produced.
|
||||
*/
|
||||
export type Effect<T = any> =
|
||||
| SyncEffect<T>
|
||||
| AsyncEffect<T>
|
||||
@@ -69,7 +93,9 @@ type AsyncEffect<T = any> =
|
||||
|
||||
/** Tree node used to expose nested effect labels for diagnostics. */
|
||||
export interface EffectMeta {
|
||||
/** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */
|
||||
label: string
|
||||
/** Metadata of nested effects registered while this effect ran. */
|
||||
children: EffectMeta[]
|
||||
}
|
||||
|
||||
@@ -80,7 +106,14 @@ interface EffectRunner<T> {
|
||||
getOuterStack: () => string[]
|
||||
}
|
||||
|
||||
/** Lifecycle state for one plugin fiber. */
|
||||
/**
|
||||
* Lifecycle state for one plugin fiber.
|
||||
*
|
||||
* `PENDING` — waiting for required services; `LOADING` — the plugin callback
|
||||
* is running; `ACTIVE` — loaded and providing; `FAILED` — the callback or its
|
||||
* config threw; `UNLOADING` — disposers are running; `DISPOSED` — the fiber
|
||||
* was removed and cannot restart.
|
||||
*/
|
||||
export const enum FiberState {
|
||||
PENDING,
|
||||
LOADING,
|
||||
@@ -92,6 +125,10 @@ export const enum FiberState {
|
||||
|
||||
/** Framework error with a stable machine-readable code. */
|
||||
export class CordisError extends Error {
|
||||
/**
|
||||
* @param code — the stable error code; also the default message.
|
||||
* @param message — optional human-readable override.
|
||||
*/
|
||||
constructor(public code: CordisError.Code, message?: string) {
|
||||
super(message ?? CordisError.Code[code])
|
||||
}
|
||||
@@ -115,12 +152,19 @@ const INACTIVE = '__INACTIVE__'
|
||||
* cleanup for the plugin context returned by `ctx.plugin()`.
|
||||
*/
|
||||
export class Fiber {
|
||||
/** Unique id within the registry; 0 for the root fiber, `null` once disposed. */
|
||||
public uid: number | null
|
||||
/** The context this fiber's plugin runs in (extends the parent context). */
|
||||
public readonly ctx: Context
|
||||
/** The validated plugin config (updated by `update()`). */
|
||||
public config: any
|
||||
/** Current lifecycle state; transitions emit `internal/status`. */
|
||||
public state = FiberState.PENDING
|
||||
/** Dispose this fiber: unload the plugin, then settle once cleanup finished. */
|
||||
public readonly dispose: () => Promise<void>
|
||||
/** Snapshot of required service implementations while loaded; `undefined` otherwise. */
|
||||
public store: Dict<Impl> | undefined
|
||||
/** The in-flight load/unload transition, if one is currently running. */
|
||||
public inertia: Promise<void> | undefined
|
||||
|
||||
public readonly _hooks: Dict<DisposableList<Function>> = Object.create(null)
|
||||
@@ -133,6 +177,16 @@ export class Fiber {
|
||||
private _runner: EffectRunner<string>
|
||||
private _store: Dict<Impl> = Object.create(null)
|
||||
|
||||
/**
|
||||
* Create a fiber. Plugin authors normally obtain fibers from `ctx.plugin()`
|
||||
* rather than constructing them directly.
|
||||
*
|
||||
* @param parent — the context the plugin was loaded from.
|
||||
* @param config — raw config, validated against the runtime's schema.
|
||||
* @param inject — resolved dependency map (service name → intercept config).
|
||||
* @param runtime — the shared plugin runtime, or `null` for the root fiber.
|
||||
* @param getOuterStack — captures the caller stack for effect diagnostics.
|
||||
*/
|
||||
constructor(
|
||||
public parent: Context,
|
||||
config: any,
|
||||
@@ -226,6 +280,7 @@ export class Fiber {
|
||||
}
|
||||
}
|
||||
|
||||
/** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */
|
||||
get name() {
|
||||
let fiber: Fiber = this
|
||||
do {
|
||||
@@ -235,7 +290,12 @@ export class Fiber {
|
||||
return 'root'
|
||||
}
|
||||
|
||||
/** Throw if the fiber has already been disposed. */
|
||||
/**
|
||||
* Throw if the fiber has already been disposed.
|
||||
*
|
||||
* @returns nothing when the fiber is still active.
|
||||
* @throws {CordisError} `INACTIVE_EFFECT` when the fiber's uid has been cleared.
|
||||
*/
|
||||
assertActive() {
|
||||
if (this.uid !== null) return
|
||||
throw new CordisError('INACTIVE_EFFECT')
|
||||
@@ -287,8 +347,21 @@ export class Fiber {
|
||||
}, runner.getOuterStack)
|
||||
}
|
||||
|
||||
/** Register a cleanup-aware effect on this fiber. */
|
||||
/**
|
||||
* Register a cleanup-aware effect on this fiber.
|
||||
*
|
||||
* `execute` runs immediately; the disposers it produces are collected and
|
||||
* run (in reverse order) either when the returned disposer is called or
|
||||
* when the fiber unloads, whichever comes first. Calling the disposer twice
|
||||
* is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is
|
||||
* already disposed, and `TypeError` if `execute` returns an invalid shape.
|
||||
*
|
||||
* @param execute — the effect body; see {@link Effect} for accepted shapes.
|
||||
* @param label — effect label shown in `getEffects()` diagnostics.
|
||||
* @returns a disposer that tears the effect down and settles once done.
|
||||
*/
|
||||
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
|
||||
/** Same as above for async effects; the disposer is also awaitable. */
|
||||
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
|
||||
effect(execute: () => Effect, label = 'anonymous'): any {
|
||||
this.assertActive()
|
||||
@@ -355,7 +428,11 @@ export class Fiber {
|
||||
return wrapper
|
||||
}
|
||||
|
||||
/** Return metadata for currently registered effects. */
|
||||
/**
|
||||
* Return metadata for currently registered effects.
|
||||
*
|
||||
* @returns one {@link EffectMeta} tree per labeled live effect.
|
||||
*/
|
||||
getEffects() {
|
||||
return [...this._disposables]
|
||||
.map<EffectMeta>(dispose => dispose[symbols.effect])
|
||||
@@ -474,7 +551,12 @@ export class Fiber {
|
||||
})
|
||||
}
|
||||
|
||||
/** Wait for current lifecycle work and rethrow startup errors. */
|
||||
/**
|
||||
* Wait for current lifecycle work and rethrow startup errors.
|
||||
*
|
||||
* @returns this fiber, once it has settled into a stable state.
|
||||
* @throws the config-validation or plugin-startup error, if any.
|
||||
*/
|
||||
async await() {
|
||||
while (this.inertia) {
|
||||
await this.inertia
|
||||
@@ -483,7 +565,12 @@ export class Fiber {
|
||||
return this
|
||||
}
|
||||
|
||||
/** Dispose and immediately reload this plugin with its current config. */
|
||||
/**
|
||||
* Dispose and immediately reload this plugin with its current config.
|
||||
*
|
||||
* @returns a promise resolving once the reload settled.
|
||||
* @throws {CordisError} `INACTIVE_EFFECT` when the fiber is already disposed.
|
||||
*/
|
||||
async restart() {
|
||||
this.assertActive()
|
||||
this._setEpoch(INACTIVE)
|
||||
@@ -491,7 +578,17 @@ export class Fiber {
|
||||
await this.await()
|
||||
}
|
||||
|
||||
/** Validate and apply new config, then restart the plugin. */
|
||||
/**
|
||||
* Validate and apply new config, then restart the plugin.
|
||||
*
|
||||
* Runs the `internal/update` waterfall first, so update hooks (and HMR)
|
||||
* can veto or replace the restart.
|
||||
*
|
||||
* @param config — the new raw config; validated before anything restarts.
|
||||
* @param noSave — hint for persistence hooks not to write the change back.
|
||||
* @returns nothing; the restart runs behind the `internal/update` waterfall.
|
||||
* @throws {ValidationError} when the new config fails validation.
|
||||
*/
|
||||
update(config: any, noSave = false) {
|
||||
this.assertActive()
|
||||
config = resolveConfig(this.runtime!, config)
|
||||
|
||||
Vendored
+9
-1
@@ -62,8 +62,11 @@ export const defaultFormatters: Record<string, Formatter> = {
|
||||
|
||||
/** Options used when creating a named logger facade. */
|
||||
export interface LoggerOptions {
|
||||
/** The logger name shown with each message. */
|
||||
name: string
|
||||
/** Message fields merged into every record from this logger. */
|
||||
meta?: Partial<Message>
|
||||
/** Default maximum level exported when an exporter has no own threshold. */
|
||||
level?: number
|
||||
}
|
||||
|
||||
@@ -220,7 +223,12 @@ export class LoggerService {
|
||||
return self
|
||||
}
|
||||
|
||||
/** Register an exporter and dispose it with the current fiber. */
|
||||
/**
|
||||
* Register an exporter and dispose it with the current fiber.
|
||||
*
|
||||
* @param exporter — the sink that receives structured log messages.
|
||||
* @returns a disposer that removes the exporter.
|
||||
*/
|
||||
exporter(exporter: Exporter) {
|
||||
return this.ctx.effect(() => {
|
||||
this.exporters.set(++this._snExporter, exporter)
|
||||
|
||||
Vendored
+125
@@ -5,14 +5,66 @@ import { Fiber, FiberState } from './fiber.ts'
|
||||
|
||||
declare module './context.ts' {
|
||||
interface Context {
|
||||
/**
|
||||
* Read a service from the store without the inject requirement.
|
||||
*
|
||||
* @param name — the service name.
|
||||
* @param strict — when `true` (default), only return implementations
|
||||
* whose providing fiber is currently active.
|
||||
* @returns the service value, or `undefined` when not (yet) provided.
|
||||
*/
|
||||
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
|
||||
/** Same as above for service names outside the typed `Context` surface. */
|
||||
get(name: string, strict?: boolean): any
|
||||
/**
|
||||
* Overwrite a provided service's value.
|
||||
*
|
||||
* Only the fiber that provided the service may set it; setting an
|
||||
* unprovided name throws.
|
||||
*
|
||||
* @param name — the service name.
|
||||
* @param value — the new service value.
|
||||
*/
|
||||
set<K extends string & keyof this>(name: K, value: undefined | this[K]): void
|
||||
/** Same as above for service names outside the typed `Context` surface. */
|
||||
set(name: string, value: any): void
|
||||
/**
|
||||
* Register a service implementation owned by the current fiber.
|
||||
*
|
||||
* The service becomes visible to dependents in the same isolation scope
|
||||
* once the fiber is active; it is unregistered (waking dependents) when
|
||||
* the returned disposer runs or the fiber unloads. Throws if the name is
|
||||
* already provided in this scope or declared as an accessor.
|
||||
*
|
||||
* @param name — the service name.
|
||||
* @param value — the service value.
|
||||
* @returns a disposer that unregisters the service.
|
||||
*/
|
||||
provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
|
||||
/** Same as above for service names outside the typed `Context` surface. */
|
||||
provide(name: string, value?: any): () => void
|
||||
/**
|
||||
* Define a computed context property backed by get/set hooks.
|
||||
*
|
||||
* The accessor is removed when the current fiber unloads. Throws if the
|
||||
* name is already declared.
|
||||
*
|
||||
* @param name — the context property name.
|
||||
* @param options — the `get` hook and optional `set` hook.
|
||||
*/
|
||||
accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
|
||||
/**
|
||||
* Expose selected members of a service directly on `ctx`.
|
||||
*
|
||||
* Each mixed-in key becomes an accessor that forwards to the service
|
||||
* (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`.
|
||||
* Mixins are removed when the current fiber unloads.
|
||||
*
|
||||
* @param name — the context property holding the source service.
|
||||
* @param mixins — keys to forward, or a source-key → ctx-key map.
|
||||
*/
|
||||
mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void
|
||||
/** Same as above with a source object instead of a context property name. */
|
||||
mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): void
|
||||
}
|
||||
}
|
||||
@@ -44,22 +96,30 @@ export type Property = Property.Service | Property.Accessor
|
||||
export namespace Property {
|
||||
/** Service property backed by a provided implementation. */
|
||||
export interface Service {
|
||||
/** Discriminator. */
|
||||
type: 'service'
|
||||
}
|
||||
|
||||
/** Computed context property backed by custom get/set hooks. */
|
||||
export interface Accessor {
|
||||
/** Discriminator. */
|
||||
type: 'accessor'
|
||||
/** Compute the property value; `error` carries the caller stack for diagnostics. */
|
||||
get: (this: Context, receiver: any, error: Error) => any
|
||||
/** Optional setter; return `false` to reject the write. */
|
||||
set?: (this: Context, value: any, receiver: any, error: Error) => boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** Concrete service implementation record stored in the root reflect service. */
|
||||
export interface Impl {
|
||||
/** The service name. */
|
||||
name: string
|
||||
/** The fiber that provided the service (owns its lifetime). */
|
||||
fiber: Fiber
|
||||
/** The current service value. */
|
||||
value?: any
|
||||
/** Optional availability predicate consulted before dependents may load. */
|
||||
check?: () => boolean
|
||||
}
|
||||
|
||||
@@ -70,6 +130,7 @@ export interface Impl {
|
||||
* the mixins that expose core service methods directly on `ctx`.
|
||||
*/
|
||||
export class ReflectService {
|
||||
/** Proxy traps implementing service resolution for every context object. */
|
||||
static handler: ProxyHandler<Context> = {
|
||||
get: (target, prop, ctx: Context) => {
|
||||
if (isSpecialProperty(prop)) {
|
||||
@@ -143,7 +204,9 @@ export class ReflectService {
|
||||
},
|
||||
}
|
||||
|
||||
/** Service implementations, keyed by isolation label. */
|
||||
public store: Dict<Impl, symbol> = Object.create(null)
|
||||
/** Declared context properties (services and accessors), by name. */
|
||||
public props: Dict<Property> = Object.create(null)
|
||||
|
||||
constructor(public ctx: Context) {
|
||||
@@ -158,6 +221,14 @@ export class ReflectService {
|
||||
this.mixin('events', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'waterfall'])
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a service from the store without the inject requirement.
|
||||
*
|
||||
* @param name — the service name.
|
||||
* @param strict — when `true`, only return implementations whose providing
|
||||
* fiber is currently active.
|
||||
* @returns the service value, or `undefined` when not (yet) provided.
|
||||
*/
|
||||
get(name: string, strict = true) {
|
||||
return getTraceable(this.ctx, this._getImpl(name, strict)?.value)
|
||||
}
|
||||
@@ -170,6 +241,15 @@ export class ReflectService {
|
||||
return impl
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite a provided service's value.
|
||||
*
|
||||
* @param name — the service name.
|
||||
* @param value — the new service value.
|
||||
* @param error — carrier for the caller stack in diagnostics.
|
||||
* @returns `true` on success.
|
||||
* @throws when `name` was never provided, or was provided by another fiber.
|
||||
*/
|
||||
set(name: string, value: any, error?: Error) {
|
||||
const key = this.ctx[symbols.isolate][name]
|
||||
const impl = this.store[key]
|
||||
@@ -183,6 +263,16 @@ export class ReflectService {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a service implementation owned by the current fiber.
|
||||
*
|
||||
* See the `ctx.provide()` overload above for the full contract.
|
||||
*
|
||||
* @param name — the service name.
|
||||
* @param value — the service value.
|
||||
* @param check — optional availability predicate for dependents.
|
||||
* @returns a disposer that unregisters the service.
|
||||
*/
|
||||
provide(name: string, value?: any, check?: () => boolean) {
|
||||
return this.ctx.fiber.effect(() => {
|
||||
if (!this.props[name]) {
|
||||
@@ -213,6 +303,13 @@ export class ReflectService {
|
||||
}, `ctx.provide(${JSON.stringify(name)})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-evaluate every fiber that requires one of the given services.
|
||||
*
|
||||
* @param names — the service names that changed.
|
||||
* @param filter — restricts notification to matching isolation scopes.
|
||||
* @returns the fibers whose dependency state was refreshed.
|
||||
*/
|
||||
notify(names: string[], filter = (ctx: Context, name: string) => ctx[symbols.isolate][name] === this.ctx[symbols.isolate][name]) {
|
||||
const fibers: Fiber[] = []
|
||||
for (const runtime of this.ctx.registry.values()) {
|
||||
@@ -232,6 +329,13 @@ export class ReflectService {
|
||||
return fibers
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a computed context property backed by get/set hooks.
|
||||
*
|
||||
* @param name — the context property name.
|
||||
* @param options — the `get` hook and optional `set` hook.
|
||||
* @returns a disposer that removes the accessor.
|
||||
*/
|
||||
accessor(name: string, options: Omit<Property.Accessor, 'type'>) {
|
||||
return this.ctx.fiber.effect(() => {
|
||||
if (name in this.props) {
|
||||
@@ -242,6 +346,15 @@ export class ReflectService {
|
||||
}, `ctx.accessor(${JSON.stringify(name)})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose selected members of a service directly on `ctx`.
|
||||
*
|
||||
* See the `ctx.mixin()` overload above for the full contract.
|
||||
*
|
||||
* @param source — a context property name or a source object.
|
||||
* @param mixins — keys to forward, or a source-key → ctx-key map.
|
||||
* @returns a disposer that removes all created accessors.
|
||||
*/
|
||||
mixin(source: any, mixins: string[] | Dict<string>) {
|
||||
const self = this
|
||||
return this.ctx.fiber.effect(function* () {
|
||||
@@ -270,10 +383,22 @@ export class ReflectService {
|
||||
}, `ctx.mixin(${JSON.stringify(source)})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach this context's tracing wrapper to a value.
|
||||
*
|
||||
* @param value — the value to wrap.
|
||||
* @returns the traceable wrapper (or the value itself when not applicable).
|
||||
*/
|
||||
trace<T>(value: T) {
|
||||
return getTraceable(this.ctx, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a callback so calls trace `this` and arguments to this context.
|
||||
*
|
||||
* @param callback — the function to wrap.
|
||||
* @returns a proxy delegating to `callback` with traced values.
|
||||
*/
|
||||
bind<T extends Function>(callback: T) {
|
||||
return new Proxy(callback, {
|
||||
apply: (target, thisArg, args) => {
|
||||
|
||||
Vendored
+92
-5
@@ -28,6 +28,11 @@ export type InjectKey = keyof {
|
||||
* On classes it contributes to the plugin's static `inject` map. On methods it
|
||||
* delays the method call until the declared services are available.
|
||||
*/
|
||||
/**
|
||||
* @param name — the required service name.
|
||||
* @param config — optional intercept config applied for that service.
|
||||
* @returns the class or method decorator.
|
||||
*/
|
||||
export function Inject<K extends InjectKey>(name: K, config?: Context[K] extends { [symbols.config]: infer T } ? T : never) {
|
||||
return function (value: any, decorator: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) {
|
||||
if (decorator.kind === 'class') {
|
||||
@@ -55,7 +60,13 @@ export function Inject<K extends InjectKey>(name: K, config?: Context[K] extends
|
||||
|
||||
/** Utilities for normalizing plugin dependency declarations. */
|
||||
export namespace Inject {
|
||||
/** Convert array/object/class-inherited inject metadata into a plain map. */
|
||||
/**
|
||||
* Convert array/object/class-inherited inject metadata into a plain map.
|
||||
*
|
||||
* @param inject — the declaration to normalize; `null`/`undefined` add nothing.
|
||||
* @param result — the map to fill (service name → intercept config or `null`).
|
||||
* @returns `result`.
|
||||
*/
|
||||
export function resolve(inject: Inject | null | undefined, result: Dict = Object.create(null)) {
|
||||
if (!inject) return result
|
||||
if (Array.isArray(inject)) {
|
||||
@@ -86,10 +97,15 @@ export type Plugin<T = any> =
|
||||
export namespace Plugin {
|
||||
/** Shared metadata understood by the plugin registry and related tooling. */
|
||||
export interface Base<T = any> {
|
||||
/** Display name used for fiber diagnostics and logger names. */
|
||||
name?: string
|
||||
/** Standard-schema validator applied to config before the plugin starts. */
|
||||
Config?: StandardSchemaV1<any, T>
|
||||
/** Services the plugin requires; it only loads while all are available. */
|
||||
inject?: Inject
|
||||
/** Service name(s) the plugin provides (read by `Service` and by loaders). */
|
||||
provide?: string | string[]
|
||||
/** Service names whose intercept config the plugin declares it consumes. */
|
||||
intercept?: Dict<boolean>
|
||||
}
|
||||
|
||||
@@ -117,9 +133,13 @@ export namespace Plugin {
|
||||
|
||||
/** Mutable registry record shared by all fibers of one plugin callback. */
|
||||
export interface Runtime {
|
||||
/** Display name copied from the first registered plugin shape. */
|
||||
name?: string
|
||||
/** Every live fiber of this plugin (one per `ctx.plugin()` call). */
|
||||
fibers: DisposableList<Fiber>
|
||||
/** The executable entrypoint all fibers share (registry identity key). */
|
||||
callback: globalThis.Function
|
||||
/** Standard-schema validator applied to each fiber's config. */
|
||||
Config?: StandardSchemaV1
|
||||
}
|
||||
}
|
||||
@@ -142,7 +162,25 @@ type GetPluginConfig<P> =
|
||||
|
||||
declare module './context.ts' {
|
||||
export interface Context {
|
||||
/**
|
||||
* Run a callback once the requested services are available.
|
||||
*
|
||||
* Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback
|
||||
* is unloaded and re-run whenever a required service changes.
|
||||
*
|
||||
* @param deps — required services, as an array or a name → config map.
|
||||
* @param callback — plugin body called with `(ctx, config)`.
|
||||
* @returns the fiber; awaiting it settles once loading finished.
|
||||
*/
|
||||
inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
|
||||
/**
|
||||
* Load a plugin in the current context.
|
||||
*
|
||||
* @param plugin — a function, class, or `{ apply }` object plugin.
|
||||
* @param args — the plugin config, validated against its `Config` schema.
|
||||
* @returns the fiber; awaiting it settles once loading finished
|
||||
* (rejecting on config or startup errors).
|
||||
*/
|
||||
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
|
||||
}
|
||||
}
|
||||
@@ -164,15 +202,22 @@ export class RegistryService {
|
||||
})
|
||||
}
|
||||
|
||||
/** Allocate the next fiber uid (increments on every read). */
|
||||
get counter() {
|
||||
return ++this._counter
|
||||
}
|
||||
|
||||
/** Number of registered plugin runtimes. */
|
||||
get size() {
|
||||
return this._internal.size
|
||||
}
|
||||
|
||||
/** Resolve a supported plugin shape to its executable callback. */
|
||||
/**
|
||||
* Resolve a supported plugin shape to its executable callback.
|
||||
*
|
||||
* @param plugin — a function, class, or `{ apply }` object plugin.
|
||||
* @returns the callback identifying the plugin, or `undefined` if invalid.
|
||||
*/
|
||||
resolve(plugin: Plugin): Function | undefined {
|
||||
// plugin.apply may throw
|
||||
try {
|
||||
@@ -181,17 +226,34 @@ export class RegistryService {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the runtime record for a plugin.
|
||||
*
|
||||
* @param plugin — any supported plugin shape.
|
||||
* @returns the runtime, or `undefined` when the plugin is not registered.
|
||||
*/
|
||||
get(plugin: Plugin) {
|
||||
const key = this.resolve(plugin)
|
||||
return key && this._internal.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a plugin has a registered runtime.
|
||||
*
|
||||
* @param plugin — any supported plugin shape.
|
||||
* @returns `true` when at least one fiber of the plugin exists.
|
||||
*/
|
||||
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. */
|
||||
/**
|
||||
* Dispose every running fiber for a plugin and remove its runtime record.
|
||||
*
|
||||
* @param plugin — any supported plugin shape.
|
||||
* @returns the removed runtime, or `undefined` when none was registered.
|
||||
*/
|
||||
delete(plugin: Plugin) {
|
||||
const key = this.resolve(plugin)
|
||||
const runtime = key && this._internal.get(key)
|
||||
@@ -203,28 +265,53 @@ export class RegistryService {
|
||||
return runtime
|
||||
}
|
||||
|
||||
/** Iterate the registered plugin callbacks. */
|
||||
keys() {
|
||||
return this._internal.keys()
|
||||
}
|
||||
|
||||
/** Iterate the registered plugin runtimes. */
|
||||
values() {
|
||||
return this._internal.values()
|
||||
}
|
||||
|
||||
/** Iterate `[callback, runtime]` pairs. */
|
||||
entries() {
|
||||
return this._internal.entries()
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit every registered runtime.
|
||||
*
|
||||
* @param callback — receives each runtime and its identifying callback.
|
||||
*/
|
||||
forEach(callback: (value: Plugin.Runtime, key: Function) => void) {
|
||||
return this._internal.forEach(callback)
|
||||
}
|
||||
|
||||
/** Start a callback once the requested dependencies are available. */
|
||||
/**
|
||||
* Start a callback once the requested dependencies are available.
|
||||
*
|
||||
* @param inject — required services, as an array or a name → config map.
|
||||
* @param callback — plugin body called with `(ctx, config)`.
|
||||
* @returns the fiber; awaiting it settles once loading finished.
|
||||
*/
|
||||
inject(inject: Inject, callback: Plugin.Function<void>) {
|
||||
return this.plugin({ inject, apply: callback, name: callback.name })
|
||||
}
|
||||
|
||||
/** Start a plugin in the current context and return its fiber. */
|
||||
/**
|
||||
* Start a plugin in the current context and return its fiber.
|
||||
*
|
||||
* Creates (or reuses) the plugin's runtime record, then starts a new fiber
|
||||
* under the current context. Throws if `plugin` is not a supported shape or
|
||||
* if the current fiber is already disposed.
|
||||
*
|
||||
* @param plugin — a function, class, or `{ apply }` object plugin.
|
||||
* @param config — the plugin config, validated against its `Config` schema.
|
||||
* @param getOuterStack — captures the caller stack for effect diagnostics.
|
||||
* @returns the fiber; awaiting it settles once loading finished.
|
||||
*/
|
||||
plugin(plugin: Plugin, config?: any, getOuterStack = buildOuterStack()) {
|
||||
// check if it's a valid plugin
|
||||
const callback = this.resolve(plugin)
|
||||
|
||||
Vendored
+29
-2
@@ -9,19 +9,36 @@ import { createCallable, joinPrototype, symbols, Tracker } from './utils.ts'
|
||||
* registered immediately and is automatically removed with the owning fiber.
|
||||
*/
|
||||
export abstract class Service<out T = never> {
|
||||
/** Symbol key of an instance method run after construction (class plugins). */
|
||||
static readonly init: unique symbol = symbols.init
|
||||
/** Symbol key of the availability predicate passed to `ctx.provide()`. */
|
||||
static readonly check: unique symbol = symbols.check
|
||||
/** Symbol key of the phantom intercept-config type parameter. */
|
||||
static readonly config: unique symbol = symbols.config
|
||||
/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */
|
||||
static readonly invoke: unique symbol = symbols.invoke
|
||||
/** Symbol key of the helper deriving an extended service instance. */
|
||||
static readonly extend: unique symbol = symbols.extend
|
||||
/** Symbol key of the tracker metadata used for context tracing. */
|
||||
static readonly tracker: unique symbol = symbols.tracker
|
||||
/** Symbol key of the intercept-config resolution helper below. */
|
||||
static readonly resolveConfig: unique symbol = symbols.resolveConfig
|
||||
|
||||
declare [symbols.config]: T
|
||||
|
||||
/** The service name this instance is registered under. */
|
||||
public name!: string
|
||||
|
||||
/** Register this instance as `name` in the current context. */
|
||||
/**
|
||||
* Register this instance as `name` in the current context.
|
||||
*
|
||||
* Calls `ctx.reflect.provide(name, this, this[Service.check])`, so the
|
||||
* service is unregistered automatically when the owning fiber unloads.
|
||||
* Services with a `[Service.invoke]` body return a callable instance.
|
||||
*
|
||||
* @param ctx — the context to register in (stored as `this.ctx`).
|
||||
* @param name — the service name; defaults to the static `provide` field.
|
||||
*/
|
||||
constructor(protected ctx: Context, name: string) {
|
||||
name ??= this.constructor['provide'] as string
|
||||
|
||||
@@ -55,7 +72,17 @@ export abstract class Service<out T = never> {
|
||||
return Object.assign(self, props)
|
||||
}
|
||||
|
||||
/** Merge intercept config from ancestors with optional base and head values. */
|
||||
/**
|
||||
* Merge intercept config from ancestors with optional base and head values.
|
||||
*
|
||||
* Entries added closer to the root apply first; `base` is prepended and
|
||||
* `head` appended. Uses `Config.merge` when the service declares one,
|
||||
* otherwise a shallow `Object.assign`.
|
||||
*
|
||||
* @param base — lowest-precedence config merged before all intercepts.
|
||||
* @param head — highest-precedence config merged after all intercepts.
|
||||
* @returns the merged config.
|
||||
*/
|
||||
[symbols.resolveConfig](base?: T, head?: T): T {
|
||||
let intercept = this.ctx[Context.intercept]
|
||||
const configs: any[] = []
|
||||
|
||||
Reference in New Issue
Block a user