import * as NodeAsyncHooks from "node:async_hooks"; import * as Cause from "effect/Cause";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as Schema from "effect/Schema";import * as Scheduler from "effect/Scheduler";import * as Semaphore from "effect/Semaphore";import * as Scope from "effect/Scope"; import type { Contribution, ContributionData, PluginActivationContext, PluginDefinition, PluginRuntimeContributionSnapshot, PluginRuntimeOptions, PluginRuntimeSnapshot,} from "./contract.ts";import { affectedPluginIds, isPluginPlanningError, planComposition, type PluginPlanningError,} from "./planner.ts"; interface LiveContribution { readonly contribution: Contribution; readonly value: unknown;} interface LivePlugin { readonly definition: PluginDefinition; readonly scope: Scope.Closeable; readonly contributions: ReadonlyMap<string, ReadonlyArray<LiveContribution>>; readonly cleanupErrors: Array<unknown>;} interface LiveComposition { readonly generation: number; readonly plugins: ReadonlyArray<LivePlugin>; readonly snapshot: PluginRuntimeSnapshot;} type RuntimeOperation = "reconcile" | "dispose" | "invoke";type PluginLifecycleCallback = "activate" | "finalizer";type PluginCallback = PluginLifecycleCallback | "contribution"; interface PluginCallbackContext { active: boolean; readonly callback: PluginCallback; readonly pluginId: string;} interface PluginEffectCallbackContext extends PluginCallbackContext { readonly runtime: object;} class PluginEffectCallback extends Context.Reference<PluginEffectCallbackContext | undefined>( "@t3tools/plugin-runtime/runtime/PluginEffectCallback", { defaultValue: () => undefined },) {} interface CleanupFailure { readonly error: unknown; readonly pluginId: string;} class PluginResolutionError extends Schema.TaggedErrorClass<PluginResolutionError>()( "PluginResolutionError", { capability: Schema.String, pluginId: Schema.String },) { override get message(): string { return `Plugin ${this.pluginId} cannot resolve inactive capability: ${this.capability}`; }} class PluginUndeclaredCapabilityError extends Schema.TaggedErrorClass<PluginUndeclaredCapabilityError>()( "PluginUndeclaredCapabilityError", { capability: Schema.String, pluginId: Schema.String },) { override get message(): string { return `Plugin ${this.pluginId} did not declare capability: ${this.capability}`; }} class PluginActivationContextExpiredError extends Schema.TaggedErrorClass<PluginActivationContextExpiredError>()( "PluginActivationContextExpiredError", { method: Schema.Literals(["resolve", "register", "onDispose"]), pluginId: Schema.String, },) { override get message(): string { return `activation context for ${this.pluginId} is no longer active (${this.method})`; }} class PluginCallbackError extends Schema.TaggedErrorClass<PluginCallbackError>()( "PluginCallbackError", { callback: Schema.Literals(["activate", "finalizer"]), cause: Schema.Defect(), pluginId: Schema.String, },) { override get message(): string { return `Plugin ${this.pluginId} ${this.callback} callback failed`; }} class PluginStagingError extends Schema.TaggedErrorClass<PluginStagingError>()( "PluginStagingError", { pluginId: Schema.String },) { override get message(): string { return `Plugin ${this.pluginId} was not staged`; }} export class PluginDuplicateContributionError extends Schema.TaggedErrorClass<PluginDuplicateContributionError>()( "PluginDuplicateContributionError", { firstPluginId: Schema.String, id: Schema.String, secondPluginId: Schema.String, slot: Schema.String, },) { override get message(): string { return `Duplicate plugin contribution ${this.slot}/${this.id} from ${this.firstPluginId} and ${this.secondPluginId}`; }} class PluginRuntimeDisposedError extends Schema.TaggedErrorClass<PluginRuntimeDisposedError>()( "PluginRuntimeDisposedError", { operation: Schema.Literals(["reconcile", "dispose", "invoke"]) },) { override get message(): string { return `Plugin runtime is disposed; cannot ${this.operation}`; }} class PluginRuntimeReentrancyError extends Schema.TaggedErrorClass<PluginRuntimeReentrancyError>()( "PluginRuntimeReentrancyError", { callback: Schema.Literals(["activate", "contribution", "finalizer"]), operation: Schema.Literals(["reconcile", "dispose", "invoke"]), pluginId: Schema.String, },) { override get message(): string { return `Plugin runtime ${this.operation} is reentrant from ${this.callback} callback for ${this.pluginId}`; }} class PluginRuntimeCleanupError extends Schema.TaggedErrorClass<PluginRuntimeCleanupError>()( "PluginRuntimeCleanupError", { failures: Schema.Array( Schema.Struct({ cause: Schema.Defect(), pluginId: Schema.String, }), ), },) { override get message(): string { return `Failed to close plugin scopes (${this.failures.length} cleanup error${this.failures.length === 1 ? "" : "s"})`; }} export type PluginRuntimeReconcileError = | PluginPlanningError | PluginCallbackError | PluginDuplicateContributionError | PluginRuntimeDisposedError | PluginRuntimeReentrancyError | PluginSnapshotValidationError | PluginStagingError; export class PluginSnapshotValidationError extends Schema.TaggedErrorClass<PluginSnapshotValidationError>()( "PluginSnapshotValidationError", { cause: Schema.Defect() },) { override get message(): string { return "Plugin contribution snapshot validation failed"; }} export type PluginRuntimeDisposeError = PluginRuntimeCleanupError | PluginRuntimeReentrancyError; export class PluginContributionGenerationError extends Schema.TaggedErrorClass<PluginContributionGenerationError>()( "PluginContributionGenerationError", { actual: Schema.Int, expected: Schema.Int },) { override get message(): string { return `Plugin contribution generation changed from ${this.expected} to ${this.actual}`; }} export class PluginContributionNotFoundError extends Schema.TaggedErrorClass<PluginContributionNotFoundError>()( "PluginContributionNotFoundError", { id: Schema.String, slot: Schema.String },) { override get message(): string { return `Plugin contribution not found: ${this.slot}/${this.id}`; }} export type PluginRuntimeContributionError = | PluginContributionGenerationError | PluginContributionNotFoundError | PluginRuntimeDisposedError | PluginRuntimeReentrancyError; export class PluginRuntime extends Context.Service< PluginRuntime, { readonly reconcile: ( definitions: ReadonlyArray<PluginDefinition>, ) => Effect.Effect<PluginRuntimeSnapshot, PluginRuntimeReconcileError>; readonly snapshot: Effect.Effect<PluginRuntimeSnapshot>; readonly contributions: (slot: string) => Effect.Effect<PluginRuntimeContributionSnapshot>; readonly useContribution: <Value, Success, Failure, Requirements>( slot: string, id: string, generation: number, use: (value: Value) => Effect.Effect<Success, Failure, Requirements>, ) => Effect.Effect<Success, Failure | PluginRuntimeContributionError, Requirements>; readonly dispose: Effect.Effect<void, PluginRuntimeDisposeError>; }>()("@t3tools/plugin-runtime/runtime/PluginRuntime") {} const createNullPrototypeRecord = <Value>(): Record<string, Value> => Object.create(null) as Record<string, Value>; const sameStringRecord = ( left: Readonly<Partial<Record<string, string>>>, right: Readonly<Partial<Record<string, string>>>,): boolean => { const leftKeys = Object.keys(left); if (leftKeys.length !== Object.keys(right).length) return false; return leftKeys.every((key) => left[key] === right[key]);}; const cloneContributionData = ( value: ContributionData, ancestors = new WeakSet<object>(),): ContributionData => { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number") { if (!Number.isFinite(value)) throw new TypeError("Contribution data numbers must be finite"); return value; } if (typeof value !== "object") { throw new TypeError("Contribution data must contain only JSON-compatible values"); } if (ancestors.has(value)) throw new TypeError("Contribution data cannot contain cycles"); ancestors.add(value); try { if (Array.isArray(value)) { const clone: Array<ContributionData> = []; for (let index = 0; index < value.length; index += 1) { if (!Object.hasOwn(value, index)) { throw new TypeError("Contribution data arrays cannot contain holes"); } clone.push(cloneContributionData(value[index]!, ancestors)); } return Object.freeze(clone); } const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) { throw new TypeError("Contribution data objects must use a plain or null prototype"); } const clone = createNullPrototypeRecord<ContributionData>(); for (const key of Reflect.ownKeys(value)) { if (typeof key !== "string") { throw new TypeError("Contribution data objects cannot use symbol keys"); } const descriptor = Object.getOwnPropertyDescriptor(value, key); if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) { throw new TypeError("Contribution data objects must use enumerable data properties"); } clone[key] = cloneContributionData(descriptor.value as ContributionData, ancestors); } return Object.freeze(clone); } finally { ancestors.delete(value); }}; const detachContribution = (contribution: Contribution): Contribution => Object.freeze({ id: contribution.id, label: contribution.label, ...(contribution.data === undefined ? {} : { data: cloneContributionData(contribution.data) }), }); const snapshotDefinitions = ( definitions: ReadonlyArray<PluginDefinition>,): ReadonlyArray<PluginDefinition> => definitions.map((definition) => { const requires = definition.requires === undefined ? undefined : Object.freeze([...definition.requires]); const provides = definition.provides === undefined ? undefined : Object.freeze(Object.assign(createNullPrototypeRecord<unknown>(), definition.provides)); return Object.freeze({ id: definition.id, version: definition.version, activate: definition.activate, ...(requires === undefined ? {} : { requires }), ...(provides === undefined ? {} : { provides }), }); }); const emptySnapshot = (): PluginRuntimeSnapshot => Object.freeze({ active: Object.freeze([]), blocked: Object.freeze(createNullPrototypeRecord<string>()), contributions: Object.freeze(createNullPrototypeRecord<ReadonlyArray<Contribution>>()), }); const snapshotOf = ( plugins: ReadonlyArray<LivePlugin>, blocked: Readonly<Record<string, string>>,): PluginRuntimeSnapshot => { const contributions = createNullPrototypeRecord<ReadonlyArray<Contribution>>(); for (const plugin of plugins) { for (const [slot, registrations] of plugin.contributions) { const values = contributions[slot] ?? []; contributions[slot] = Object.freeze([ ...values, ...registrations.map((registration) => registration.contribution), ]); } } return Object.freeze({ active: Object.freeze(plugins.map((plugin) => plugin.definition.id)), blocked: Object.freeze(Object.assign(createNullPrototypeRecord<string>(), blocked)), contributions: Object.freeze(contributions), });}; const validateUniqueContributions = ( plugins: ReadonlyArray<LivePlugin>,): Effect.Effect<void, PluginDuplicateContributionError> => Effect.gen(function* () { const owners = new Map<string, Map<string, string>>(); for (const plugin of plugins) { for (const [slot, registrations] of plugin.contributions) { const slotOwners = owners.get(slot) ?? new Map<string, string>(); owners.set(slot, slotOwners); for (const registration of registrations) { const id = registration.contribution.id; const firstPluginId = slotOwners.get(id); if (firstPluginId !== undefined) { return yield* new PluginDuplicateContributionError({ firstPluginId, id, secondPluginId: plugin.definition.id, slot, }); } slotOwners.set(id, plugin.definition.id); } } } }); export const make = (options: PluginRuntimeOptions = {}) => Effect.gen(function* () { const parentScope = yield* Effect.scope; const transitionSemaphore = yield* Semaphore.make(1); const baseScheduler = yield* Scheduler.Scheduler; const runtimeIdentity = {}; let current: LiveComposition = { generation: 0, plugins: [], snapshot: emptySnapshot() }; let disposalStarted = false; let disposed = false; const callbackContext = new NodeAsyncHooks.AsyncLocalStorage<PluginCallbackContext>(); const reportLifecycle = ( phase: "activate" | "deactivate", pluginId: string, ): Effect.Effect<void> => Effect.sync(() => { try { options.onLifecycle?.({ phase, pluginId }); } catch (error) { try { options.onLifecycleError?.({ phase, pluginId, error }); } catch { // Observer error reporting must never interrupt a commit or cleanup. } } }); const reportCleanupErrors = ( phase: "retire" | "rollback", failures: ReadonlyArray<CleanupFailure>, ): Effect.Effect<void> => Effect.sync(() => { for (const { error } of failures) { try { options.onCleanupError?.({ phase, error }); } catch { // Cleanup reporting must not replace activation errors or undo a committed snapshot. } } }); const closePlugins = ( plugins: ReadonlyArray<LivePlugin>, notifyDeactivation: boolean, ): Effect.Effect<ReadonlyArray<CleanupFailure>> => Effect.gen(function* () { const failures: Array<CleanupFailure> = []; for (const plugin of plugins.toReversed()) { const closeExit = yield* Effect.exit(Scope.close(plugin.scope, Exit.void)); if (Exit.isFailure(closeExit)) { failures.push({ error: Cause.squash(closeExit.cause), pluginId: plugin.definition.id, }); } for (const error of plugin.cleanupErrors.splice(0)) { failures.push({ error, pluginId: plugin.definition.id }); } if (notifyDeactivation) { yield* reportLifecycle("deactivate", plugin.definition.id); } } return failures; }); const invokePluginCallback = <Result>( callback: PluginLifecycleCallback, pluginId: string, invoke: () => Result | PromiseLike<Result>, onSettled?: () => void, ): Effect.Effect<Result, PluginCallbackError> => { const callbackState: PluginCallbackContext = { active: true, callback, pluginId }; let expired = false; const expire = () => { if (expired) return; expired = true; onSettled?.(); }; const settleCallback = () => { callbackState.active = false; expire(); }; return Effect.tryPromise({ try: async () => { try { const result = callbackContext.run(callbackState, invoke); if (typeof result === "object" && result !== null && "then" in result) { return await Promise.resolve(result).finally(settleCallback); } settleCallback(); return result; } catch (error) { settleCallback(); throw error; } }, catch: (cause) => new PluginCallbackError({ callback, cause, pluginId }), }).pipe(Effect.ensuring(Effect.sync(expire))); }; const activatePlugin = ( definition: PluginDefinition, capabilities: ReadonlyMap<string, unknown>, ): Effect.Effect<LivePlugin, PluginCallbackError> => Effect.uninterruptibleMask((restore) => Effect.gen(function* () { const scope = yield* Scope.fork(parentScope, "sequential"); const contributions = new Map<string, Array<LiveContribution>>(); const cleanupErrors: Array<unknown> = []; const finalizers: Array<() => void | Promise<void>> = []; const plugin: LivePlugin = { definition, scope, contributions, cleanupErrors }; let activating = true; const assertActivating = (method: "resolve" | "register" | "onDispose") => { if (!activating) { throw new PluginActivationContextExpiredError({ method, pluginId: definition.id }); } }; const context: PluginActivationContext = { resolve: <Service>(capability: string): Service => { assertActivating("resolve"); if (!(definition.requires ?? []).includes(capability)) { throw new PluginUndeclaredCapabilityError({ capability, pluginId: definition.id }); }Lines 1–500 of 809. Load more
import * as NodeAsyncHooks from "node:async_hooks"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import * as Scheduler from "effect/Scheduler"; import * as Semaphore from "effect/Semaphore"; import * as Scope from "effect/Scope"; import type { Contribution, ContributionData, PluginActivationContext, PluginDefinition, PluginRuntimeContributionSnapshot, PluginRuntimeOptions, PluginRuntimeSnapshot, } from "./contract.ts"; import { affectedPluginIds, isPluginPlanningError, planComposition, type PluginPlanningError, } from "./planner.ts"; interface LiveContribution { readonly contribution: Contribution; readonly value: unknown; } interface LivePlugin { readonly definition: PluginDefinition; readonly scope: Scope.Closeable; readonly contributions: ReadonlyMap<string, ReadonlyArray<LiveContribution>>; readonly cleanupErrors: Array<unknown>; } interface LiveComposition { readonly generation: number; readonly plugins: ReadonlyArray<LivePlugin>; readonly snapshot: PluginRuntimeSnapshot; } type RuntimeOperation = "reconcile" | "dispose" | "invoke"; type PluginLifecycleCallback = "activate" | "finalizer"; type PluginCallback = PluginLifecycleCallback | "contribution"; interface PluginCallbackContext { active: boolean; readonly callback: PluginCallback; readonly pluginId: string; } interface PluginEffectCallbackContext extends PluginCallbackContext { readonly runtime: object; } class PluginEffectCallback extends Context.Reference<PluginEffectCallbackContext | undefined>( "@t3tools/plugin-runtime/runtime/PluginEffectCallback", { defaultValue: () => undefined }, ) {} interface CleanupFailure { readonly error: unknown; readonly pluginId: string; } class PluginResolutionError extends Schema.TaggedErrorClass<PluginResolutionError>()( "PluginResolutionError", { capability: Schema.String, pluginId: Schema.String }, ) { override get message(): string { return `Plugin ${this.pluginId} cannot resolve inactive capability: ${this.capability}`; } } class PluginUndeclaredCapabilityError extends Schema.TaggedErrorClass<PluginUndeclaredCapabilityError>()( "PluginUndeclaredCapabilityError", { capability: Schema.String, pluginId: Schema.String }, ) { override get message(): string { return `Plugin ${this.pluginId} did not declare capability: ${this.capability}`; } } class PluginActivationContextExpiredError extends Schema.TaggedErrorClass<PluginActivationContextExpiredError>()( "PluginActivationContextExpiredError", { method: Schema.Literals(["resolve", "register", "onDispose"]), pluginId: Schema.String, }, ) { override get message(): string { return `activation context for ${this.pluginId} is no longer active (${this.method})`; } } class PluginCallbackError extends Schema.TaggedErrorClass<PluginCallbackError>()( "PluginCallbackError", { callback: Schema.Literals(["activate", "finalizer"]), cause: Schema.Defect(), pluginId: Schema.String, }, ) { override get message(): string { return `Plugin ${this.pluginId} ${this.callback} callback failed`; } } class PluginStagingError extends Schema.TaggedErrorClass<PluginStagingError>()( "PluginStagingError", { pluginId: Schema.String }, ) { override get message(): string { return `Plugin ${this.pluginId} was not staged`; } } export class PluginDuplicateContributionError extends Schema.TaggedErrorClass<PluginDuplicateContributionError>()( "PluginDuplicateContributionError", { firstPluginId: Schema.String, id: Schema.String, secondPluginId: Schema.String, slot: Schema.String, }, ) { override get message(): string { return `Duplicate plugin contribution ${this.slot}/${this.id} from ${this.firstPluginId} and ${this.secondPluginId}`; } } class PluginRuntimeDisposedError extends Schema.TaggedErrorClass<PluginRuntimeDisposedError>()( "PluginRuntimeDisposedError", { operation: Schema.Literals(["reconcile", "dispose", "invoke"]) }, ) { override get message(): string { return `Plugin runtime is disposed; cannot ${this.operation}`; } } class PluginRuntimeReentrancyError extends Schema.TaggedErrorClass<PluginRuntimeReentrancyError>()( "PluginRuntimeReentrancyError", { callback: Schema.Literals(["activate", "contribution", "finalizer"]), operation: Schema.Literals(["reconcile", "dispose", "invoke"]), pluginId: Schema.String, }, ) { override get message(): string { return `Plugin runtime ${this.operation} is reentrant from ${this.callback} callback for ${this.pluginId}`; } } class PluginRuntimeCleanupError extends Schema.TaggedErrorClass<PluginRuntimeCleanupError>()( "PluginRuntimeCleanupError", { failures: Schema.Array( Schema.Struct({ cause: Schema.Defect(), pluginId: Schema.String, }), ), }, ) { override get message(): string { return `Failed to close plugin scopes (${this.failures.length} cleanup error${this.failures.length === 1 ? "" : "s"})`; } } export type PluginRuntimeReconcileError = | PluginPlanningError | PluginCallbackError | PluginDuplicateContributionError | PluginRuntimeDisposedError | PluginRuntimeReentrancyError | PluginSnapshotValidationError | PluginStagingError; export class PluginSnapshotValidationError extends Schema.TaggedErrorClass<PluginSnapshotValidationError>()( "PluginSnapshotValidationError", { cause: Schema.Defect() }, ) { override get message(): string { return "Plugin contribution snapshot validation failed"; } } export type PluginRuntimeDisposeError = PluginRuntimeCleanupError | PluginRuntimeReentrancyError; export class PluginContributionGenerationError extends Schema.TaggedErrorClass<PluginContributionGenerationError>()( "PluginContributionGenerationError", { actual: Schema.Int, expected: Schema.Int }, ) { override get message(): string { return `Plugin contribution generation changed from ${this.expected} to ${this.actual}`; } } export class PluginContributionNotFoundError extends Schema.TaggedErrorClass<PluginContributionNotFoundError>()( "PluginContributionNotFoundError", { id: Schema.String, slot: Schema.String }, ) { override get message(): string { return `Plugin contribution not found: ${this.slot}/${this.id}`; } } export type PluginRuntimeContributionError = | PluginContributionGenerationError | PluginContributionNotFoundError | PluginRuntimeDisposedError | PluginRuntimeReentrancyError; export class PluginRuntime extends Context.Service< PluginRuntime, { readonly reconcile: ( definitions: ReadonlyArray<PluginDefinition>, ) => Effect.Effect<PluginRuntimeSnapshot, PluginRuntimeReconcileError>; readonly snapshot: Effect.Effect<PluginRuntimeSnapshot>; readonly contributions: (slot: string) => Effect.Effect<PluginRuntimeContributionSnapshot>; readonly useContribution: <Value, Success, Failure, Requirements>( slot: string, id: string, generation: number, use: (value: Value) => Effect.Effect<Success, Failure, Requirements>, ) => Effect.Effect<Success, Failure | PluginRuntimeContributionError, Requirements>; readonly dispose: Effect.Effect<void, PluginRuntimeDisposeError>; } >()("@t3tools/plugin-runtime/runtime/PluginRuntime") {} const createNullPrototypeRecord = <Value>(): Record<string, Value> => Object.create(null) as Record<string, Value>; const sameStringRecord = ( left: Readonly<Partial<Record<string, string>>>, right: Readonly<Partial<Record<string, string>>>, ): boolean => { const leftKeys = Object.keys(left); if (leftKeys.length !== Object.keys(right).length) return false; return leftKeys.every((key) => left[key] === right[key]); }; const cloneContributionData = ( value: ContributionData, ancestors = new WeakSet<object>(), ): ContributionData => { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number") { if (!Number.isFinite(value)) throw new TypeError("Contribution data numbers must be finite"); return value; } if (typeof value !== "object") { throw new TypeError("Contribution data must contain only JSON-compatible values"); } if (ancestors.has(value)) throw new TypeError("Contribution data cannot contain cycles"); ancestors.add(value); try { if (Array.isArray(value)) { const clone: Array<ContributionData> = []; for (let index = 0; index < value.length; index += 1) { if (!Object.hasOwn(value, index)) { throw new TypeError("Contribution data arrays cannot contain holes"); } clone.push(cloneContributionData(value[index]!, ancestors)); } return Object.freeze(clone); } const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) { throw new TypeError("Contribution data objects must use a plain or null prototype"); } const clone = createNullPrototypeRecord<ContributionData>(); for (const key of Reflect.ownKeys(value)) { if (typeof key !== "string") { throw new TypeError("Contribution data objects cannot use symbol keys"); } const descriptor = Object.getOwnPropertyDescriptor(value, key); if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) { throw new TypeError("Contribution data objects must use enumerable data properties"); } clone[key] = cloneContributionData(descriptor.value as ContributionData, ancestors); } return Object.freeze(clone); } finally { ancestors.delete(value); } }; const detachContribution = (contribution: Contribution): Contribution => Object.freeze({ id: contribution.id, label: contribution.label, ...(contribution.data === undefined ? {} : { data: cloneContributionData(contribution.data) }), }); const snapshotDefinitions = ( definitions: ReadonlyArray<PluginDefinition>, ): ReadonlyArray<PluginDefinition> => definitions.map((definition) => { const requires = definition.requires === undefined ? undefined : Object.freeze([...definition.requires]); const provides = definition.provides === undefined ? undefined : Object.freeze(Object.assign(createNullPrototypeRecord<unknown>(), definition.provides)); return Object.freeze({ id: definition.id, version: definition.version, activate: definition.activate, ...(requires === undefined ? {} : { requires }), ...(provides === undefined ? {} : { provides }), }); }); const emptySnapshot = (): PluginRuntimeSnapshot => Object.freeze({ active: Object.freeze([]), blocked: Object.freeze(createNullPrototypeRecord<string>()), contributions: Object.freeze(createNullPrototypeRecord<ReadonlyArray<Contribution>>()), }); const snapshotOf = ( plugins: ReadonlyArray<LivePlugin>, blocked: Readonly<Record<string, string>>, ): PluginRuntimeSnapshot => { const contributions = createNullPrototypeRecord<ReadonlyArray<Contribution>>(); for (const plugin of plugins) { for (const [slot, registrations] of plugin.contributions) { const values = contributions[slot] ?? []; contributions[slot] = Object.freeze([ ...values, ...registrations.map((registration) => registration.contribution), ]); } } return Object.freeze({ active: Object.freeze(plugins.map((plugin) => plugin.definition.id)), blocked: Object.freeze(Object.assign(createNullPrototypeRecord<string>(), blocked)), contributions: Object.freeze(contributions), }); }; const validateUniqueContributions = ( plugins: ReadonlyArray<LivePlugin>, ): Effect.Effect<void, PluginDuplicateContributionError> => Effect.gen(function* () { const owners = new Map<string, Map<string, string>>(); for (const plugin of plugins) { for (const [slot, registrations] of plugin.contributions) { const slotOwners = owners.get(slot) ?? new Map<string, string>(); owners.set(slot, slotOwners); for (const registration of registrations) { const id = registration.contribution.id; const firstPluginId = slotOwners.get(id); if (firstPluginId !== undefined) { return yield* new PluginDuplicateContributionError({ firstPluginId, id, secondPluginId: plugin.definition.id, slot, }); } slotOwners.set(id, plugin.definition.id); } } } }); export const make = (options: PluginRuntimeOptions = {}) => Effect.gen(function* () { const parentScope = yield* Effect.scope; const transitionSemaphore = yield* Semaphore.make(1); const baseScheduler = yield* Scheduler.Scheduler; const runtimeIdentity = {}; let current: LiveComposition = { generation: 0, plugins: [], snapshot: emptySnapshot() }; let disposalStarted = false; let disposed = false; const callbackContext = new NodeAsyncHooks.AsyncLocalStorage<PluginCallbackContext>(); const reportLifecycle = ( phase: "activate" | "deactivate", pluginId: string, ): Effect.Effect<void> => Effect.sync(() => { try { options.onLifecycle?.({ phase, pluginId }); } catch (error) { try { options.onLifecycleError?.({ phase, pluginId, error }); } catch { // Observer error reporting must never interrupt a commit or cleanup. } } }); const reportCleanupErrors = ( phase: "retire" | "rollback", failures: ReadonlyArray<CleanupFailure>, ): Effect.Effect<void> => Effect.sync(() => { for (const { error } of failures) { try { options.onCleanupError?.({ phase, error }); } catch { // Cleanup reporting must not replace activation errors or undo a committed snapshot. } } }); const closePlugins = ( plugins: ReadonlyArray<LivePlugin>, notifyDeactivation: boolean, ): Effect.Effect<ReadonlyArray<CleanupFailure>> => Effect.gen(function* () { const failures: Array<CleanupFailure> = []; for (const plugin of plugins.toReversed()) { const closeExit = yield* Effect.exit(Scope.close(plugin.scope, Exit.void)); if (Exit.isFailure(closeExit)) { failures.push({ error: Cause.squash(closeExit.cause), pluginId: plugin.definition.id, }); } for (const error of plugin.cleanupErrors.splice(0)) { failures.push({ error, pluginId: plugin.definition.id }); } if (notifyDeactivation) { yield* reportLifecycle("deactivate", plugin.definition.id); } } return failures; }); const invokePluginCallback = <Result>( callback: PluginLifecycleCallback, pluginId: string, invoke: () => Result | PromiseLike<Result>, onSettled?: () => void, ): Effect.Effect<Result, PluginCallbackError> => { const callbackState: PluginCallbackContext = { active: true, callback, pluginId }; let expired = false; const expire = () => { if (expired) return; expired = true; onSettled?.(); }; const settleCallback = () => { callbackState.active = false; expire(); }; return Effect.tryPromise({ try: async () => { try { const result = callbackContext.run(callbackState, invoke); if (typeof result === "object" && result !== null && "then" in result) { return await Promise.resolve(result).finally(settleCallback); } settleCallback(); return result; } catch (error) { settleCallback(); throw error; } }, catch: (cause) => new PluginCallbackError({ callback, cause, pluginId }), }).pipe(Effect.ensuring(Effect.sync(expire))); }; const activatePlugin = ( definition: PluginDefinition, capabilities: ReadonlyMap<string, unknown>, ): Effect.Effect<LivePlugin, PluginCallbackError> => Effect.uninterruptibleMask((restore) => Effect.gen(function* () { const scope = yield* Scope.fork(parentScope, "sequential"); const contributions = new Map<string, Array<LiveContribution>>(); const cleanupErrors: Array<unknown> = []; const finalizers: Array<() => void | Promise<void>> = []; const plugin: LivePlugin = { definition, scope, contributions, cleanupErrors }; let activating = true; const assertActivating = (method: "resolve" | "register" | "onDispose") => { if (!activating) { throw new PluginActivationContextExpiredError({ method, pluginId: definition.id }); } }; const context: PluginActivationContext = { resolve: <Service>(capability: string): Service => { assertActivating("resolve"); if (!(definition.requires ?? []).includes(capability)) { throw new PluginUndeclaredCapabilityError({ capability, pluginId: definition.id }); }