| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- /**
- * Scoped-context primitive: mint a Cordis context that tags registrations with
- * an opaque identity and build routing-only event carriers for that identity.
- *
- * @module @deepseek-ai/dsh-scope
- */
- import type { Context, Fiber } from 'cordis'
- import { Context as CordisContext } from 'cordis'
- export { AnonymousEntries, NamedEntries, ScopedLayers } from './store.ts'
- export type { ScopeLayer } from './store.ts'
- /** An opaque, identity-compared scope key. */
- export type ScopeKey = object
- /** Context tag written by {@link createScope}. */
- const kScope = Symbol('dsh.scope')
- declare const ScopedBrand: unique symbol
- /**
- * A routing-only event receiver built by {@link scopeTarget}. The type
- * parameter records the subject type for dispatch checking; the carrier does
- * not expose the subject's properties. Event payloads carry the real subject.
- */
- export type Scoped<T extends object> = object & { readonly [ScopedBrand]: T }
- /** The key associated with each carrier. Presence distinguishes an unkeyed carrier from a non-carrier. */
- const carrierKeys = new WeakMap<object, ScopeKey | undefined>()
- /** A minted registration scope and its quiescent disposal boundaries. */
- export interface Scope {
- /** Context through which scope-owned registrations are made. */
- ctx: Context
- /** Exact Cordis disposer, used when nesting this scope in an ordered composite effect. */
- rawDispose: () => Promise<void> | void
- /** Dispose every scope-owned registration; racing calls await the same completion. */
- dispose(): Promise<void>
- }
- /** Follow a Cordis fiber through asynchronous teardown even if its raw disposer was already claimed. */
- async function quiesceFiber(fiber: Fiber): Promise<void> {
- await Promise.resolve(fiber.dispose())
- while (fiber.inertia !== undefined) await fiber.inertia
- }
- /** Shared no-op plugin used as the backing scope fiber. */
- function scope(): void {}
- /**
- * Mint a scope under `ctx`. The scoped context inherits the minting plugin's
- * dependency surface and owns every registration made through it.
- * @param ctx - active context whose dependency surface the scope inherits.
- * @param key - opaque identity used for listener routing.
- * @returns the scoped context and exact/shared disposal boundaries.
- */
- export function createScope(ctx: Context, key: ScopeKey): Scope {
- const fiber = ctx.plugin(scope)
- const scoped: Context = fiber.ctx.extend({ [kScope]: key })
- let disposing: Promise<void> | undefined
- return {
- ctx: scoped,
- rawDispose: fiber.dispose,
- dispose: () => (disposing ??= quiesceFiber(fiber)),
- }
- }
- /**
- * Read the nearest scope tag inherited by a context.
- * @param ctx - context to inspect.
- * @returns its scope key, or `undefined` for an unscoped context.
- */
- export function scopeOf(ctx: Context): ScopeKey | undefined {
- return (ctx as Context & { [kScope]?: ScopeKey })[kScope]
- }
- /**
- * Build an opaque receiver that preserves the base filter, admits untagged
- * listeners globally, and admits tagged listeners only for a matching key.
- * @param base - subject or service whose existing Cordis filter is preserved.
- * @param key - routed scope identity, or `undefined` for an unscoped subject.
- * @returns a carrier whose subject remains available only through event arguments.
- */
- export function scopeTarget<T extends object>(base: T, key: ScopeKey | undefined): Scoped<T> {
- const baseFilter = (base as { [CordisContext.filter]?: (ctx: Context) => boolean })[CordisContext.filter]
- const carrier = {
- [CordisContext.filter](ctx: Context): boolean {
- if (baseFilter !== undefined && !baseFilter.call(base, ctx)) return false
- const tag = scopeOf(ctx)
- return tag === undefined || tag === key
- },
- }
- carrierKeys.set(carrier, key)
- return carrier as unknown as Scoped<T>
- }
- /**
- * Test whether a value is a scope carrier.
- * @param value - dispatch receiver to inspect.
- * @returns whether {@link scopeTarget} created it.
- */
- export function isScopeCarrier(value: unknown): value is Scoped<object> {
- return typeof value === 'object' && value !== null && carrierKeys.has(value)
- }
- /**
- * Read a carrier's routing key.
- * @param value - dispatch receiver to inspect.
- * @returns the carrier key, or `undefined` for an unkeyed/non-carrier value.
- */
- export function carrierKeyOf(value: unknown): ScopeKey | undefined {
- if (!isScopeCarrier(value)) return undefined
- return carrierKeys.get(value)
- }
|