/** * 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 = object & { readonly [ScopedBrand]: T } /** The key associated with each carrier. Presence distinguishes an unkeyed carrier from a non-carrier. */ const carrierKeys = new WeakMap() /** 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 /** Dispose every scope-owned registration; racing calls await the same completion. */ dispose(): Promise } /** Follow a Cordis fiber through asynchronous teardown even if its raw disposer was already claimed. */ async function quiesceFiber(fiber: Fiber): Promise { 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 | 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(base: T, key: ScopeKey | undefined): Scoped { 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 } /** * 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 { 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) }