680 lines · 22.5 KB · TypeScript
RawDownload
import * as NodeAsyncHooks from "node:async_hooks"; import * as Cause from "effect/Cause";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope"; import type {  Contribution,  PluginActivationContext,  PluginDefinition,  PluginRuntime,  PluginRuntimeFactory,  PluginRuntimeSnapshot,} from "./contract.ts"; interface PlannedComposition {  readonly blocked: Readonly<Record<string, string>>;  readonly definitions: ReadonlyArray<PluginDefinition>;} interface LivePlugin {  readonly definition: PluginDefinition;  readonly scope: Scope.Closeable;  readonly contributions: ReadonlyMap<string, ReadonlyArray<Contribution>>;  readonly cleanupErrors: Array<unknown>;} interface LiveComposition {  readonly plugins: ReadonlyArray<LivePlugin>;  readonly snapshot: PluginRuntimeSnapshot;} type RuntimeOperation = "reconcile" | "dispose";type PluginCallback = "activate" | "finalizer"; interface PluginCallbackContext {  active: boolean;  readonly callback: PluginCallback;  readonly pluginId: string;} interface CleanupFailure {  readonly error: unknown;  readonly pluginId: string;} class DuplicatePluginIdError extends Schema.TaggedErrorClass<DuplicatePluginIdError>()(  "DuplicatePluginIdError",  { pluginId: Schema.String },) {  override get message(): string {    return `Duplicate plugin id: ${this.pluginId}`;  }} class DuplicateCapabilityError extends Schema.TaggedErrorClass<DuplicateCapabilityError>()(  "DuplicateCapabilityError",  {    capability: Schema.String,    pluginId: Schema.String,    previousPluginId: Schema.String,  },) {  override get message(): string {    return `Duplicate capability ${this.capability} provided by ${this.previousPluginId} and ${this.pluginId}`;  }} class DependencyCycleError extends Schema.TaggedErrorClass<DependencyCycleError>()(  "DependencyCycleError",  { cycle: Schema.Array(Schema.String) },) {  override get message(): string {    return `Dependency cycle: ${this.cycle.join(" -> ")}`;  }} 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 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`;  }} class PluginRuntimeDisposedError extends Schema.TaggedErrorClass<PluginRuntimeDisposedError>()(  "PluginRuntimeDisposedError",  { operation: Schema.Literals(["reconcile", "dispose"]) },) {  override get message(): string {    return `Plugin runtime is disposed; cannot ${this.operation}`;  }} class PluginRuntimeReentrancyError extends Schema.TaggedErrorClass<PluginRuntimeReentrancyError>()(  "PluginRuntimeReentrancyError",  {    callback: Schema.Literals(["activate", "finalizer"]),    operation: Schema.Literals(["reconcile", "dispose"]),    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"})`;  }} const PluginPlanningError = Schema.Union([  DuplicatePluginIdError,  DuplicateCapabilityError,  DependencyCycleError,]);type PluginPlanningError = typeof PluginPlanningError.Type; const isPluginPlanningError = Schema.is(PluginPlanningError); type PluginReconcileError =  | PluginPlanningError  | PluginCallbackError  | PluginRuntimeDisposedError  | PluginStagingError; const createNullPrototypeRecord = <Value>(): Record<string, Value> =>  Object.create(null) as Record<string, Value>; const emptySnapshot = (): PluginRuntimeSnapshot =>  Object.freeze({    active: Object.freeze([]),    blocked: Object.freeze(createNullPrototypeRecord<string>()),    contributions: Object.freeze(createNullPrototypeRecord<ReadonlyArray<Contribution>>()),  }); const planComposition = (definitions: ReadonlyArray<PluginDefinition>): PlannedComposition => {  const definitionsById = new Map<string, PluginDefinition>();  const providersByCapability = new Map<string, PluginDefinition>();   for (const definition of definitions) {    if (definitionsById.has(definition.id)) {      throw new DuplicatePluginIdError({ pluginId: definition.id });    }    definitionsById.set(definition.id, definition);     for (const capability of Object.keys(definition.provides ?? {})) {      const previous = providersByCapability.get(capability);      if (previous !== undefined) {        throw new DuplicateCapabilityError({          capability,          pluginId: definition.id,          previousPluginId: previous.id,        });      }      providersByCapability.set(capability, definition);    }  }   const visitState = new Map<string, "visiting" | "visited">();  const stack: Array<string> = [];  const detectCycle = (definition: PluginDefinition): void => {    const state = visitState.get(definition.id);    if (state === "visited") return;    if (state === "visiting") {      const cycleStart = stack.indexOf(definition.id);      const cycle = [...stack.slice(cycleStart), definition.id];      throw new DependencyCycleError({ cycle });    }     visitState.set(definition.id, "visiting");    stack.push(definition.id);    for (const capability of definition.requires ?? []) {      const provider = providersByCapability.get(capability);      if (provider !== undefined) detectCycle(provider);    }    stack.pop();    visitState.set(definition.id, "visited");  };   for (const definition of definitions) detectCycle(definition);   const blocked = new Map<string, string | undefined>();  const blockedReason = (definition: PluginDefinition): string | undefined => {    if (blocked.has(definition.id)) return blocked.get(definition.id);     for (const capability of definition.requires ?? []) {      const provider = providersByCapability.get(capability);      if (provider === undefined) {        const reason = `Missing dependency: ${capability}`;        blocked.set(definition.id, reason);        return reason;      }       const providerReason = blockedReason(provider);      if (providerReason !== undefined) {        const reason = `Dependency ${capability} is blocked: ${providerReason}`;        blocked.set(definition.id, reason);        return reason;      }    }     blocked.set(definition.id, undefined);    return undefined;  };   for (const definition of definitions) blockedReason(definition);   const ordered: Array<PluginDefinition> = [];  const orderedIds = new Set<string>();  const addInDependencyOrder = (definition: PluginDefinition): void => {    if (orderedIds.has(definition.id) || blocked.get(definition.id) !== undefined) return;     for (const capability of definition.requires ?? []) {      const provider = providersByCapability.get(capability);      if (provider !== undefined) addInDependencyOrder(provider);    }     orderedIds.add(definition.id);    ordered.push(definition);  };   for (const definition of definitions) addInDependencyOrder(definition);   const blockedRecord = createNullPrototypeRecord<string>();  for (const definition of definitions) {    const reason = blocked.get(definition.id);    if (reason !== undefined) blockedRecord[definition.id] = reason;  }   return { blocked: blockedRecord, definitions: ordered };}; const sameStrings = (left: ReadonlyArray<string>, right: ReadonlyArray<string>) => {  if (left.length !== right.length) return false;  const sortedLeft = [...left].sort();  const sortedRight = [...right].sort();  return sortedLeft.every((value, index) => value === sortedRight[index]);}; const sameDefinition = (left: PluginDefinition, right: PluginDefinition): boolean => {  if (    left.id !== right.id ||    left.version !== right.version ||    !Object.is(left.activate, right.activate)  ) {    return false;  }  if (!sameStrings(left.requires ?? [], right.requires ?? [])) return false;   const leftProvides = left.provides ?? {};  const rightProvides = right.provides ?? {};  const leftCapabilities = Object.keys(leftProvides);  const rightCapabilities = Object.keys(rightProvides);  return (    leftCapabilities.length === rightCapabilities.length &&    leftCapabilities.every(      (capability) =>        Object.hasOwn(rightProvides, capability) &&        Object.is(leftProvides[capability], rightProvides[capability]),    )  );}; const dependentsByPlugin = (definitions: ReadonlyArray<PluginDefinition>) => {  const providers = new Map<string, string>();  const dependents = new Map<string, Set<string>>();  for (const definition of definitions) {    for (const capability of Object.keys(definition.provides ?? {})) {      providers.set(capability, definition.id);    }  }  for (const definition of definitions) {    for (const capability of definition.requires ?? []) {      const providerId = providers.get(capability);      if (providerId === undefined) continue;      const values = dependents.get(providerId) ?? new Set<string>();      values.add(definition.id);      dependents.set(providerId, values);    }  }  return dependents;}; const affectedPluginIds = (  current: ReadonlyArray<LivePlugin>,  desired: ReadonlyArray<PluginDefinition>,): ReadonlySet<string> => {  const currentById = new Map(current.map((plugin) => [plugin.definition.id, plugin]));  const desiredById = new Map(desired.map((definition) => [definition.id, definition]));  const affected = new Set<string>();   for (const plugin of current) {    const next = desiredById.get(plugin.definition.id);    if (next === undefined || !sameDefinition(plugin.definition, next)) {      affected.add(plugin.definition.id);    }  }  for (const definition of desired) {    const previous = currentById.get(definition.id);    if (previous === undefined || !sameDefinition(previous.definition, definition)) {      affected.add(definition.id);    }  }   const currentDependents = dependentsByPlugin(current.map((plugin) => plugin.definition));  const desiredDependents = dependentsByPlugin(desired);  const queue = [...affected];  for (let index = 0; index < queue.length; index += 1) {    const pluginId = queue[index];    if (pluginId === undefined) continue;    for (const dependent of [      ...(currentDependents.get(pluginId) ?? []),      ...(desiredDependents.get(pluginId) ?? []),    ]) {      if (affected.has(dependent)) continue;      affected.add(dependent);      queue.push(dependent);    }  }   return affected;}; 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) =>          Object.freeze({ id: registration.id, label: registration.label }),        ),      ]);    }  }  return Object.freeze({    active: Object.freeze(plugins.map((plugin) => plugin.definition.id)),    blocked: Object.freeze(Object.assign(createNullPrototypeRecord<string>(), blocked)),    contributions: Object.freeze(contributions),  });}; export const createEffectScopeRuntime: PluginRuntimeFactory = (options = {}): PluginRuntime => {  let current: LiveComposition = { plugins: [], snapshot: emptySnapshot() };  let disposed = false;  let transition: Promise<void> = Promise.resolve();  let runtimeScope: Scope.Closeable | undefined;  const callbackContext = new NodeAsyncHooks.AsyncLocalStorage<PluginCallbackContext>();   const getRuntimeScope = (): Effect.Effect<Scope.Closeable> =>    Effect.gen(function* () {      if (runtimeScope === undefined) runtimeScope = yield* Scope.make("sequential");      return runtimeScope;    });   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: PluginCallback,    pluginId: string,    invoke: () => Result | PromiseLike<Result>,    onSettled?: () => void,  ): Effect.Effect<Result, PluginCallbackError> =>    Effect.tryPromise({      try: async () => {        const callbackState: PluginCallbackContext = { active: true, callback, pluginId };        let settled = false;        const settle = () => {          if (settled) return;          settled = true;          callbackState.active = false;          onSettled?.();        };        try {          const result = callbackContext.run(callbackState, invoke);          if (typeof result === "object" && result !== null && "then" in result) {            return await Promise.resolve(result).finally(settle);          }          settle();          return result;        } catch (error) {          settle();          throw error;        }      },      catch: (cause) => new PluginCallbackError({ callback, cause, pluginId }),    });   const activatePlugin = (    definition: PluginDefinition,    capabilities: ReadonlyMap<string, unknown>,  ): Effect.Effect<LivePlugin, PluginCallbackError> =>    Effect.gen(function* () {      const parentScope = yield* getRuntimeScope();

Lines 1–500 of 680.