UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

282 lines (242 loc) 11 kB
import { PubNubClientManagerRegisterEvent, PubNubClientManagerUnregisterEvent, } from './custom-events/client-manager-event'; import { PubNubClientEvent, PubNubClientUnregisterEvent } from './custom-events/client-event'; import { RegisterEvent } from '../subscription-worker-types'; import { PubNubClient } from './pubnub-client'; /** * Registered {@link PubNubClient|PubNub} client instances manager. * * Manager responsible for keeping track and interaction with registered {@link PubNubClient|PubNub}. */ export class PubNubClientsManager extends EventTarget { // -------------------------------------------------------- // ---------------------- Information --------------------- // -------------------------------------------------------- // region Information /** * Map of started `PING` timeouts per subscription key. */ private timeouts: { [subKey: string]: { timeout?: ReturnType<typeof setTimeout>; interval: number; unsubscribeOffline: boolean }; } = {}; /** * Map of previously created {@link PubNubClient|PubNub} clients. */ private clients: Record<string, { client: PubNubClient; abortController: AbortController }> = {}; /** * Map of previously created {@link PubNubClient|PubNub} clients to the corresponding subscription key. */ private clientBySubscribeKey: Record<string, PubNubClient[]> = {}; // endregion // -------------------------------------------------------- // --------------------- Constructors --------------------- // -------------------------------------------------------- // region Constructors /** * Create {@link PubNubClient|PubNub} clients manager. * * @param sharedWorkerIdentifier - Unique `Subscription` worker identifier that will work with clients. */ constructor(private readonly sharedWorkerIdentifier: string) { super(); } // endregion // -------------------------------------------------------- // ----------------- Client registration ------------------ // -------------------------------------------------------- // region Client registration /** * Create {@link PubNubClient|PubNub} client. * * Function called in response to the `client-register` from the core {@link PubNubClient|PubNub} client module. * * @param event - Registration event with base {@link PubNubClient|PubNub} client information. * @param port - Message port for two-way communication with core {@link PubNubClient|PubNub} client module. * @returns New {@link PubNubClient|PubNub} client or existing one from the cache. */ createClient(event: RegisterEvent, port: MessagePort) { if (this.clients[event.clientIdentifier]) return this.clients[event.clientIdentifier]; const client = new PubNubClient( event.clientIdentifier, event.subscriptionKey, event.userId, port, event.workerLogLevel, event.heartbeatInterval, ); this.registerClient(client); // Start offline PubNub clients checks (ping-pong). if (event.workerOfflineClientsCheckInterval) { this.startClientTimeoutCheck( event.subscriptionKey, event.workerOfflineClientsCheckInterval, event.workerUnsubscribeOfflineClients ?? false, ); } return client; } /** * Store {@link PubNubClient|PubNub} client in manager's internal state. * * @param client - Freshly created {@link PubNubClient|PubNub} client which should be registered. */ private registerClient(client: PubNubClient) { this.clients[client.identifier] = { client, abortController: new AbortController() }; // Associate client with subscription key. if (!this.clientBySubscribeKey[client.subKey]) this.clientBySubscribeKey[client.subKey] = [client]; else this.clientBySubscribeKey[client.subKey].push(client); this.forEachClient(client.subKey, (subKeyClient) => subKeyClient.logger.debug( `'${client.identifier}' client registered with '${this.sharedWorkerIdentifier}' shared worker (${ this.clientBySubscribeKey[client.subKey].length } active clients).`, ), ); this.subscribeOnClientEvents(client); this.dispatchEvent(new PubNubClientManagerRegisterEvent(client)); } /** * Remove {@link PubNubClient|PubNub} client from manager's internal state. * * @param client - Previously created {@link PubNubClient|PubNub} client which should be removed. * @param [withLeave=false] - Whether `leave` request should be sent or not. * @param [onClientInvalidation=false] - Whether client removal caused by its invalidation (event from the * {@link PubNubClient|PubNub} client) or as result of timeout check. */ private unregisterClient(client: PubNubClient, withLeave = false, onClientInvalidation = false) { if (!this.clients[client.identifier]) return; // Make sure to detach all listeners for this `client`. if (this.clients[client.identifier].abortController) this.clients[client.identifier].abortController.abort(); delete this.clients[client.identifier]; const clientsBySubscribeKey = this.clientBySubscribeKey[client.subKey]; if (clientsBySubscribeKey) { const clientIdx = clientsBySubscribeKey.indexOf(client); clientsBySubscribeKey.splice(clientIdx, 1); if (clientsBySubscribeKey.length === 0) { delete this.clientBySubscribeKey[client.subKey]; this.stopClientTimeoutCheck(client); } } this.forEachClient(client.subKey, (subKeyClient) => subKeyClient.logger.debug( `'${this.sharedWorkerIdentifier}' shared worker unregistered '${client.identifier}' client (${ this.clientBySubscribeKey[client.subKey].length } active clients).`, ), ); if (!onClientInvalidation) client.invalidate(); this.dispatchEvent(new PubNubClientManagerUnregisterEvent(client, withLeave)); } // endregion // -------------------------------------------------------- // ----------------- Availability check ------------------- // -------------------------------------------------------- // region Availability check /** * Start timer for _timeout_ {@link PubNubClient|PubNub} client checks. * * @param subKey - Subscription key to get list of {@link PubNubClient|PubNub} clients that should be checked. * @param interval - Interval at which _timeout_ check should be done. * @param unsubscribeOffline - Whether _timeout_ (or _offline_) {@link PubNubClient|PubNub} clients should send * `leave` request before invalidation or not. */ private startClientTimeoutCheck(subKey: string, interval: number, unsubscribeOffline: boolean) { if (this.timeouts[subKey]) return; this.forEachClient(subKey, (client) => client.logger.debug(`Setup PubNub client ping for every ${interval} seconds.`), ); this.timeouts[subKey] = { interval, unsubscribeOffline, timeout: setTimeout(() => this.handleTimeoutCheck(subKey), interval * 500 - 1), }; } /** * Stop _timeout_ (or _offline_) {@link PubNubClient|PubNub} clients pinging. * * **Note:** This method is used only when all clients for a specific subscription key have been unregistered. * * @param client - {@link PubNubClient|PubNub} client with which the last client related by subscription key has been * removed. */ private stopClientTimeoutCheck(client: PubNubClient) { if (!this.timeouts[client.subKey]) return; if (this.timeouts[client.subKey].timeout) clearTimeout(this.timeouts[client.subKey].timeout); delete this.timeouts[client.subKey]; } /** * Handle periodic {@link PubNubClient|PubNub} client timeout checks. * * @param subKey - Subscription key to get list of {@link PubNubClient|PubNub} clients that should be checked. */ private handleTimeoutCheck(subKey: string) { if (!this.timeouts[subKey]) return; const interval = this.timeouts[subKey].interval; [...this.clientBySubscribeKey[subKey]].forEach((client) => { // Handle potential SharedWorker timers throttling and early eviction of the PubNub core client. // If timer fired later than specified interval - it has been throttled and shouldn't unregister client. if (client.lastPingRequest && Date.now() / 1000 - client.lastPingRequest - 0.2 > interval * 0.5) { client.logger.warn('PubNub clients timeout timer fired after throttling past due time.'); client.lastPingRequest = undefined; } if ( client.lastPingRequest && (!client.lastPongEvent || Math.abs(client.lastPongEvent - client.lastPingRequest) > interval) ) { this.unregisterClient(client, this.timeouts[subKey].unsubscribeOffline); // Notify other clients with same subscription key that one of them became inactive. this.forEachClient(subKey, (subKeyClient) => { if (subKeyClient.identifier !== client.identifier) subKeyClient.logger.debug(`'${client.identifier}' client is inactive. Invalidating...`); }); } if (this.clients[client.identifier]) { client.lastPingRequest = Date.now() / 1000; client.postEvent({ type: 'shared-worker-ping' }); } }); // Restart PubNub clients timeout check timer. if (this.timeouts[subKey]) this.timeouts[subKey].timeout = setTimeout(() => this.handleTimeoutCheck(subKey), interval * 500); } // endregion // -------------------------------------------------------- // ------------------- Event Handlers --------------------- // -------------------------------------------------------- // region Event handlers /** * Listen for {@link PubNubClient|PubNub} client events that affect aggregated subscribe/heartbeat requests. * * @param client - {@link PubNubClient|PubNub} client for which event should be listened. */ private subscribeOnClientEvents(client: PubNubClient) { client.addEventListener( PubNubClientEvent.Unregister, () => this.unregisterClient( client, this.timeouts[client.subKey] ? this.timeouts[client.subKey].unsubscribeOffline : false, true, ), { signal: this.clients[client.identifier].abortController.signal, once: true }, ); } // endregion // -------------------------------------------------------- // ----------------------- Helpers ------------------------ // -------------------------------------------------------- // region Helpers /** * Call callback function for all {@link PubNubClient|PubNub} clients that have similar `subscribeKey`. * * @param subKey - Subscription key for which list of clients should be retrieved. * @param callback - Function that will be called for each client list entry. */ private forEachClient(subKey: string, callback: (client: PubNubClient) => void) { if (!this.clientBySubscribeKey[subKey]) return; this.clientBySubscribeKey[subKey].forEach(callback); } // endregion }