chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
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<string> {
|
||||
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<string, unknown> =
|
||||
currentMode === "BROADCAST"
|
||||
? (globalThis as unknown as Record<string, unknown>)
|
||||
: {
|
||||
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<BrowserRuntimeSnapshot> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user