import { expect, test, type Page, } from "../support/browser/strict-browser-test.ts"; const TOPIC = "sample-topic-alpha"; const TOPIC_VERSION = 1; const STATE_KEY = "__crossContextInvalidationCapability"; const STORAGE_PULSE_KEY = "ca-frontend:cache-invalidation:v1:pulse"; type BrowserTransportMode = "BROADCAST" | "STORAGE"; type BrowserDeliverySnapshot = Readonly<{ ordering: string; sequence: number; topic: string; transport: string; }>; type BrowserRuntimeSnapshot = Readonly<{ status: string; deliveries: readonly BrowserDeliverySnapshot[]; pulseRetained: boolean; }>; async function installRuntime( page: Page, mode: BrowserTransportMode, cacheEpoch: string, ): Promise { return await page.evaluate( async ({ currentCacheEpoch, currentMode, stateKey, topic, topicVersion, }) => { const modulePath = "/src/adapters/cross-context-invalidation/index.ts"; const { createBrowserCrossContextInvalidationFromHost } = (await import( /* @vite-ignore */ modulePath )) as typeof import("../../src/adapters/cross-context-invalidation/index.ts"); const host: Record = currentMode === "BROADCAST" ? (globalThis as unknown as Record) : { crypto: globalThis.crypto, localStorage: globalThis.localStorage, addEventListener: globalThis.addEventListener.bind(globalThis), removeEventListener: globalThis.removeEventListener.bind(globalThis), }; const runtime = createBrowserCrossContextInvalidationFromHost({ host, cacheEpoch: currentCacheEpoch, topics: [{ topic, topicVersion }], }); if (!runtime) { throw new Error( "Cross-context invalidation runtime is unavailable.", ); } const deliveries: BrowserDeliverySnapshot[] = []; runtime.subscribe((delivery) => { deliveries.push({ ordering: delivery.ordering, sequence: delivery.event.sequence, topic: delivery.event.topic, transport: delivery.transport, }); }); Reflect.set(globalThis, stateKey, { deliveries, runtime }); return runtime.getStatus(); }, { currentCacheEpoch: cacheEpoch, currentMode: mode, stateKey: STATE_KEY, topic: TOPIC, topicVersion: TOPIC_VERSION, }, ); } async function publish(page: Page) { return await page.evaluate( ({ stateKey, topic, topicVersion }) => { const state = Reflect.get(globalThis, stateKey) as | { runtime: { publish(input: { topic: string; topicVersion: number; }): unknown; }; } | undefined; if (!state) throw new Error("Capability runtime is not installed."); return state.runtime.publish({ topic, topicVersion }); }, { stateKey: STATE_KEY, topic: TOPIC, topicVersion: TOPIC_VERSION, }, ); } async function snapshot(page: Page): Promise { return await page.evaluate( ({ pulseKey, stateKey }) => { const state = Reflect.get(globalThis, stateKey) as | { deliveries: BrowserDeliverySnapshot[]; runtime: { getStatus(): string }; } | undefined; if (!state) throw new Error("Capability runtime is not installed."); return { status: state.runtime.getStatus(), deliveries: structuredClone(state.deliveries), pulseRetained: localStorage.getItem(pulseKey) !== null, }; }, { pulseKey: STORAGE_PULSE_KEY, stateKey: STATE_KEY }, ); } async function closeRuntime(page: Page): Promise { return await page.evaluate((stateKey) => { const state = Reflect.get(globalThis, stateKey) as | { runtime: { close(): void; getStatus(): string } } | undefined; if (!state) return null; state.runtime.close(); return state.runtime.getStatus(); }, STATE_KEY); } async function disposeRuntime(page: Page): Promise { await page.evaluate( ({ pulseKey, stateKey }) => { const state = Reflect.get(globalThis, stateKey) as | { runtime: { close(): void } } | undefined; state?.runtime.close(); localStorage.removeItem(pulseKey); Reflect.deleteProperty(globalThis, stateKey); }, { pulseKey: STORAGE_PULSE_KEY, stateKey: STATE_KEY }, ); } test("delivers invalidation through native BroadcastChannel and cleans the receiver", async ({ page, }) => { const secondPage = await page.context().newPage(); const cacheEpoch = `browser.primary.${Date.now()}`; try { await Promise.all([page.goto("/"), secondPage.goto("/")]); await page.evaluate( (pulseKey) => localStorage.removeItem(pulseKey), STORAGE_PULSE_KEY, ); const statuses = await Promise.all([ installRuntime(page, "BROADCAST", cacheEpoch), installRuntime(secondPage, "BROADCAST", cacheEpoch), ]); expect(statuses).toEqual([ "ACTIVE_BROADCAST", "ACTIVE_BROADCAST", ]); expect(await publish(page)).toEqual({ ok: true, transport: "BROADCAST", }); await expect .poll(async () => (await snapshot(secondPage)).deliveries) .toEqual([ { ordering: "NEXT", sequence: 1, topic: TOPIC, transport: "BROADCAST", }, ]); expect((await snapshot(page)).deliveries).toEqual([]); expect(await closeRuntime(secondPage)).toBe("CLOSED"); expect(await publish(page)).toEqual({ ok: true, transport: "BROADCAST", }); await page.waitForTimeout(100); expect(await snapshot(secondPage)).toEqual({ status: "CLOSED", deliveries: [ { ordering: "NEXT", sequence: 1, topic: TOPIC, transport: "BROADCAST", }, ], pulseRetained: false, }); } finally { await Promise.allSettled([ disposeRuntime(page), disposeRuntime(secondPage), ]); await secondPage.close(); } }); test("falls back to native localStorage events and removes the pulse during cleanup", async ({ page, }) => { const secondPage = await page.context().newPage(); const cacheEpoch = `browser.fallback.${Date.now()}`; try { await Promise.all([page.goto("/"), secondPage.goto("/")]); await page.evaluate( (pulseKey) => localStorage.removeItem(pulseKey), STORAGE_PULSE_KEY, ); const statuses = await Promise.all([ installRuntime(page, "STORAGE", cacheEpoch), installRuntime(secondPage, "STORAGE", cacheEpoch), ]); expect(statuses).toEqual([ "ACTIVE_STORAGE_FALLBACK", "ACTIVE_STORAGE_FALLBACK", ]); expect(await publish(page)).toEqual({ ok: true, transport: "STORAGE", }); await expect .poll(async () => (await snapshot(secondPage)).deliveries) .toEqual([ { ordering: "NEXT", sequence: 1, topic: TOPIC, transport: "STORAGE", }, ]); expect(await snapshot(page)).toMatchObject({ deliveries: [], pulseRetained: false, }); expect(await snapshot(secondPage)).toMatchObject({ pulseRetained: false, }); expect(await closeRuntime(secondPage)).toBe("CLOSED"); expect(await publish(page)).toEqual({ ok: true, transport: "STORAGE", }); await page.waitForTimeout(100); expect(await snapshot(secondPage)).toEqual({ status: "CLOSED", deliveries: [ { ordering: "NEXT", sequence: 1, topic: TOPIC, transport: "STORAGE", }, ], pulseRetained: false, }); } finally { await Promise.allSettled([ disposeRuntime(page), disposeRuntime(secondPage), ]); await secondPage.close(); } });