pubnub
Version:
Publish & Subscribe Real-time Messaging with PubNub
311 lines (310 loc) • 13.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Subscription = void 0;
const subscription_capable_1 = require("./interfaces/subscription-capable");
const subscription_1 = require("../core/types/api/subscription");
const subscription_base_1 = require("./subscription-base");
const subscription_set_1 = require("./subscription-set");
const utils_1 = require("../core/utils");
/**
* {@link Subscription} state object.
*
* State object used across multiple {@link Subscription} object clones.
*
* @internal
*/
class SubscriptionState extends subscription_base_1.SubscriptionBaseState {
/**
* 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) {
var _a, _b;
const names = parameters.entity.subscriptionNames((_b = (_a = parameters.options) === null || _a === void 0 ? void 0 : _a.receivePresenceEvents) !== null && _b !== void 0 ? _b : false);
const subscriptionInput = new subscription_1.SubscriptionInput({
[parameters.entity.subscriptionType == subscription_capable_1.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.
*/
class Subscription extends subscription_base_1.SubscriptionBase {
/**
* Create a subscribing capable object for entity.
*
* @param parameters - Subscription object configuration.
*
* @internal
*/
constructor(parameters) {
if ('client' in parameters) {
parameters.client.logger.debug('Subscription', () => ({
messageType: 'object',
details: 'Create subscription with parameters:',
message: Object.assign({ 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));
/**
* 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
*/
this.parents = [];
/**
* 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
*/
this.handledUpdates = [];
this.state.storeClone(this.id, this);
}
/**
* Get a {@link Subscription} object state.
*
* @returns: {@link Subscription} object state.
*
* @internal
*/
get state() {
return super.state;
}
/**
* Get number of {@link SubscriptionSet} which use this subscription object.
*
* @returns Number of {@link SubscriptionSet} which use this subscription object.
*
* @internal
*/
get parentSetsCount() {
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
*/
handleEvent(cursor, event) {
var _a, _b;
if (!this.state.isSubscribed ||
!this.state.subscriptionInput.contains((_a = event.data.subscription) !== null && _a !== void 0 ? _a : event.data.channel))
return;
if (this.parentSetsCount > 0) {
// Creating from whole payload (not only for published messages).
const fingerprint = (0, utils_1.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((_b = event.data.subscription) !== null && _b !== void 0 ? _b : 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
*/
subscriptionInput(forUnsubscribe = false) {
if (forUnsubscribe && this.state.entity.subscriptionsCount > 0)
return new subscription_1.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.
*/
cloneEmpty() {
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() {
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
*/
invalidate(forDestroy = 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) {
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) {
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) {
this.state.client.logger.debug(this.subscriptionType, () => ({
messageType: 'text',
message: `Create set with subscription: ${subscription}`,
}));
const subscriptionSet = new subscription_set_1.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
*/
register(parameters) {
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
*/
unregister(_subscriptions) {
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() {
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'} }`;
}
}
exports.Subscription = Subscription;