104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
createDashboardRealtimeClient,
|
|
queryKeysForDashboardEvent,
|
|
type DashboardEventSource,
|
|
} from '../../apps/dashboard/src/features/operations/realtime-client.js';
|
|
|
|
class FakeEventSource implements DashboardEventSource {
|
|
readonly listeners = new Map<string, Array<(event: unknown) => void>>();
|
|
readonly close = vi.fn();
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
const listeners = this.listeners.get(type) ?? [];
|
|
listeners.push(listener);
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
|
|
emit(type: string, event: unknown = {}): void {
|
|
for (const listener of this.listeners.get(type) ?? []) listener(event);
|
|
}
|
|
}
|
|
|
|
describe('dashboard realtime client', () => {
|
|
it('tracks connect/reconnect state and parses typed events before delivery', () => {
|
|
const source = new FakeEventSource();
|
|
const states: string[] = [];
|
|
const events = vi.fn();
|
|
const invalid = vi.fn();
|
|
const client = createDashboardRealtimeClient({
|
|
sourceFactory: () => source,
|
|
onEvent: events,
|
|
onStateChange: (state) => states.push(`${state.status}:${state.isStale}`),
|
|
onInvalidEvent: invalid,
|
|
});
|
|
|
|
client.start();
|
|
source.emit('open');
|
|
source.emit('log.created', {
|
|
data: JSON.stringify({
|
|
id: 'event-1',
|
|
occurredAt: '2026-08-08T12:00:00.000Z',
|
|
type: 'log.created',
|
|
payload: { level: 'info', message: 'Connected' },
|
|
}),
|
|
});
|
|
source.emit('log.created', { data: '{bad json' });
|
|
source.emit('error');
|
|
|
|
expect(states).toEqual(['connecting:true', 'connected:false', 'reconnecting:true']);
|
|
expect(events).toHaveBeenCalledOnce();
|
|
expect(events).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-1', type: 'log.created' }));
|
|
expect(invalid).toHaveBeenCalledOnce();
|
|
expect(client.getSnapshot()).toMatchObject({ status: 'reconnecting', isStale: true, lastEventId: 'event-1' });
|
|
|
|
client.stop();
|
|
expect(source.close).toHaveBeenCalledOnce();
|
|
expect(client.getSnapshot()).toMatchObject({ status: 'closed', isStale: true });
|
|
});
|
|
|
|
it('requests an authoritative repair on initial connection and every reconnect', () => {
|
|
const source = new FakeEventSource();
|
|
const connected = vi.fn();
|
|
const client = createDashboardRealtimeClient({
|
|
sourceFactory: () => source,
|
|
onEvent: vi.fn(),
|
|
onConnected: connected,
|
|
});
|
|
|
|
client.start();
|
|
source.emit('open');
|
|
source.emit('error');
|
|
source.emit('open');
|
|
|
|
expect(connected).toHaveBeenNthCalledWith(1, { isReconnect: false });
|
|
expect(connected).toHaveBeenNthCalledWith(2, { isReconnect: true });
|
|
});
|
|
|
|
it('maps events to the queries whose authoritative snapshots need refresh', () => {
|
|
expect(
|
|
queryKeysForDashboardEvent({
|
|
id: '1',
|
|
occurredAt: '2026-08-08T12:00:00.000Z',
|
|
type: 'sync.progress',
|
|
payload: { syncId: 'sync-1', kind: 'media', completed: 2, total: 5 },
|
|
}),
|
|
).toEqual([
|
|
['operations', 'sync-status'],
|
|
['conversations'],
|
|
['media'],
|
|
]);
|
|
expect(
|
|
queryKeysForDashboardEvent({
|
|
id: '2',
|
|
occurredAt: '2026-08-08T12:00:00.000Z',
|
|
type: 'rate-limit.changed',
|
|
payload: { limited: true },
|
|
}),
|
|
).toEqual([
|
|
['dashboard', 'stats'],
|
|
['dashboard', 'timers'],
|
|
]);
|
|
});
|
|
});
|