UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

382 lines (339 loc) 13.4 kB
import { SubscriptionCapable, SubscriptionType, SubscriptionOptions } from './interfaces/subscription-capable'; import { SubscriptionCursor, SubscriptionInput, SubscriptionResponse } from '../core/types/api/subscription'; import { SubscriptionBase, SubscriptionBaseState } from './subscription-base'; import { EventHandleCapable } from './interfaces/event-handle-capable'; import type { PubNubCore as PubNub } from '../core/pubnub-common'; import { EntityInterface } from './interfaces/entity-interface'; import { SubscriptionSet } from './subscription-set'; import { messageFingerprint } from '../core/utils'; /** * {@link Subscription} state object. * * State object used across multiple {@link Subscription} object clones. * * @internal */ class SubscriptionState extends SubscriptionBaseState { /** * Subscription-capable entity. * * EntityInterface with information that is required to receive real-time updates for it. */ entity: EntityInterface & SubscriptionCapable; /** * Create a subscription state object. * * @param parameters - State configuration options * @param parameters.client - PubNub client which will work with a subscription object. * @param parameters.entity - Entity for which a subscription object has been created. * @param [parameters.options] - Subscription behavior options. */ constructor(parameters: { client: PubNub<unknown, unknown>; entity: EntityInterface & SubscriptionCapable; options?: SubscriptionOptions; }) { const names = parameters.entity.subscriptionNames(parameters.options?.receivePresenceEvents ?? false); const subscriptionInput = new SubscriptionInput({ [parameters.entity.subscriptionType == SubscriptionType.Channel ? 'channels' : 'channelGroups']: names, }); super(parameters.client, subscriptionInput, parameters.options, parameters.client.subscriptionTimetoken); this.entity = parameters.entity; } } /** * Single-entity subscription object which can be used to receive and handle real-time updates. */ export class Subscription extends SubscriptionBase { /** * List of subscription {@link SubscriptionSet sets} which contains {@link Subscription subscription}. * * List if used to track usage of a specific {@link Subscription subscription} in other subscription * {@link SubscriptionSet sets}. * * **Important:** Tracking is required to prevent cloned instance dispose if there are sets that still use it. * * @internal */ private parents: SubscriptionSet[] = []; /** * List of fingerprints from updates which has been handled already. * * **Important:** Tracking is required to avoid repetitive call of the subscription object's listener when the object * is part of multiple subscribed sets. Handler will be called once, and then the fingerprint will be stored in this * list to avoid another listener call for it. * * @internal */ private handledUpdates: string[] = []; /** * Create a subscribing capable object for entity. * * @param parameters - Subscription object configuration. * * @internal */ constructor( parameters: | { client: PubNub<unknown, unknown>; entity: EntityInterface & SubscriptionCapable; options: SubscriptionOptions | undefined; } | { state: SubscriptionState }, ) { if ('client' in parameters) { parameters.client.logger.debug('Subscription', () => ({ messageType: 'object', details: 'Create subscription with parameters:', message: { entity: parameters.entity, ...(parameters.options ? parameters.options : {}) }, })); } else parameters.state.client.logger.debug('Subscription', 'Create subscription clone'); super('state' in parameters ? parameters.state : new SubscriptionState(parameters)); this.state.storeClone(this.id, this); } /** * Get a {@link Subscription} object state. * * @returns: {@link Subscription} object state. * * @internal */ override get state(): SubscriptionState { return super.state as SubscriptionState; } /** * Get number of {@link SubscriptionSet} which use this subscription object. * * @returns Number of {@link SubscriptionSet} which use this subscription object. * * @internal */ get parentSetsCount(): number { return this.parents.length; } // -------------------------------------------------------- // -------------------- Event handler --------------------- // -------------------------------------------------------- // region Event handler /** * Dispatch received a real-time update. * * @param cursor - A time cursor for the next portion of events. * @param event - A real-time event from multiplexed subscription. * * @return `true` if receiver has consumed event. * * @internal */ override handleEvent(cursor: SubscriptionCursor, event: SubscriptionResponse['messages'][0]): void { if ( !this.state.isSubscribed || !this.state.subscriptionInput.contains(event.data.subscription ?? event.data.channel) ) return; if (this.parentSetsCount > 0) { // Creating from whole payload (not only for published messages). const fingerprint = messageFingerprint(event.data); if (this.handledUpdates.includes(fingerprint)) { this.state.client.logger.trace( this.subscriptionType, `Event (${fingerprint}) already handled by ${this.id}. Ignoring.`, ); return; } // Update a list of tracked messages and shrink it if too big. this.handledUpdates.push(fingerprint); if (this.handledUpdates.length > 10) this.handledUpdates.shift(); } // Check whether an event is not designated for this subscription set. if (!this.state.subscriptionInput.contains(event.data.subscription ?? event.data.channel)) return; super.handleEvent(cursor, event); } // endregion /** * User-provided subscription input associated with this {@link Subscription} object. * * @param forUnsubscribe - Whether list subscription input created for unsubscription (means entity should be free). * * @returns Subscription input object. * * @internal */ override subscriptionInput(forUnsubscribe: boolean = false) { if (forUnsubscribe && this.state.entity.subscriptionsCount > 0) return new SubscriptionInput({}); return this.state.subscriptionInput; } /** * Make a bare copy of the {@link Subscription} object. * * Copy won't have any type-specific listeners or added listener objects but will have the same internal state as * the source object. * * @returns Bare copy of a {@link Subscription} object. */ override cloneEmpty(): Subscription { return new Subscription({ state: this.state }); } /** * Graceful {@link Subscription} object destruction. * * This is an instance destructor, which will properly deinitialize it: * - remove and unset all listeners, * - try to unsubscribe (if subscribed and there are no more instances interested in the same data stream). * * **Important:** {@link Subscription#dispose dispose} won't have any effect if a subscription object is part of * {@link SubscriptionSet set}. To gracefully dispose an object, it should be removed from the set using * {@link SubscriptionSet#removeSubscription removeSubscription} (in this case call of * {@link Subscription#dispose dispose} not required). * * **Note:** Disposed instance won't call the dispatcher to deliver updates to the listeners. */ dispose(): void { if (this.parentSetsCount > 0) { this.state.client.logger.debug(this.subscriptionType, () => ({ messageType: 'text', message: `'${this.state.entity.subscriptionNames()}' subscription still in use. Ignore dispose request.`, })); return; } this.handledUpdates.splice(0, this.handledUpdates.length); super.dispose(); } /** * Invalidate subscription object. * * Clean up resources used by a subscription object. * * **Note:** An invalidated instance won't call the dispatcher to deliver updates to the listeners. * * @param forDestroy - Whether subscription object invalidated as part of PubNub client destroy process or not. * * @internal */ override invalidate(forDestroy: boolean = false) { if (forDestroy) this.state.entity.decreaseSubscriptionCount(this.state.id); this.handledUpdates.splice(0, this.handledUpdates.length); super.invalidate(forDestroy); } /** * Add another {@link SubscriptionSet} into which subscription has been added. * * @param parent - {@link SubscriptionSet} which has been modified. * * @internal */ addParentSet(parent: SubscriptionSet) { if (!this.parents.includes(parent)) { this.parents.push(parent); this.state.client.logger.trace( this.subscriptionType, `Add parent subscription set for ${this.id}: ${parent.id}. Parent subscription set count: ${ this.parentSetsCount }`, ); } } /** * Remove {@link SubscriptionSet} upon subscription removal from it. * * @param parent - {@link SubscriptionSet} which has been modified. * * @internal */ removeParentSet(parent: SubscriptionSet) { const parentIndex = this.parents.indexOf(parent); if (parentIndex !== -1) { this.parents.splice(parentIndex, 1); this.state.client.logger.trace( this.subscriptionType, `Remove parent subscription set from ${this.id}: ${parent.id}. Parent subscription set count: ${ this.parentSetsCount }`, ); } if (this.parentSetsCount === 0) this.handledUpdates.splice(0, this.handledUpdates.length); } /** * Merge entities' subscription objects into {@link SubscriptionSet}. * * @param subscription - Another entity's subscription object to be merged with receiver. * * @return {@link SubscriptionSet} which contains both receiver and other entities' subscription objects. */ addSubscription(subscription: Subscription): SubscriptionSet { this.state.client.logger.debug(this.subscriptionType, () => ({ messageType: 'text', message: `Create set with subscription: ${subscription}`, })); const subscriptionSet = new SubscriptionSet({ client: this.state.client, subscriptions: [this, subscription], options: this.state.options, }); // Check whether a source subscription is already subscribed or not. if (!this.state.isSubscribed && !subscription.state.isSubscribed) return subscriptionSet; this.state.client.logger.trace( this.subscriptionType, 'Subscribe resulting set because the receiver is already subscribed.', ); // Subscribing resulting subscription set because source subscription was subscribed. subscriptionSet.subscribe(); return subscriptionSet; } /** * Register {@link Subscription} object for real-time events' retrieval. * * **Note:** Superclass calls this method only in response to a {@link Subscription.subscribe subscribe} method call. * * @param parameters - Object registration parameters. * @param [parameters.cursor] - Subscription real-time events catch-up cursor. * @param [parameters.subscriptions] - List of subscription objects which should be registered (in case of partial * modification). * * @internal */ protected register(parameters: { cursor?: SubscriptionCursor; subscriptions?: EventHandleCapable[] }): void { this.state.entity.increaseSubscriptionCount(this.state.id); this.state.client.logger.trace(this.subscriptionType, () => ({ messageType: 'text', message: `Register subscription for real-time events: ${this}`, })); this.state.client.registerEventHandleCapable(this, parameters.cursor); } /** * Unregister {@link Subscription} object from real-time events' retrieval. * * **Note:** Superclass calls this method only in response to a {@link Subscription.unsubscribe unsubscribe} method * call. * * @param [_subscriptions] - List of subscription objects which should be unregistered (in case of partial * modification). * * @internal */ protected unregister(_subscriptions?: Subscription[]) { this.state.entity.decreaseSubscriptionCount(this.state.id); this.state.client.logger.trace(this.subscriptionType, () => ({ messageType: 'text', message: `Unregister subscription from real-time events: ${this}`, })); this.handledUpdates.splice(0, this.handledUpdates.length); this.state.client.unregisterEventHandleCapable(this); } /** * Stringify subscription object. * * @returns Serialized subscription object. */ toString(): string { const state = this.state; return `${this.subscriptionType} { id: ${this.id}, stateId: ${state.id}, entity: ${state.entity .subscriptionNames(false) .pop()}, clonesCount: ${ Object.keys(state.clones).length }, isSubscribed: ${state.isSubscribed}, parentSetsCount: ${this.parentSetsCount}, cursor: ${ state.cursor ? state.cursor.timetoken : 'not set' }, referenceTimetoken: ${state.referenceTimetoken ? state.referenceTimetoken : 'not set'} }`; } }