import * as plugins from './typedsocket.plugins.js'; export type TTypedSocketSide = 'server' | 'client'; export type TConnectionStatus = 'new' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting'; const TAG_PREFIX = '__typedsocket_tag__'; /** * Internal TypedRequest interfaces for tag management */ interface IReq_SetClientTag extends plugins.typedrequestInterfaces.ITypedRequest { method: '__typedsocket_setTag'; request: { name: string; payload: any }; response: { success: boolean }; } interface IReq_RemoveClientTag extends plugins.typedrequestInterfaces.ITypedRequest { method: '__typedsocket_removeTag'; request: { name: string }; response: { success: boolean }; } /** * Options for creating a TypedSocket client */ export interface ITypedSocketClientOptions { autoReconnect?: boolean; maxRetries?: number; initialBackoffMs?: number; maxBackoffMs?: number; abortSignal?: AbortSignal; } /** * Lifecycle controls for one TypedRequest sent over a TypedSocket. */ export interface ITypedSocketRequestOptions { timeoutMs?: number; abortSignal?: AbortSignal; } interface IPendingServerRequest { peerId: string; cancel: (errorArg: Error) => void; cleanupComplete: Promise; } /** * Wrapper for SmartServe's IWebSocketPeer to provide tag compatibility */ export interface ISmartServeConnectionWrapper { peer: plugins.IWebSocketPeer; getTagById(tagId: string): Promise<{ id: string; payload: any } | undefined>; } /** * Creates a wrapper around IWebSocketPeer for tag compatibility */ function wrapSmartServePeer(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper { return { peer, async getTagById(tagId: string): Promise<{ id: string; payload: any } | undefined> { if (!peer.tags.has(tagId)) { return undefined; } const payload = peer.data.get(`${TAG_PREFIX}${tagId}`); return { id: tagId, payload }; }, }; } export class TypedSocket { // ============================================================================ // STATIC METHODS // ============================================================================ /** * Creates a TypedSocket client using native WebSocket. * Works in both browser and Node.js environments. * * @param typedrouterArg - TypedRouter for handling server-initiated requests * @param serverUrlArg - Server URL (e.g., 'http://localhost:3000' or 'wss://example.com') * @param options - Connection options * * @example * ```typescript * const typedRouter = new TypedRouter(); * const client = await TypedSocket.createClient( * typedRouter, * 'http://localhost:3000', * { autoReconnect: true } * ); * ``` */ public static async createClient( typedrouterArg: plugins.typedrequest.TypedRouter, serverUrlArg: string, options: ITypedSocketClientOptions = {} ): Promise { const defaultOptions: Omit, 'abortSignal'> = { autoReconnect: true, maxRetries: 100, initialBackoffMs: 1000, maxBackoffMs: 60000, }; const opts = { ...defaultOptions, ...options }; const typedSocket = new TypedSocket('client', typedrouterArg); typedSocket.clientOptions = opts; typedSocket.serverUrl = serverUrlArg; typedSocket.currentBackoff = opts.initialBackoffMs; if (opts.abortSignal?.aborted) { throw typedSocket.createAbortError(); } if (opts.abortSignal) { typedSocket.abortSignalListener = () => { void typedSocket.stop(); }; opts.abortSignal.addEventListener('abort', typedSocket.abortSignalListener, { once: true }); } try { await typedSocket.connect(); } catch (error) { await typedSocket.stop(); throw error; } return typedSocket; } /** * Returns the current window location origin URL. * Useful in browser environments for connecting to the same origin. */ public static useWindowLocationOriginUrl = (): string => { return plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString(); }; /** * Creates a TypedSocket server from an existing SmartServe instance. * This is the only way to create a server-side TypedSocket. * * @param smartServeArg - SmartServe instance with typedRouter configured in websocket options * @param typedRouterArg - TypedRouter for handling requests (must match SmartServe's typedRouter) * * @example * ```typescript * const typedRouter = new TypedRouter(); * const smartServe = new SmartServe({ * port: 3000, * websocket: { * typedRouter, * onConnectionOpen: (peer) => peer.tags.add('client') * } * }); * await smartServe.start(); * const typedSocket = TypedSocket.fromSmartServe(smartServe, typedRouter); * ``` */ public static fromSmartServe( smartServeArg: plugins.SmartServe, typedRouterArg: plugins.typedrequest.TypedRouter ): TypedSocket { // Register built-in tag handlers TypedSocket.registerTagHandlers(typedRouterArg); const typedSocket = new TypedSocket('server', typedRouterArg); typedSocket.smartServeRef = smartServeArg; typedSocket.unsubscribeSmartServeConnectionClose = smartServeArg.subscribeWebSocketConnectionClose((peerArg) => { typedSocket.cancelPendingServerRequestsForPeer( peerArg.id, new Error(`TypedSocket target connection closed: ${peerArg.id}`), ); }); return typedSocket; } /** * Registers built-in TypedHandlers for tag management */ private static registerTagHandlers(typedRouter: plugins.typedrequest.TypedRouter): void { // Set tag handler typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler('__typedsocket_setTag', async (data, meta) => { const peer = meta?.localData?.peer as plugins.IWebSocketPeer; if (!peer) { console.warn('setTag: No peer found in request context'); return { success: false }; } peer.tags.add(data.name); peer.data.set(`${TAG_PREFIX}${data.name}`, data.payload); return { success: true }; }) ); // Remove tag handler typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler('__typedsocket_removeTag', async (data, meta) => { const peer = meta?.localData?.peer as plugins.IWebSocketPeer; if (!peer) { console.warn('removeTag: No peer found in request context'); return { success: false }; } peer.tags.delete(data.name); peer.data.delete(`${TAG_PREFIX}${data.name}`); return { success: true }; }) ); } // ============================================================================ // INSTANCE PROPERTIES // ============================================================================ public readonly side: TTypedSocketSide; public readonly typedrouter: plugins.typedrequest.TypedRouter; // Connection status observable public statusSubject = new plugins.smartrx.rxjs.Subject(); private connectionStatus: TConnectionStatus = 'new'; // Client-specific properties private websocket: WebSocket | null = null; private clientOptions: (Omit, 'abortSignal'> & Pick) | null = null; private serverUrl: string = ''; private retryCount = 0; private currentBackoff = 1000; private stopped = false; private abortSignalListener?: () => void; private stopListeners = new Set<() => void>(); private pendingRequests = new Map void; reject: (error: Error) => void; }>(); // Tags set via setTag(), kept client-side so reconnects can restore them on // the fresh server-side connection (server tags die with the old socket). private clientTags = new Map(); private clientTagMutations = new Map(); // Server-specific properties (SmartServe mode) private smartServeRef: plugins.SmartServe | null = null; private serverStopping = false; private unsubscribeSmartServeConnectionClose: (() => void) | null = null; private pendingServerRequests = new Map(); private pendingServerRequestIdsByPeerId = new Map>(); // ============================================================================ // CONSTRUCTOR // ============================================================================ private constructor( sideArg: TTypedSocketSide, typedrouterArg: plugins.typedrequest.TypedRouter ) { this.side = sideArg; this.typedrouter = typedrouterArg; } // ============================================================================ // CLIENT METHODS // ============================================================================ /** * Connects the client to the server using native WebSocket */ private async connect(): Promise { if (this.stopped || this.clientOptions?.abortSignal?.aborted) { throw this.createAbortError(); } const done = plugins.smartpromise.defer(); let connectionSettled = false; let connectionTimeout: ReturnType | undefined; let abortConnection: (() => void) | undefined; const settleConnection = (errorArg?: Error) => { if (connectionSettled) { return; } connectionSettled = true; if (connectionTimeout) { clearTimeout(connectionTimeout); } if (abortConnection) { this.stopListeners.delete(abortConnection); this.clientOptions?.abortSignal?.removeEventListener('abort', abortConnection); } if (errorArg) { done.reject(errorArg); } else { done.resolve(); } }; this.updateStatus('connecting'); // Convert HTTP URL to WebSocket URL const wsUrl = this.toWebSocketUrl(this.serverUrl); this.logDebug(`TypedSocket connecting to ${wsUrl}...`); this.websocket = new WebSocket(wsUrl); abortConnection = () => { this.websocket?.close(); settleConnection(this.createAbortError()); }; this.stopListeners.add(abortConnection); this.clientOptions?.abortSignal?.addEventListener('abort', abortConnection, { once: true }); connectionTimeout = setTimeout(() => { if (this.connectionStatus !== 'connected') { this.logTransient(`TypedSocket connection timeout for ${wsUrl}`); this.websocket?.close(); settleConnection(new Error('Connection timeout')); } }, 10000); this.websocket.onopen = async () => { const openedWebsocket = this.websocket; try { await this.reapplyClientTags(); if (this.websocket !== openedWebsocket || openedWebsocket?.readyState !== WebSocket.OPEN) { throw new Error('TypedSocket connection changed while restoring client tags'); } this.logDebug('TypedSocket connected and client registration restored!'); this.retryCount = 0; this.currentBackoff = this.clientOptions?.initialBackoffMs ?? 1000; this.updateStatus('connected'); settleConnection(); } catch (error) { const restorationError = error instanceof Error ? error : new Error(String(error)); this.logTransient(`TypedSocket client registration restoration failed: ${restorationError.message}`); settleConnection(restorationError); openedWebsocket?.close(); } }; this.websocket.onmessage = async (event) => { await this.handleMessage(event.data); }; this.websocket.onclose = () => { if (this.connectionStatus === 'connected') { this.handleDisconnect(); } else { this.rejectPendingClientRequests(new Error('TypedSocket disconnected')); settleConnection(this.stopped || this.clientOptions?.abortSignal?.aborted ? this.createAbortError() : new Error('TypedSocket connection closed before it opened')); } }; this.websocket.onerror = () => { // transient socket errors (network blips, proxy TLS hiccups) are expected // and handled by reconnection; only exhausted retries surface as errors this.logTransient(`TypedSocket websocket error on ${wsUrl}`); }; try { await done.promise; } catch (err) { if (connectionTimeout) { clearTimeout(connectionTimeout); } if (this.shouldReconnect()) { await this.scheduleReconnect(); if (this.connectionStatus !== 'connected') { throw this.stopped || this.clientOptions?.abortSignal?.aborted ? this.createAbortError() : err; } } else { throw err; } } } /** * Converts an HTTP(S) URL to a WebSocket URL */ private toWebSocketUrl(url: string): string { const parsed = new URL(url); const wsProtocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'; return `${wsProtocol}//${parsed.host}${parsed.pathname}`; } /** * Handles incoming WebSocket messages */ private async handleMessage(data: string | ArrayBuffer): Promise { try { const messageText = typeof data === 'string' ? data : new TextDecoder().decode(data); const message = plugins.smartjson.parse(messageText) as plugins.typedrequestInterfaces.ITypedRequest; // Check if this is a response to a pending request if (message.correlation?.id && this.pendingRequests.has(message.correlation.id)) { const pending = this.pendingRequests.get(message.correlation.id)!; this.pendingRequests.delete(message.correlation.id); pending.resolve(message); return; } // Server-initiated request - route through TypedRouter const response = await this.typedrouter.routeAndAddResponse(message); if (response && this.websocket?.readyState === WebSocket.OPEN) { this.websocket.send(plugins.smartjson.stringify(response)); } } catch (err) { console.error('TypedSocket failed to process message:', err); } } /** * Handles WebSocket disconnection */ private handleDisconnect(): void { if (this.connectionStatus === 'disconnected') { return; // Already handled } this.updateStatus('disconnected'); // Reject all pending requests — the connection is gone and they'll never receive a response this.rejectPendingClientRequests(new Error('TypedSocket disconnected')); if (this.shouldReconnect()) { this.scheduleReconnect(); } } /** * Schedules a reconnection attempt with exponential backoff */ private async scheduleReconnect(): Promise { if (!this.shouldReconnect()) return; const clientOptions = this.clientOptions!; this.updateStatus('reconnecting'); this.retryCount++; // Exponential backoff with jitter const jitter = this.currentBackoff * 0.2 * (Math.random() * 2 - 1); const delay = Math.min(this.currentBackoff + jitter, clientOptions.maxBackoffMs); this.logDebug(`TypedSocket reconnecting in ${Math.round(delay)}ms (attempt ${this.retryCount}/${clientOptions.maxRetries})`); await this.waitForReconnectDelay(delay); if (!this.shouldReconnect()) return; // Increase backoff for next time this.currentBackoff = Math.min(this.currentBackoff * 2, clientOptions.maxBackoffMs); try { await this.connect(); } catch (err) { if (this.shouldReconnect()) { this.logDebug(`TypedSocket reconnection attempt failed: ${err instanceof Error ? err.message : String(err)}`); } else if (!this.stopped && !this.clientOptions?.abortSignal?.aborted) { console.error( `TypedSocket giving up on ${this.serverUrl} after ${this.retryCount} attempts:`, err instanceof Error ? err.message : err, ); } } } /** * Lifecycle chatter: hidden by default in browser devtools (verbose level) * and irrelevant for servers, but available when debugging. */ private logDebug(messageArg: string) { console.debug(messageArg); } /** * Failures that reconnection is expected to recover from. Logged as debug * while retries remain; the final failure is reported by scheduleReconnect. */ private logTransient(messageArg: string) { if (this.shouldReconnect()) { this.logDebug(messageArg); } else if (!this.stopped && !this.clientOptions?.abortSignal?.aborted) { console.warn(messageArg); } } private shouldReconnect(): boolean { return Boolean( this.clientOptions?.autoReconnect && !this.stopped && !this.clientOptions.abortSignal?.aborted && this.retryCount < this.clientOptions.maxRetries, ); } private async waitForReconnectDelay(delayMsArg: number): Promise { await new Promise((resolve) => { let timeout: ReturnType | undefined; let finished = false; const finish = () => { if (finished) return; finished = true; if (timeout) { clearTimeout(timeout); } this.stopListeners.delete(finish); this.clientOptions?.abortSignal?.removeEventListener('abort', finish); resolve(); }; if (this.clientOptions?.abortSignal?.aborted) { finish(); return; } if (this.stopped) { finish(); return; } timeout = setTimeout(finish, delayMsArg); this.stopListeners.add(finish); this.clientOptions?.abortSignal?.addEventListener('abort', finish, { once: true }); }); } private createAbortError(): Error { return new Error('TypedSocket client startup aborted'); } private rejectPendingClientRequests(errorArg: Error): void { for (const pending of this.pendingRequests.values()) { pending.reject(errorArg); } this.pendingRequests.clear(); } /** * Updates connection status and notifies subscribers */ private updateStatus(status: TConnectionStatus): void { if (this.connectionStatus !== status) { this.connectionStatus = status; this.statusSubject.next(status); } } /** * Sends a request to the server and waits for response (client-side) */ private async sendRequest( request: T, optionsArg: ITypedSocketRequestOptions = {}, ): Promise { if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { throw new Error('WebSocket not connected'); } const timeoutMs = this.normalizeRequestTimeout(optionsArg.timeoutMs); if (optionsArg.abortSignal?.aborted) { throw new Error('TypedSocket request aborted'); } request.correlation ||= { id: plugins.smartstring.create.createCryptoRandomString(), phase: 'request', }; const correlationId = request.correlation.id; return new Promise((resolve, reject) => { let settled = false; const cleanup = () => { clearTimeout(timeout); optionsArg.abortSignal?.removeEventListener('abort', onAbort); this.pendingRequests.delete(correlationId); }; const settle = (errorArg?: Error, responseArg?: T) => { if (settled) { return; } settled = true; cleanup(); if (errorArg) { reject(errorArg); } else { resolve(responseArg!); } }; const onAbort = () => settle(new Error('TypedSocket request aborted')); const timeout = setTimeout(() => { settle(new Error('TypedSocket request timed out')); }, timeoutMs); this.pendingRequests.set(correlationId, { resolve: (response) => { settle(undefined, response as T); }, reject: (error) => { settle(error); }, }); optionsArg.abortSignal?.addEventListener('abort', onAbort, { once: true }); if (optionsArg.abortSignal?.aborted) { onAbort(); return; } try { this.websocket!.send(plugins.smartjson.stringify(request)); } catch (error) { settle(error instanceof Error ? error : new Error(String(error))); } }); } private normalizeRequestTimeout(timeoutMsArg: number | undefined): number { const timeoutMs = timeoutMsArg ?? 30000; if (!Number.isFinite(timeoutMs) || timeoutMs < 0) { throw new Error('TypedSocket request timeout must be a finite non-negative number'); } return timeoutMs; } private cancelPendingServerRequestsForPeer(peerIdArg: string, errorArg: Error): void { const correlationIds = this.pendingServerRequestIdsByPeerId.get(peerIdArg); if (!correlationIds) { return; } for (const correlationId of Array.from(correlationIds)) { this.pendingServerRequests.get(correlationId)?.cancel(errorArg); } } private async sendServerRequest( typedRequestDataArg: T, targetArg: ISmartServeConnectionWrapper, optionsArg: ITypedSocketRequestOptions, ): Promise { const timeoutMs = this.normalizeRequestTimeout(optionsArg.timeoutMs); if (this.serverStopping || !this.smartServeRef) { throw new Error('TypedSocket server stopped'); } if (optionsArg.abortSignal?.aborted) { throw new Error('TypedSocket request aborted'); } typedRequestDataArg.correlation ||= { id: plugins.smartstring.create.createCryptoRandomString(), phase: 'request', }; const correlationId = typedRequestDataArg.correlation.id; const peerId = targetArg.peer.id; if (this.pendingServerRequests.has(correlationId)) { throw new Error(`Duplicate in-flight TypedSocket correlation ID: ${correlationId}`); } const cancellation = plugins.smartpromise.defer(); // A stop/abort may happen while InterestMap.addInterest is yielding. Attach // a rejection handler immediately; Promise.race below still receives the // original rejection once the interest exists. void cancellation.promise.catch(() => {}); const cleanup = plugins.smartpromise.defer(); let cancellationRequested = false; const cancel = (errorArg: Error) => { if (cancellationRequested) { return; } cancellationRequested = true; cancellation.reject(errorArg); }; const pendingRequest: IPendingServerRequest = { peerId, cancel, cleanupComplete: cleanup.promise, }; this.pendingServerRequests.set(correlationId, pendingRequest); const requestsForPeer = this.pendingServerRequestIdsByPeerId.get(peerId) ?? new Set(); requestsForPeer.add(correlationId); this.pendingServerRequestIdsByPeerId.set(peerId, requestsForPeer); const onAbort = () => cancel(new Error('TypedSocket request aborted')); const timeout = setTimeout( () => cancel(new Error(`TypedSocket request timed out after ${timeoutMs}ms`)), timeoutMs, ); optionsArg.abortSignal?.addEventListener('abort', onAbort, { once: true }); if (optionsArg.abortSignal?.aborted) { onAbort(); } let destroyInterest: (() => void) | undefined; try { const interest = await this.typedrouter.fireEventInterestMap.addInterest( correlationId, typedRequestDataArg, ); destroyInterest = () => interest.destroy(); // Establish the rejecting race before send so synchronous close/abort and // send failures all use the same cleanup path. const response = Promise.race([ interest.interestFullfilled as Promise, cancellation.promise, ]); const targetIsStillConnected = this.smartServeRef ?.getWebSocketConnections() .some((peerArg) => peerArg.id === peerId) ?? false; if (!targetIsStillConnected) { cancel(new Error(`TypedSocket target connection closed: ${peerId}`)); } if (this.serverStopping) { cancel(new Error('TypedSocket server stopped')); } if (!cancellationRequested && targetIsStillConnected && !this.serverStopping) { targetArg.peer.send(plugins.smartjson.stringify(typedRequestDataArg)); } return await response; } finally { clearTimeout(timeout); optionsArg.abortSignal?.removeEventListener('abort', onAbort); this.pendingServerRequests.delete(correlationId); const remainingRequestsForPeer = this.pendingServerRequestIdsByPeerId.get(peerId); remainingRequestsForPeer?.delete(correlationId); if (remainingRequestsForPeer?.size === 0) { this.pendingServerRequestIdsByPeerId.delete(peerId); } destroyInterest?.(); cleanup.resolve(); } } // ============================================================================ // PUBLIC API - SHARED // ============================================================================ /** * Creates a TypedRequest for the specified method. * On clients, sends to the server. * On servers, sends to the specified target connection. */ public createTypedRequest( methodName: T['method'], targetConnection?: ISmartServeConnectionWrapper, optionsArg: ITypedSocketRequestOptions = {}, ): plugins.typedrequest.TypedRequest { const postMethod = async ( requestDataArg: plugins.typedrequestInterfaces.ITypedRequest, ): Promise => { const typedRequestData = requestDataArg as T; if (this.side === 'client') { return this.sendRequest(typedRequestData, optionsArg); } // Server-side: send to target connection if (!this.smartServeRef) { throw new Error('Server not initialized'); } let target = targetConnection; if (!target) { const allConnections = this.smartServeRef.getWebSocketConnections(); if (allConnections.length === 1) { const peer = allConnections[0]; target = wrapSmartServePeer(peer); } else if (allConnections.length === 0) { throw new Error('No WebSocket connections available'); } else { throw new Error('Multiple connections available - specify targetConnection'); } } return await this.sendServerRequest(typedRequestData, target, optionsArg); }; return new plugins.typedrequest.TypedRequest( new plugins.typedrequest.TypedTarget({ postMethod }), methodName ); } /** * Gets the current connection status */ public getStatus(): TConnectionStatus { return this.connectionStatus; } /** * Stops the TypedSocket client or cleans up server state */ public async stop(): Promise { if (this.side === 'client') { this.stopped = true; for (const stopListener of Array.from(this.stopListeners)) { stopListener(); } this.stopListeners.clear(); if (this.clientOptions) { this.clientOptions.autoReconnect = false; if (this.clientOptions.abortSignal && this.abortSignalListener) { this.clientOptions.abortSignal.removeEventListener('abort', this.abortSignalListener); } } this.abortSignalListener = undefined; if (this.websocket) { this.websocket.close(); this.websocket = null; } for (const pendingRequest of this.pendingRequests.values()) { pendingRequest.reject(new Error('TypedSocket stopped')); } this.pendingRequests.clear(); } else { this.serverStopping = true; const pendingRequests = Array.from(this.pendingServerRequests.values()); for (const pendingRequest of pendingRequests) { pendingRequest.cancel(new Error('TypedSocket server stopped')); } await Promise.all(pendingRequests.map((pendingRequest) => pendingRequest.cleanupComplete)); this.unsubscribeSmartServeConnectionClose?.(); this.unsubscribeSmartServeConnectionClose = null; this.smartServeRef = null; } } // ============================================================================ // CLIENT-ONLY METHODS // ============================================================================ /** * Sets a tag on this client connection. * Tags are stored on the server and can be used for filtering. * @client-only */ public async setTag( name: T['name'], payload: T['payload'] ): Promise { if (this.side !== 'client') { throw new Error('setTag is only available on clients'); } const mutation = Symbol(name); this.clientTagMutations.set(name, mutation); try { const request = this.createTypedRequest('__typedsocket_setTag'); const response = await request.fire({ name, payload }); if (!response.success) { throw new Error('Failed to set tag on server'); } if (this.clientTagMutations.get(name) === mutation) { this.clientTags.set(name, payload); } } finally { if (this.clientTagMutations.get(name) === mutation) { this.clientTagMutations.delete(name); } } } /** * Re-applies all client tags after a reconnect, since server-side tags are * bound to the previous connection. */ private async reapplyClientTags(): Promise { for (const [name, payload] of this.clientTags) { const request = this.createTypedRequest('__typedsocket_setTag'); const response = await request.fire({ name, payload }); if (!response.success) { throw new Error(`TypedSocket failed to restore tag ${name} after reconnect`); } } } /** * Removes a tag from this client connection. * @client-only */ public async removeTag(name: string): Promise { if (this.side !== 'client') { throw new Error('removeTag is only available on clients'); } const mutation = Symbol(name); this.clientTagMutations.set(name, mutation); this.clientTags.delete(name); try { const request = this.createTypedRequest('__typedsocket_removeTag'); const response = await request.fire({ name }); if (!response.success) { throw new Error('Failed to remove tag on server'); } } finally { if (this.clientTagMutations.get(name) === mutation) { this.clientTagMutations.delete(name); } } } // ============================================================================ // SERVER-ONLY METHODS // ============================================================================ /** * Finds all connections matching the filter function. * @server-only */ public async findAllTargetConnections( asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise ): Promise { if (this.side !== 'server' || !this.smartServeRef) { throw new Error('findAllTargetConnections is only available on servers'); } const matchingConnections: ISmartServeConnectionWrapper[] = []; for (const peer of this.smartServeRef.getWebSocketConnections()) { const wrapper = wrapSmartServePeer(peer); if (await asyncFindFuncArg(wrapper)) { matchingConnections.push(wrapper); } } return matchingConnections; } /** * Finds the first connection matching the filter function. * @server-only */ public async findTargetConnection( asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise ): Promise { const allMatching = await this.findAllTargetConnections(asyncFindFuncArg); return allMatching[0]; } /** * Finds all connections with the specified tag. * @server-only */ public async findAllTargetConnectionsByTag( keyArg: TTag['name'], payloadArg?: TTag['payload'] ): Promise { if (this.side !== 'server' || !this.smartServeRef) { throw new Error('findAllTargetConnectionsByTag is only available on servers'); } const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg); const results: ISmartServeConnectionWrapper[] = []; for (const peer of peers) { const wrapper = wrapSmartServePeer(peer); // If payload specified, also filter by payload if (payloadArg !== undefined) { const tag = await wrapper.getTagById(keyArg); if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) { continue; } } results.push(wrapper); } return results; } /** * Finds the first connection with the specified tag. * @server-only */ public async findTargetConnectionByTag( keyArg: TTag['name'], payloadArg?: TTag['payload'] ): Promise { const allResults = await this.findAllTargetConnectionsByTag(keyArg, payloadArg); return allResults[0]; } }