import { describe, expect, it, vi } from "vitest"; import { CACHE_INVALIDATION_PROTOCOL_VERSION, CACHE_INVALIDATION_WIRE_LIMITS, decodeCacheInvalidationWireEvent, parseCacheInvalidationWireEvent, type CacheInvalidationWireEvent, } from "../../src/contracts/cache-invalidation.ts"; import { createBrowserCrossContextInvalidation, createBrowserCrossContextInvalidationFromHost, type BroadcastChannelFacade, type BroadcastMessageListener, type BrowserCrossContextInvalidationDependencies, type CrossContextInvalidationDelivery, type CrossContextInvalidationObservation, type StorageEventTargetFacade, type StoragePulseFacade, type StoragePulseListener, } from "../../src/adapters/cross-context-invalidation/index.ts"; const NOW = 1_000_000; const CACHE_EPOCH = "cache-epoch-0001"; const TOPIC = "sample-topic-alpha"; const CHANNEL_NAME = "cache-invalidation-v1"; const STORAGE_PULSE_KEY = "ca-frontend:cache-invalidation:v1:pulse"; class FakeBroadcastNetwork { readonly channels: FakeBroadcastChannel[] = []; readonly messages: unknown[] = []; createChannel = (name: string): FakeBroadcastChannel => { const channel = new FakeBroadcastChannel(name, this); this.channels.push(channel); return channel; }; emit(value: unknown): void { this.messages.push(value); for (const channel of this.channels) channel.emit(value); } } class FakeBroadcastChannel implements BroadcastChannelFacade { readonly listeners = new Set(); closed = false; failPost = false; closeCount = 0; removeCount = 0; constructor( readonly name: string, private readonly network: FakeBroadcastNetwork, ) {} postMessage(value: unknown): void { if (this.closed || this.failPost) { throw new DOMException("Broadcast failed", "InvalidStateError"); } // Deliberately includes the sender. Native BroadcastChannel can still // deliver through another same-context channel, so the adapter must use // source identity rather than relying on provider echo behavior. this.network.emit(value); } addEventListener( _type: "message", listener: BroadcastMessageListener, ): void { this.listeners.add(listener); } removeEventListener( _type: "message", listener: BroadcastMessageListener, ): void { this.removeCount += 1; this.listeners.delete(listener); } close(): void { this.closeCount += 1; this.closed = true; this.listeners.clear(); } emit(value: unknown): void { if (this.closed) return; for (const listener of [...this.listeners]) { listener({ data: value }); } } } class FakeStorageEventTarget implements StorageEventTargetFacade { readonly listeners = new Set(); removeCount = 0; addEventListener( _type: "storage", listener: StoragePulseListener, ): void { this.listeners.add(listener); } removeEventListener( _type: "storage", listener: StoragePulseListener, ): void { this.removeCount += 1; this.listeners.delete(listener); } emit(key: string | null, newValue: string | null): void { for (const listener of [...this.listeners]) { listener({ key, newValue }); } } } class FakeStorageBus { readonly values = new Map(); readonly targets = new Set(); createEndpoint(): Readonly<{ storage: FakeStorageEndpoint; target: FakeStorageEventTarget; }> { const target = new FakeStorageEventTarget(); this.targets.add(target); return Object.freeze({ storage: new FakeStorageEndpoint(this, target), target, }); } set( owner: FakeStorageEventTarget, key: string, value: string, ): void { this.values.set(key, value); for (const target of this.targets) { if (target !== owner) target.emit(key, value); } } remove(owner: FakeStorageEventTarget, key: string): void { this.values.delete(key); for (const target of this.targets) { if (target !== owner) target.emit(key, null); } } } class FakeStorageEndpoint implements StoragePulseFacade { failSet = false; failRemove = false; setCount = 0; removeCount = 0; constructor( private readonly bus: FakeStorageBus, private readonly owner: FakeStorageEventTarget, ) {} setItem(key: string, value: string): void { this.setCount += 1; if (this.failSet) { throw new DOMException("Storage denied", "SecurityError"); } this.bus.set(this.owner, key, value); } removeItem(key: string): void { this.removeCount += 1; if (this.failRemove) { throw new DOMException("Storage denied", "SecurityError"); } this.bus.remove(this.owner, key); } } function dependencies( id: string, overrides: Partial = {}, ): BrowserCrossContextInvalidationDependencies { let eventNumber = 0; return { channelName: CHANNEL_NAME, storagePulseKey: STORAGE_PULSE_KEY, sourceId: `${id}-source`, sourceEpoch: `${id}-epoch`, cacheEpoch: CACHE_EPOCH, topics: [{ topic: TOPIC, topicVersion: 1 }], createEventId: () => `${id}-event-${String(++eventNumber).padStart(8, "0")}`, nowEpochMilliseconds: () => NOW, ...overrides, }; } function wireEvent( overrides: Partial = {}, ): CacheInvalidationWireEvent { return { protocolVersion: CACHE_INVALIDATION_PROTOCOL_VERSION, eventId: "remote-event-00000001", sourceId: "remote-source-0000001", sourceEpoch: "remote-epoch-0000001", sequence: 1, cacheEpoch: CACHE_EPOCH, topic: TOPIC, topicVersion: 1, emittedAt: NOW, expiresAt: NOW + 60_000, ...overrides, }; } describe("cache invalidation wire contract", () => { const policy = { cacheEpoch: CACHE_EPOCH, topicVersions: { [TOPIC]: 1 }, nowEpochMilliseconds: NOW, } as const; it("accepts only the exact payload-free, query-key-free envelope", () => { const accepted = parseCacheInvalidationWireEvent(wireEvent(), policy); expect(accepted).toMatchObject({ ok: true }); if (!accepted.ok) throw new Error("Expected a valid event"); expect(Object.keys(accepted.value).sort()).toEqual([ "cacheEpoch", "emittedAt", "eventId", "expiresAt", "protocolVersion", "sequence", "sourceEpoch", "sourceId", "topic", "topicVersion", ]); expect(accepted.value).not.toHaveProperty("payload"); expect(accepted.value).not.toHaveProperty("queryKey"); expect( parseCacheInvalidationWireEvent( { ...wireEvent(), payload: { secret: "must-not-cross" } }, policy, ), ).toEqual({ ok: false, reason: "INVALID_ENVELOPE" }); expect( parseCacheInvalidationWireEvent( { ...wireEvent(), queryKey: ["resource", "sensitive-id"] }, policy, ), ).toEqual({ ok: false, reason: "INVALID_ENVELOPE" }); }); it("bounds bytes, protocol, allowlist, cache epoch and event lifetime", () => { expect( decodeCacheInvalidationWireEvent("{not-json", policy), ).toEqual({ ok: false, reason: "MALFORMED_JSON" }); expect( decodeCacheInvalidationWireEvent( "x".repeat( CACHE_INVALIDATION_WIRE_LIMITS.maxWireBytes + 1, ), policy, ), ).toEqual({ ok: false, reason: "OVERSIZED" }); expect( parseCacheInvalidationWireEvent( { ...wireEvent(), protocolVersion: 2 }, policy, ), ).toEqual({ ok: false, reason: "PROTOCOL_MISMATCH" }); expect( parseCacheInvalidationWireEvent( { ...wireEvent(), topic: "unknown-topic" }, policy, ), ).toEqual({ ok: false, reason: "TOPIC_REJECTED" }); expect( parseCacheInvalidationWireEvent( { ...wireEvent(), topicVersion: 2 }, policy, ), ).toEqual({ ok: false, reason: "TOPIC_REJECTED" }); expect( parseCacheInvalidationWireEvent( { ...wireEvent(), cacheEpoch: "other-cache-epoch" }, policy, ), ).toEqual({ ok: false, reason: "CACHE_EPOCH_MISMATCH", }); expect( parseCacheInvalidationWireEvent( wireEvent({ emittedAt: NOW - 60_000, expiresAt: NOW }), policy, ), ).toEqual({ ok: false, reason: "EXPIRED" }); }); }); describe("browser cross-context host", () => { it("does not inspect browser capabilities when no topic is installed", () => { const reads: string[] = []; const host = new Proxy>( {}, { get(_target, property) { reads.push(String(property)); throw new DOMException("Capability access denied", "SecurityError"); }, }, ); expect( createBrowserCrossContextInvalidationFromHost({ host, cacheEpoch: CACHE_EPOCH, topics: [], }), ).toBeUndefined(); expect(reads).toEqual([]); }); }); describe("browser cross-context invalidation transport", () => { it("publishes one exact BroadcastChannel event and filters self echo", () => { const network = new FakeBroadcastNetwork(); const first = createBrowserCrossContextInvalidation( dependencies("tab-a", { createBroadcastChannel: network.createChannel, }), ); const second = createBrowserCrossContextInvalidation( dependencies("tab-b", { createBroadcastChannel: network.createChannel, }), ); const local = vi.fn(); const remote = vi.fn(); first.subscribe(local); second.subscribe(remote); expect(first.getStatus()).toBe("ACTIVE_BROADCAST"); expect(first.publish({ topic: TOPIC, topicVersion: 1 })).toEqual({ ok: true, transport: "BROADCAST", }); expect(local).not.toHaveBeenCalled(); expect(remote).toHaveBeenCalledOnce(); expect(remote.mock.calls[0]?.[0]).toMatchObject({ ordering: "NEXT", transport: "BROADCAST", event: { protocolVersion: 1, sequence: 1, cacheEpoch: CACHE_EPOCH, topic: TOPIC, topicVersion: 1, }, }); expect(network.messages).toHaveLength(1); expect(JSON.stringify(network.messages[0])).not.toMatch( /payload|queryKey|sensitive/, ); first.close(); second.close(); }); it("de-duplicates the same event delivered by both transports", () => { const network = new FakeBroadcastNetwork(); const storage = new FakeStorageBus().createEndpoint(); const runtime = createBrowserCrossContextInvalidation( dependencies("tab-b", { createBroadcastChannel: network.createChannel, storage: storage.storage, storageEvents: storage.target, }), ); const received = vi.fn(); runtime.subscribe(received); const event = wireEvent(); network.emit(event); storage.target.emit(STORAGE_PULSE_KEY, JSON.stringify(event)); expect(received).toHaveBeenCalledOnce(); expect(received.mock.calls[0]?.[0]).toMatchObject({ transport: "BROADCAST", ordering: "NEXT", }); runtime.close(); }); it("reports per-source gaps and drops stale out-of-order events", () => { const network = new FakeBroadcastNetwork(); const observations: CrossContextInvalidationObservation[] = []; const runtime = createBrowserCrossContextInvalidation( dependencies("tab-b", { createBroadcastChannel: network.createChannel, observe: (observation) => observations.push(observation), }), ); const delivered: CrossContextInvalidationDelivery[] = []; runtime.subscribe((delivery) => delivered.push(delivery)); network.emit(wireEvent({ eventId: "remote-event-00000001", sequence: 1 })); network.emit(wireEvent({ eventId: "remote-event-00000003", sequence: 3 })); network.emit(wireEvent({ eventId: "remote-event-00000002", sequence: 2 })); network.emit( wireEvent({ eventId: "remote-new-epoch-event", sourceEpoch: "remote-epoch-0000002", sequence: 1, }), ); expect( delivered.map(({ event, ordering }) => [ event.sequence, ordering, ]), ).toEqual([ [1, "NEXT"], [3, "GAP"], [1, "NEXT"], ]); expect(observations).toContainEqual({ operation: "RECEIVE", outcome: "DROPPED", transport: "BROADCAST", reason: "STALE", }); runtime.close(); }); it("falls back when BroadcastChannel open or publish fails", () => { const storageBus = new FakeStorageBus(); const firstStorage = storageBus.createEndpoint(); const secondStorage = storageBus.createEndpoint(); const firstNetwork = new FakeBroadcastNetwork(); const secondNetwork = new FakeBroadcastNetwork(); const first = createBrowserCrossContextInvalidation( dependencies("tab-a", { createBroadcastChannel: firstNetwork.createChannel, storage: firstStorage.storage, storageEvents: firstStorage.target, }), ); const second = createBrowserCrossContextInvalidation( dependencies("tab-b", { createBroadcastChannel: () => { throw new DOMException("Denied", "SecurityError"); }, storage: secondStorage.storage, storageEvents: secondStorage.target, }), ); firstNetwork.channels[0]!.failPost = true; const received = vi.fn(); second.subscribe(received); expect(second.getStatus()).toBe("ACTIVE_STORAGE_FALLBACK"); expect(first.publish({ topic: TOPIC, topicVersion: 1 })).toEqual({ ok: true, transport: "STORAGE", }); expect(first.getStatus()).toBe("ACTIVE_STORAGE_FALLBACK"); expect(received).toHaveBeenCalledOnce(); expect(firstStorage.storage.setCount).toBe(1); expect(firstStorage.storage.removeCount).toBe(1); expect(storageBus.values.has(STORAGE_PULSE_KEY)).toBe(false); expect(secondNetwork.channels).toHaveLength(0); first.close(); second.close(); }); it("enters explicit local-only degradation when every transport fails", () => { const storageBus = new FakeStorageBus(); const endpoint = storageBus.createEndpoint(); endpoint.storage.failSet = true; const observations: CrossContextInvalidationObservation[] = []; const runtime = createBrowserCrossContextInvalidation( dependencies("tab-a", { createBroadcastChannel: () => { throw new DOMException("Denied", "SecurityError"); }, storage: endpoint.storage, storageEvents: endpoint.target, observe: (observation) => observations.push(observation), }), ); expect(runtime.getStatus()).toBe("ACTIVE_STORAGE_FALLBACK"); expect(runtime.publish({ topic: TOPIC, topicVersion: 1 })).toEqual({ ok: false, reason: "TRANSPORT_UNAVAILABLE", }); expect(runtime.getStatus()).toBe("DEGRADED_LOCAL_ONLY"); expect(observations).toContainEqual({ operation: "PUBLISH", outcome: "DEGRADED", transport: "STORAGE", reason: "STORAGE_PUBLISH_FAILED", }); runtime.close(); }); it("isolates handler and diagnostics failures without leaking identifiers", () => { const network = new FakeBroadcastNetwork(); const observations: CrossContextInvalidationObservation[] = []; const runtime = createBrowserCrossContextInvalidation( dependencies("tab-b", { createBroadcastChannel: network.createChannel, observe(observation) { observations.push(observation); if (observation.reason === "HANDLER_FAILED") { throw new Error("diagnostics unavailable"); } }, }), ); const healthy = vi.fn(); runtime.subscribe(() => { throw new Error("listener secret"); }); runtime.subscribe(healthy); network.emit( wireEvent({ eventId: "sensitive-event-identifier", topic: TOPIC, }), ); expect(healthy).toHaveBeenCalledOnce(); expect(observations.some(({ reason }) => reason === "HANDLER_FAILED")).toBe( true, ); expect(JSON.stringify(observations)).not.toMatch( /sensitive-event-identifier|sample-topic-alpha|cache-epoch-0001/, ); runtime.close(); }); it("bounds TTL and LRU tracking instead of growing with remote sources", () => { const network = new FakeBroadcastNetwork(); let currentTime = NOW; const runtime = createBrowserCrossContextInvalidation( dependencies("tab-b", { createBroadcastChannel: network.createChannel, nowEpochMilliseconds: () => currentTime, dedupeCapacity: 2, sourceCapacity: 2, }), ); const received = vi.fn(); runtime.subscribe(received); const distinctSource = ( index: number, eventId: string, emittedAt = currentTime, ) => wireEvent({ eventId, sourceId: `remote-source-${index}`, sourceEpoch: `remote-epoch-${index}`, emittedAt, expiresAt: emittedAt + 1_000, }); network.emit(distinctSource(1, "bounded-event-1")); network.emit(distinctSource(2, "bounded-event-2")); network.emit(distinctSource(3, "bounded-event-3")); network.emit(distinctSource(1, "bounded-event-1")); expect(received).toHaveBeenCalledTimes(4); currentTime += 1_001; network.emit( distinctSource(3, "bounded-event-3", currentTime), ); expect(received).toHaveBeenCalledTimes(5); runtime.close(); }); it("cleans up listeners once and rejects queued work after close", () => { const network = new FakeBroadcastNetwork(); const storage = new FakeStorageBus().createEndpoint(); const runtime = createBrowserCrossContextInvalidation( dependencies("tab-b", { createBroadcastChannel: network.createChannel, storage: storage.storage, storageEvents: storage.target, }), ); const received = vi.fn(); const unsubscribe = runtime.subscribe(received); unsubscribe(); unsubscribe(); runtime.subscribe(received); runtime.close(); runtime.close(); network.emit(wireEvent()); storage.target.emit( STORAGE_PULSE_KEY, JSON.stringify(wireEvent()), ); expect(runtime.getStatus()).toBe("CLOSED"); expect(runtime.publish({ topic: TOPIC, topicVersion: 1 })).toEqual({ ok: false, reason: "CLOSED", }); expect(received).not.toHaveBeenCalled(); expect(network.channels[0]?.closeCount).toBe(1); expect(network.channels[0]?.removeCount).toBe(1); expect(storage.target.removeCount).toBe(1); expect(storage.target.listeners.size).toBe(0); }); it("fails invalid publish inputs closed without touching a provider", () => { const network = new FakeBroadcastNetwork(); const runtime = createBrowserCrossContextInvalidation( dependencies("tab-a", { createBroadcastChannel: network.createChannel, }), ); expect( runtime.publish({ topic: "not-allowlisted", topicVersion: 1 }), ).toEqual({ ok: false, reason: "INVALID_EVENT" }); expect( runtime.publish({ topic: TOPIC, topicVersion: 2 }), ).toEqual({ ok: false, reason: "INVALID_EVENT" }); expect(network.messages).toHaveLength(0); runtime.close(); }); });