UNPKG

opinionated-machine

Version:

Very opinionated DI framework for fastify, built on top of awilix

136 lines 5.91 kB
/** * Connection spy for testing SSE controllers. * Tracks connection and disconnection events separately. */ export class SSESessionSpy { events = []; activeConnections = new Set(); claimedConnections = new Set(); connectionWaiters = []; disconnectionWaiters = []; /** @internal Called when a connection is established */ addConnection(connection) { this.events.push({ type: 'connect', connectionId: connection.id, connection }); this.activeConnections.add(connection.id); // Find and resolve first matching connection waiter const waiterIndex = this.connectionWaiters.findIndex((w) => !w.predicate || w.predicate(connection)); if (waiterIndex !== -1) { // biome-ignore lint/style/noNonNullAssertion: we just received this index const waiter = this.connectionWaiters[waiterIndex]; this.connectionWaiters.splice(waiterIndex, 1); clearTimeout(waiter.timeoutId); this.claimedConnections.add(connection.id); waiter.resolve(connection); } } /** @internal Called when a connection is closed */ addDisconnection(connectionId) { this.events.push({ type: 'disconnect', connectionId }); this.activeConnections.delete(connectionId); // Resolve all pending disconnection waiters for this connection const matchingWaiters = this.disconnectionWaiters.filter((w) => w.connectionId === connectionId); for (const waiter of matchingWaiters) { clearTimeout(waiter.timeoutId); waiter.resolve(); } this.disconnectionWaiters = this.disconnectionWaiters.filter((w) => w.connectionId !== connectionId); } /** * Wait for a connection to be established. * * @param options.timeout - Timeout in milliseconds (default: 5000) * @param options.predicate - Optional predicate to match a specific connection. * When provided, waits for an unclaimed connection that matches the predicate. * Connections are "claimed" when returned by waitForConnection, allowing * multiple sequential waits for the same URL path. * * @example * ```typescript * // Wait for any connection * const conn = await spy.waitForConnection() * * // Wait for a connection with specific URL * const conn = await spy.waitForConnection({ * predicate: (c) => c.request.url.includes('/api/notifications'), * }) * ``` */ waitForConnection(options) { const timeout = options?.timeout ?? 5000; const predicate = options?.predicate; // Check if a matching unclaimed connection already exists (must still be active) const connectEvent = this.events.find((e) => e.type === 'connect' && e.connection && !this.claimedConnections.has(e.connection.id) && this.activeConnections.has(e.connection.id) && (!predicate || predicate(e.connection))); if (connectEvent?.connection) { this.claimedConnections.add(connectEvent.connection.id); return Promise.resolve(connectEvent.connection); } // No matching connection yet, create a waiter return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { const index = this.connectionWaiters.findIndex((w) => w.resolve === resolve); if (index !== -1) { this.connectionWaiters.splice(index, 1); } reject(new Error(`Timeout waiting for connection after ${timeout}ms`)); }, timeout); this.connectionWaiters.push({ resolve, reject, timeoutId, predicate }); }); } /** Wait for a specific connection to disconnect */ waitForDisconnection(connectionId, options) { const timeout = options?.timeout ?? 5000; // Check if already disconnected const hasDisconnected = this.events.some((e) => e.type === 'disconnect' && e.connectionId === connectionId); if (hasDisconnected) { return Promise.resolve(); } // Not disconnected yet, create a waiter return new Promise((resolve, reject) => { const waiter = { connectionId, resolve, reject, timeoutId: setTimeout(() => { const index = this.disconnectionWaiters.indexOf(waiter); if (index !== -1) { this.disconnectionWaiters.splice(index, 1); } reject(new Error(`Timeout waiting for disconnection after ${timeout}ms`)); }, timeout), }; this.disconnectionWaiters.push(waiter); }); } /** Check if a connection is currently active */ isConnected(connectionId) { return this.activeConnections.has(connectionId); } /** Get all connection events in order, optionally filtered by connectionId */ getEvents(connectionId) { if (connectionId === undefined) { return [...this.events]; } return this.events.filter((e) => e.connectionId === connectionId); } /** Clear all events and cancel pending waiters */ clear() { this.events = []; this.activeConnections.clear(); this.claimedConnections.clear(); for (const waiter of this.connectionWaiters) { clearTimeout(waiter.timeoutId); waiter.reject(new Error('SessionSpy was cleared')); } for (const waiter of this.disconnectionWaiters) { clearTimeout(waiter.timeoutId); waiter.reject(new Error('SessionSpy was cleared')); } this.connectionWaiters = []; this.disconnectionWaiters = []; } } //# sourceMappingURL=SSESessionSpy.js.map