import { it } from "@effect/vitest"; import { describe, expect } from "vite-plus/test"; 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 Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Scope from "effect/Scope"; import type { PluginDefinition, PluginRuntimeOptions, PluginRuntimeSnapshot, } from "../src/contract.ts"; import * as PluginRuntime from "../src/runtime.ts"; import { defineRuntimeContract } from "./runtimeContract.ts"; const makeTestRuntime = (options: PluginRuntimeOptions = {}) => PluginRuntime.make(options); const makeEffectCallback = () => { let unsafeResume: (effect: Effect.Effect) => void = () => { throw new Error("effect callback has not started"); }; return { await: Effect.callback((resume) => { unsafeResume = resume; }), resume: (effect: Effect.Effect) => unsafeResume(effect), } as const; }; defineRuntimeContract("plugin runtime", makeTestRuntime); describe("plugin runtime errors", () => { it.effect("returns schema-tagged planning errors", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* makeTestRuntime(); const duplicate = { id: "acme.duplicate", version: "1.0.0", activate() {}, }; const error = yield* Effect.flip(runtime.reconcile([duplicate, duplicate])); expect(error).toMatchObject({ _tag: "DuplicatePluginIdError", pluginId: "acme.duplicate", }); yield* runtime.dispose; }), ), ); }); describe("plugin runtime planner", () => { it.effect("plans a deep acyclic dependency chain without using the call stack", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* makeTestRuntime(); const pluginCount = 20_000; const definitions: Array = Array.from( { length: pluginCount }, (_, index) => ({ id: `plugin-${index}`, version: "1.0.0", ...(index === 0 ? {} : { requires: [`capability-${index - 1}`] }), provides: { [`capability-${index}`]: index }, activate() {}, }), ); const snapshot = yield* runtime.reconcile(definitions.toReversed()); expect(snapshot.active).toHaveLength(pluginCount); yield* runtime.dispose; }), ), ); }); describe("plugin runtime layer", () => { it.effect("closes active plugin scopes when the layer is released", () => Effect.gen(function* () { let disposed = false; const lifecycle: Array = []; yield* PluginRuntime.PluginRuntime.use((runtime) => runtime.reconcile([ { id: "acme.layer-owned", version: "1.0.0", activate(context) { context.onDispose(() => { disposed = true; }); }, }, ]), ).pipe( Effect.provide( PluginRuntime.layer({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }), ), ); expect(disposed).toBe(true); expect(lifecycle).toEqual(["activate:acme.layer-owned", "deactivate:acme.layer-owned"]); }), ); it.effect("serializes layer release with an in-flight reconcile", () => Effect.gen(function* () { let markActivationStarted!: () => void; const activationStarted = new Promise((resolve) => { markActivationStarted = resolve; }); let releaseActivation!: () => void; const activationGate = new Promise((resolve) => { releaseActivation = resolve; }); const lifecycle: Array = []; const runtimeScope = yield* Scope.make("sequential"); const services = yield* Layer.buildWithScope( PluginRuntime.layer({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }), runtimeScope, ); const runtime = Context.get(services, PluginRuntime.PluginRuntime); const reconcileFiber = yield* Effect.forkChild( runtime.reconcile([ { id: "acme.layer-release-race", version: "1.0.0", async activate() { markActivationStarted(); await activationGate; }, }, ]), ); yield* Effect.callback((resume) => { void activationStarted.then(() => resume(Effect.void)); }); const layerRelease = yield* Effect.forkChild(Scope.close(runtimeScope, Exit.void)); yield* Effect.yieldNow; expect(layerRelease.pollUnsafe()).toBeUndefined(); releaseActivation(); yield* Fiber.join(reconcileFiber); yield* Fiber.join(layerRelease); expect(lifecycle).toEqual([ "activate:acme.layer-release-race", "deactivate:acme.layer-release-race", ]); }), ); }); describe("plugin runtime disposal", () => { it.effect("retries scope cleanup after an interrupted dispose", () => Effect.scoped( Effect.gen(function* () { let markStarted!: () => void; let releaseCleanup!: () => void; const started = new Promise((resolve) => { markStarted = resolve; }); const cleanupGate = new Promise((resolve) => { releaseCleanup = resolve; }); const runtime = yield* makeTestRuntime(); yield* runtime.reconcile([ { id: "acme.interrupted-dispose", version: "1.0.0", activate(context) { context.onDispose(async () => { markStarted(); await cleanupGate; }); }, }, ]); const firstDispose = yield* Effect.forkChild(runtime.dispose); yield* Effect.callback((resume) => { void started.then(() => resume(Effect.void)); }); const interruption = yield* Effect.forkChild(Fiber.interrupt(firstDispose)); yield* Effect.yieldNow; const retry = yield* Effect.forkChild(runtime.dispose); yield* Effect.yieldNow; expect(retry.pollUnsafe()).toBeUndefined(); releaseCleanup(); yield* Fiber.join(interruption); yield* Fiber.join(retry); expect(yield* runtime.snapshot).toEqual({ active: [], blocked: {}, contributions: {}, }); }), ), ); }); describe("plugin runtime interruption", () => { it.effect("expires activation context when reconcile is interrupted", () => Effect.scoped( Effect.gen(function* () { let markActivationStarted!: () => void; const activationStarted = new Promise((resolve) => { markActivationStarted = resolve; }); let interruptCompleted = false; let lateRegistrationSucceeded = false; let lateFailure: unknown; let earlyFinalizerRan = false; let lateFinalizerRan = false; let releaseActivation!: () => void; const activationGate = new Promise((resolve) => { releaseActivation = resolve; }); const runtime = yield* makeTestRuntime(); const nested = makeEffectCallback, never>(); const nestedFiber = yield* Effect.forkChild(nested.await); yield* Effect.yieldNow; const reconcileFiber = yield* Effect.forkChild( runtime.reconcile([ { id: "acme.interrupted-activation", version: "1.0.0", async activate(context) { context.onDispose(() => { earlyFinalizerRan = true; }); markActivationStarted(); await activationGate; try { context.onDispose(() => { lateFinalizerRan = true; }); lateRegistrationSucceeded = true; } catch (error) { lateFailure = error; } nested.resume(Effect.exit(runtime.reconcile([]))); }, }, ]), ); yield* Effect.callback((resume) => { void activationStarted.then(() => resume(Effect.void)); }); const interruption = yield* Effect.forkChild( Fiber.interrupt(reconcileFiber).pipe( Effect.ensuring(Effect.sync(() => (interruptCompleted = true))), ), ); for (let attempt = 0; attempt < 10; attempt += 1) { yield* Effect.yieldNow; } expect(interruptCompleted).toBe(true); releaseActivation(); yield* Fiber.join(interruption); yield* Effect.callback((resume) => { queueMicrotask(() => resume(Effect.void)); }); const nestedExit = yield* Fiber.join(nestedFiber); yield* runtime.dispose; expect(lateRegistrationSucceeded).toBe(false); expect(lateFailure).toBeInstanceOf(Error); expect(earlyFinalizerRan).toBe(true); expect(lateFinalizerRan).toBe(false); expect(Exit.isFailure(nestedExit)).toBe(true); if (Exit.isFailure(nestedExit)) { expect(Cause.squash(nestedExit.cause)).toMatchObject({ _tag: "PluginRuntimeReentrancyError", }); } }), ), ); });