import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { Identity } from "@commonfabric/identity"; import { defer, type Deferred } from "@commonfabric/utils/defer"; import { inActionExecution, runInActionExecution, runInFrameContext, } from "../src/builder/frame-context.ts"; import { getTopFrame, popFrame, pushFrame, pushRuntimeDefaultFrame, } from "../src/builder/pattern.ts"; import type { Cell, Frame, Pattern } from "../src/builder/types.ts"; import type { RuntimeProgram } from "../src/harness/types.ts"; import { resolveLink } from "../src/link-resolution.ts"; import { parseLink } from "../src/link-utils.ts"; import { Runtime } from "../src/runtime.ts"; import { StorageManager } from "../src/storage/cache.deno.ts"; import { createTrustedBuilder } from "./support/trusted-builder.ts"; const signerA = await Identity.fromPassphrase("frame context A"); const signerB = await Identity.fromPassphrase("frame context B"); const signerC = await Identity.fromPassphrase("frame context C"); const spaceA = signerA.did(); const spaceB = signerB.did(); // A pattern whose body mints an internal cell and binds it into its UI, so the // space its `$value` link names is the space module evaluation bound it to. const DRAFT_PROGRAM: RuntimeProgram = { main: "/main.tsx", files: [{ name: "/main.tsx", contents: [ "import { NAME, UI, pattern, Writable } from 'commonfabric';", "export default pattern(() => {", " const draft = new Writable('').for('draft');", " return {", " [NAME]: 'draft holder',", " [UI]: ,", " };", "});", ].join("\n"), }], }; // A compiled pattern whose handler reads the ambient clock after an `await`, // which the sandbox answers only under the handler's own frame. const ASYNC_CLOCK_PROGRAM: RuntimeProgram = { main: "/main.tsx", files: [{ name: "/main.tsx", contents: [ "import { handler, pattern, Writable } from 'commonfabric';", "const stamp = handler<{ go: boolean }, { at: Writable }>(", " async (_event, { at }) => {", " await Promise.resolve();", " at.set(Date.now());", " },", ");", "export default pattern<{ at: Writable }>(({ at }) => {", " return { at, stamp: stamp({ at }) };", "});", ].join("\n"), }], }; /** A handler run held open at an `await` until the test releases it. */ type HeldHandler = { /** Resolves once the handler body has started. */ started: Deferred; /** Resolving it lets the handler body continue past its `await`. */ gate: Deferred; /** The top frame the body read before its `await`. */ before?: Frame; /** The top frame the body read after its `await`. */ after?: Frame; }; /** * Helper for the interleaving tests, which runs a piece in `space` whose async * handler records the top frame on either side of an `await` on * `held.gate`, and sends it one event. */ async function sendToHeldHandler( runtime: Runtime, space: string, held: HeldHandler, ): Promise { const { commonfabric } = createTrustedBuilder(runtime); const { cell, handler, pattern } = commonfabric; const wait = handler<{ n: number }, { effects: Cell<{ n: number }> }>( true, { type: "object", properties: { effects: { type: "object", asCell: ["cell"] } }, }, async (_event, { effects }) => { held.before = getTopFrame(); held.started.resolve(); await held.gate.promise; held.after = getTopFrame(); effects.key("n").set(1); }, ); const root = pattern(() => { const effects = cell({ n: 0 }); return { effects, stream: wait({ effects }) }; }); const tx = runtime.edit(); const rootCell = runtime.getCell<{ effects: unknown; stream: unknown }>( space as `did:${string}:${string}`, `frame context piece ${space}`, undefined, tx, ); const result = runtime.run(tx, root, {}, rootCell); await tx.commit().settled; await runtime.idle(); const streamLink = resolveLink( runtime, runtime.readTx(), result.key("stream").getAsNormalizedFullLink(), ); runtime.scheduler.queueEvent(streamLink, { n: 1 }, true); await held.started.promise; } /** Returns a handler run not yet started. */ function heldHandler(): HeldHandler { return { started: defer(), gate: defer() }; } /** * Helper for the module-evaluation tests, which runs `pattern` in `space` and * returns the space its UI's `$value` link names. */ async function boundSpace( runtime: Runtime, space: string, pattern: Pattern, ): Promise { const result = runtime.getCell( space as `did:${string}:${string}`, "frame context result", ); await runtime.runSynced(result, pattern, {}); const rendered = result.getRaw() as { $UI: { props: { $value: unknown } } }; return parseLink(rendered.$UI.props.$value, result)?.space; } describe("frame-context", () => { describe("runInFrameContext()", () => { it("hides a frame pushed in the context from code outside it while the context awaits", async () => { const gate = defer(); const pushed = defer(); const run = runInFrameContext(async () => { const frame = pushFrame({ cause: "inside" }); pushed.resolve(frame); await gate.promise; popFrame(frame); }); const inside = await pushed.promise; try { expect(getTopFrame()).not.toBe(inside); } finally { gate.resolve(); await run; } }); it("keeps the context's own frame on top after an `await`, over a frame pushed elsewhere meanwhile", async () => { const gate = defer(); const pushed = defer(); const run = runInFrameContext(async () => { const frame = pushFrame({ cause: "inside" }); pushed.resolve(frame); await gate.promise; const top = getTopFrame(); popFrame(frame); return { frame, top }; }); await pushed.promise; const outside = pushFrame({ cause: "outside" }); try { gate.resolve(); const { frame, top } = await run; expect(top).toBe(frame); } finally { popFrame(outside); } }); it("returns the root stack's top while the context's own stack is empty", () => { const root = pushFrame({ cause: "root" }); try { expect(runInFrameContext(() => getTopFrame())).toBe(root); } finally { popFrame(root); } }); }); describe("pushRuntimeDefaultFrame()", () => { it("pushes a frame carrying only the runtime onto the root stack, from inside a context", () => { const runtime = {} as Runtime; const frame = runInFrameContext(() => { const action = pushFrame({ cause: "action", space: spaceA }); const pushed = pushRuntimeDefaultFrame(runtime); popFrame(action); return pushed; }); try { expect(frame.runtime).toBe(runtime); expect(frame.space).toBeUndefined(); expect(frame.tx).toBeUndefined(); expect(getTopFrame()).toBe(frame); } finally { popFrame(frame); } }); }); describe("runInActionExecution()", () => { it("marks only its own context as executing an action while it awaits", async () => { const gate = defer(); const started = defer(); const run = runInFrameContext(() => runInActionExecution(async () => { started.resolve(); await gate.promise; return inActionExecution(); }) ); await started.promise; try { expect(inActionExecution()).toBe(false); } finally { gate.resolve(); } expect(await run).toBe(true); }); }); describe("interleaving across runtimes", () => { let storageA: ReturnType; let storageB: ReturnType; let runtimeA: Runtime; let runtimeB: Runtime; beforeEach(() => { storageA = StorageManager.emulate({ as: signerA }); storageB = StorageManager.emulate({ as: signerB }); runtimeA = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: storageA, }); runtimeB = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: storageB, }); }); afterEach(async () => { await runtimeA.dispose(); await runtimeB.dispose(); await storageA.close(); await storageB.close(); }); it("binds a module evaluated while a handler of the same runtime awaits to the space it runs in", async () => { const held = heldHandler(); await sendToHeldHandler(runtimeA, spaceB, held); let pattern: Pattern; try { const { main } = await runtimeA.harness.compileAndEvaluateModules( DRAFT_PROGRAM, ); pattern = (main as { default: Pattern }).default; } finally { held.gate.resolve(); } await runtimeA.idle(); expect(await boundSpace(runtimeA, spaceA, pattern)).toBe(spaceA); }); it("keeps an async handler's own frame on top after it resumes while another runtime's handler awaits", async () => { const heldA = heldHandler(); const heldB = heldHandler(); await sendToHeldHandler(runtimeA, spaceA, heldA); try { await sendToHeldHandler(runtimeB, spaceB, heldB); heldA.gate.resolve(); await runtimeA.idle(); } finally { heldA.gate.resolve(); heldB.gate.resolve(); } await runtimeB.idle(); expect(heldA.after).toBe(heldA.before); expect(heldA.after?.space).toBe(spaceA); }); it("gives a runtime built while another runtime's handler awaits a default frame with no space or transaction", async () => { const held = heldHandler(); await sendToHeldHandler(runtimeA, spaceA, held); const storageC = StorageManager.emulate({ as: signerC }); let runtimeC: Runtime | undefined; try { runtimeC = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: storageC, }); } finally { held.gate.resolve(); } await runtimeA.idle(); const top = getTopFrame(); try { expect(top?.runtime).toBe(runtimeC); expect(top?.space).toBeUndefined(); expect(top?.tx).toBeUndefined(); } finally { await runtimeC.dispose(); await storageC.close(); } }); it("binds a module evaluated after that handler finished to the space it runs in", async () => { const held = heldHandler(); await sendToHeldHandler(runtimeA, spaceA, held); const storageC = StorageManager.emulate({ as: signerC }); let runtimeC: Runtime | undefined; try { runtimeC = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: storageC, }); } finally { held.gate.resolve(); } await runtimeA.idle(); try { const { main } = await runtimeB.harness.compileAndEvaluateModules( DRAFT_PROGRAM, ); const pattern = (main as { default: Pattern }).default; expect(await boundSpace(runtimeB, spaceB, pattern)).toBe(spaceB); } finally { await runtimeC.dispose(); await storageC.close(); } }); }); describe("a compiled async handler", () => { let storage: ReturnType; let runtime: Runtime; beforeEach(() => { storage = StorageManager.emulate({ as: signerA }); runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: storage, }); }); afterEach(async () => { await runtime.dispose(); await storage.close(); }); it("reads the ambient clock after an `await`, which needs its own handler frame", async () => { const { main } = await runtime.harness.compileAndEvaluateModules( ASYNC_CLOCK_PROGRAM, ); const pattern = (main as { default: Pattern }).default; const errors: unknown[] = []; runtime.scheduler.onError((error) => errors.push(error)); const result = runtime.getCell<{ at: number; stamp: unknown }>( spaceA, "frame context clock", ); await runtime.runSynced(result, pattern, { at: 0 }); const streamLink = resolveLink( runtime, runtime.readTx(), result.key("stamp").getAsNormalizedFullLink(), ); runtime.scheduler.queueEvent(streamLink, { go: true }, true); await runtime.idle(); expect(errors).toEqual([]); expect(result.key("at").get()).toBeGreaterThan(0); }); }); });