Files
openchamber/packages/ui/src/sync/__tests__/event-pipeline-online.test.js
T

108 lines
3.5 KiB
JavaScript
Raw Normal View History

import { afterEach, describe, expect, it } from 'bun:test';
import { createEventPipeline } from '../event-pipeline';
const savedDocument = globalThis.document;
const savedWindow = globalThis.window;
const savedNavigator = globalThis.navigator;
afterEach(() => {
globalThis.document = savedDocument;
globalThis.window = savedWindow;
globalThis.navigator = savedNavigator;
});
// Multi-listener event-target stub. The simpler single-slot stub used in
// event-pipeline-resume.test.js would break here because waitForRetry and the
// top-level onOnline handler both register for `online`.
function createEventTarget(extras = {}) {
const listeners = new Map();
return {
...extras,
addEventListener(event, handler) {
const list = listeners.get(event);
if (list) list.add(handler);
else listeners.set(event, new Set([handler]));
},
removeEventListener(event, handler) {
listeners.get(event)?.delete(handler);
},
dispatch(event) {
const list = listeners.get(event);
if (!list) return;
for (const handler of Array.from(list)) {
handler();
}
},
};
}
describe('createEventPipeline — online event', () => {
it('cuts the inter-attempt wait short when `online` fires after disconnect', async () => {
globalThis.document = createEventTarget({ visibilityState: 'visible' });
globalThis.window = createEventTarget({
location: { href: 'http://127.0.0.1:3000/', origin: 'http://127.0.0.1:3000' },
});
globalThis.navigator = { onLine: false };
let sdkCallIndex = 0;
const sdk = {
global: {
event: async () => {
const idx = sdkCallIndex++;
if (idx === 0) {
// Force a real failure so the loop enters the offline backoff path
// (computeRetryDelay returns the long cap because navigator.onLine
// is false). Without our `online` interrupt this would wait the
// full hidden/offline cap of 60s and the test would time out.
throw new Error('simulated network error');
}
return {
stream: (async function* () {
yield {
payload: {
type: 'session.status',
properties: { sessionID: 's1', status: { type: 'idle' } },
},
};
await new Promise(() => {});
})(),
};
},
},
};
const startedAt = Date.now();
const elapsed = await new Promise((resolve) => {
let connects = 0;
const { cleanup } = createEventPipeline({
sdk,
transport: 'sse',
heartbeatTimeoutMs: 60_000,
reconnectDelayMs: 60_000,
onEvent: () => {},
onDisconnect: () => {
// We're now inside waitForRetry on the long offline cap.
// Flip the browser back online and fire the event; waitForRetry
// should resolve early and the next attempt should fire.
setTimeout(() => {
globalThis.navigator = { onLine: true };
globalThis.window.dispatch('online');
}, 30);
},
onReconnect: () => {
connects += 1;
if (connects === 1) {
cleanup();
resolve(Date.now() - startedAt);
}
},
});
});
// Two attempts: the failed one + the recovery one. If the `online`
// interrupt didn't fire, the test would have hung on the 60s offline cap.
expect(sdkCallIndex).toBe(2);
expect(elapsed).toBeLessThan(2_000);
});
});