pubnub
Version:
Publish & Subscribe Real-time Messaging with PubNub
1,497 lines (1,331 loc) • 181 kB
text/typescript
/**
* Core PubNub API module.
*/
// region Imports
// region Components
import { EventDispatcher, Listener } from './components/event-dispatcher';
import { SubscriptionManager } from './components/subscription-manager';
import NotificationsPayload from './components/push_payload';
import { TokenManager } from './components/token_manager';
import { AbstractRequest } from './components/request';
import Crypto from './components/cryptography/index';
import { encode } from './components/base64_codec';
import uuidGenerator from './components/uuid';
// endregion
// region Types
import { Payload, ResultCallback, Status, StatusCallback, StatusEvent } from './types/api';
// endregion
// region Component Interfaces
import { ClientConfiguration, PrivateClientConfiguration } from './interfaces/configuration';
import { Cryptography } from './interfaces/cryptography';
import { Transport } from './interfaces/transport';
// endregion
// region Constants
import RequestOperation from './constants/operations';
import StatusCategory from './constants/categories';
// endregion
import { createValidationError, PubNubError } from '../errors/pubnub-error';
import { PubNubAPIError } from '../errors/pubnub-api-error';
import { RetryPolicy, Endpoint } from './components/retry-policy';
// region Event Engine
import { PresenceEventEngine } from '../event-engine/presence/presence';
import { EventEngine } from '../event-engine';
// endregion
// region Publish & Signal
import * as Publish from './endpoints/publish';
import * as Signal from './endpoints/signal';
// endregion
// region Subscription
import {
SubscribeRequestParameters as SubscribeRequestParameters,
SubscribeRequest,
PubNubEventType,
} from './endpoints/subscribe';
import { ReceiveMessagesSubscribeRequest } from './endpoints/subscriptionUtils/receiveMessages';
import { HandshakeSubscribeRequest } from './endpoints/subscriptionUtils/handshake';
import { Subscription as SubscriptionObject } from '../entities/subscription';
import * as Subscription from './types/api/subscription';
// endregion
// region Presence
import { GetPresenceStateRequest } from './endpoints/presence/get_state';
import { SetPresenceStateRequest } from './endpoints/presence/set_state';
import { HeartbeatRequest } from './endpoints/presence/heartbeat';
import { PresenceLeaveRequest } from './endpoints/presence/leave';
import { WhereNowRequest } from './endpoints/presence/where_now';
import { HereNowRequest } from './endpoints/presence/here_now';
import * as Presence from './types/api/presence';
// endregion
// region Message Storage
import { DeleteMessageRequest } from './endpoints/history/delete_messages';
import { MessageCountRequest } from './endpoints/history/message_counts';
import { GetHistoryRequest } from './endpoints/history/get_history';
import { FetchMessagesRequest } from './endpoints/fetch_messages';
import * as History from './types/api/history';
// endregion
// region Message Actions
import { GetMessageActionsRequest } from './endpoints/actions/get_message_actions';
import { AddMessageActionRequest } from './endpoints/actions/add_message_action';
import { RemoveMessageAction } from './endpoints/actions/remove_message_action';
import * as MessageAction from './types/api/message-action';
// endregion
// region File sharing
import { PublishFileMessageRequest } from './endpoints/file_upload/publish_file';
import { GetFileDownloadUrlRequest } from './endpoints/file_upload/get_file_url';
import { DeleteFileRequest } from './endpoints/file_upload/delete_file';
import { FilesListRequest } from './endpoints/file_upload/list_files';
import { SendFileRequest } from './endpoints/file_upload/send_file';
import * as FileSharing from './types/api/file-sharing';
import { PubNubFileInterface } from './types/file';
// endregion
// region PubNub Access Manager
import { RevokeTokenRequest } from './endpoints/access_manager/revoke_token';
import { GrantTokenRequest } from './endpoints/access_manager/grant_token';
import { GrantRequest } from './endpoints/access_manager/grant';
import { AuditRequest } from './endpoints/access_manager/audit';
import * as PAM from './types/api/access-manager';
// endregion
// region Entities
import {
SubscriptionCapable,
SubscriptionOptions,
SubscriptionType,
} from '../entities/interfaces/subscription-capable';
import { EventEmitCapable } from '../entities/interfaces/event-emit-capable';
import { EntityInterface } from '../entities/interfaces/entity-interface';
import { SubscriptionBase } from '../entities/subscription-base';
import { ChannelMetadata } from '../entities/channel-metadata';
import { SubscriptionSet } from '../entities/subscription-set';
import { ChannelGroup } from '../entities/channel-group';
import { UserMetadata } from '../entities/user-metadata';
import { Channel } from '../entities/channel';
// endregion
// region Channel Groups
import PubNubChannelGroups from './pubnub-channel-groups';
// endregion
// region Push Notifications
import PubNubPushNotifications from './pubnub-push';
// endregion
// region App Context
import * as AppContext from './types/api/app-context';
import PubNubObjects from './pubnub-objects';
// endregion
// region Time
import * as Time from './endpoints/time';
// endregion
import { EventHandleCapable } from '../entities/interfaces/event-handle-capable';
import { DownloadFileRequest } from './endpoints/file_upload/download_file';
import { SubscriptionInput } from './types/api/subscription';
import { LoggerManager } from './components/logger-manager';
import { LogLevel as LoggerLogLevel } from './interfaces/logger';
import { encodeString, messageFingerprint } from './utils';
import { Entity } from '../entities/entity';
import Categories from './constants/categories';
// endregion
// --------------------------------------------------------
// ------------------------ Types -------------------------
// --------------------------------------------------------
// region Types
/**
* Core PubNub client configuration object.
*
* @internal
*/
type ClientInstanceConfiguration<CryptographyTypes> = {
/**
* Client-provided configuration.
*/
configuration: PrivateClientConfiguration;
/**
* Transport provider for requests execution.
*/
transport: Transport;
/**
* REST API endpoints access tokens manager.
*/
tokenManager?: TokenManager;
/**
* Legacy crypto module implementation.
*/
cryptography?: Cryptography<CryptographyTypes>;
/**
* Legacy crypto (legacy data encryption / decryption and request signature support).
*/
crypto?: Crypto;
};
// endregion
/**
* Platform-agnostic PubNub client core.
*/
export class PubNubCore<
CryptographyTypes,
FileConstructorParameters,
PlatformFile extends Partial<PubNubFileInterface> = Record<string, unknown>,
> implements EventEmitCapable
{
/**
* PubNub client configuration.
*
* @internal
*/
protected readonly _configuration: PrivateClientConfiguration;
/**
* Subscription loop manager.
*
* **Note:** Manager created when EventEngine is off.
*
* @internal
*/
private readonly subscriptionManager?: SubscriptionManager;
/**
* Transport for network requests processing.
*
* @internal
*/
protected readonly transport: Transport;
/**
* `userId` change handler.
*
* @internal
*/
protected onUserIdChange?: (userId: string) => void;
/**
* Heartbeat interval change handler.
*
* @internal
*/
protected onHeartbeatIntervalChange?: (interval: number) => void;
/**
* User's associated presence data change handler.
*
* @internal
*/
protected onPresenceStateChange?: (state: Record<string, Payload>) => void;
/**
* `authKey` or `token` change handler.
*
* @internal
*/
protected onAuthenticationChange?: (auth?: string) => void;
/**
* REST API endpoints access tokens manager.
*
* @internal
*/
private readonly tokenManager?: TokenManager;
/**
* Legacy crypto module implementation.
*
* @internal
*/
private readonly cryptography?: Cryptography<CryptographyTypes>;
/**
* Legacy crypto (legacy data encryption / decryption and request signature support).
*
* @internal
*/
private readonly crypto?: Crypto;
/**
* User's presence event engine.
*
* @internal
*/
private readonly presenceEventEngine?: PresenceEventEngine;
/**
* List of subscribe capable objects with active subscriptions.
*
* Track list of {@link Subscription} and {@link SubscriptionSet} objects with active
* subscription.
*
* @internal
*/
private eventHandleCapable: Record<string, EventEmitCapable & EventHandleCapable> = {};
/**
* Client-level subscription set.
*
* **Note:** client-level subscription set for {@link subscribe}, {@link unsubscribe}, and {@link unsubscribeAll}
* backward compatibility.
*
* **Important:** This should be removed as soon as the legacy subscription loop will be dropped.
*
* @internal
*/
private _globalSubscriptionSet?: SubscriptionSet;
/**
* Subscription event engine.
*
* @internal
*/
private readonly eventEngine?: EventEngine;
/**
* Client-managed presence information.
*
* @internal
*/
private readonly presenceState?: Record<string, Payload>;
/**
* Event emitter, which will notify listeners about updates received for channels / groups.
*
* @internal
*/
private readonly eventDispatcher?: EventDispatcher;
/**
* Created entities.
*
* Map of entities which have been created to access.
*
* @internal
*/
private readonly entities: Record<string, EntityInterface | undefined> = {};
/**
* PubNub App Context REST API entry point.
*
* @internal
*/
// @ts-expect-error Allowed to simplify interface when module can be disabled.
private readonly _objects: PubNubObjects;
/**
* PubNub Channel Group REST API entry point.
*
* @internal
*/
// @ts-expect-error Allowed to simplify interface when module can be disabled.
private readonly _channelGroups: PubNubChannelGroups;
/**
* PubNub Push Notification REST API entry point.
*
* @internal
*/
// @ts-expect-error Allowed to simplify interface when module can be disabled.
private readonly _push: PubNubPushNotifications;
/**
* {@link ArrayBuffer} to {@link string} decoder.
*
* @internal
*/
private static decoder = new TextDecoder();
// --------------------------------------------------------
// ----------------------- Static -------------------------
// --------------------------------------------------------
// region Static
/**
* Type of REST API endpoint which reported status.
*/
static OPERATIONS = RequestOperation;
/**
* API call status category.
*/
static CATEGORIES = StatusCategory;
/**
* Enum with API endpoint groups which can be used with retry policy to set up exclusions (which shouldn't be
* retried).
*/
static Endpoint = Endpoint;
/**
* Exponential retry policy constructor.
*/
static ExponentialRetryPolicy = RetryPolicy.ExponentialRetryPolicy;
/**
* Linear retry policy constructor.
*/
static LinearRetryPolicy = RetryPolicy.LinearRetryPolicy;
/**
* Disabled / inactive retry policy.
*
* **Note:** By default `ExponentialRetryPolicy` is set for subscribe requests and this one can be used to disable
* retry policy for all requests (setting `undefined` for retry configuration will set default policy).
*/
static NoneRetryPolicy = RetryPolicy.None;
/**
* Available minimum log levels.
*/
static LogLevel = LoggerLogLevel;
/**
* Construct notification payload which will trigger push notification.
*
* @param title - Title which will be shown on notification.
* @param body - Payload which will be sent as part of notification.
*
* @returns Pre-formatted message payload which will trigger push notification.
*/
static notificationPayload(title: string, body: string) {
if (process.env.PUBLISH_MODULE !== 'disabled') {
return new NotificationsPayload(title, body);
} else throw new Error('Notification Payload error: publish module disabled');
}
/**
* Generate unique identifier.
*
* @returns Unique identifier.
*/
static generateUUID() {
return uuidGenerator.createUUID();
}
// endregion
/**
* Create and configure PubNub client core.
*
* @param configuration - PubNub client core configuration.
* @returns Configured and ready to use PubNub client.
*
* @internal
*/
constructor(configuration: ClientInstanceConfiguration<CryptographyTypes>) {
this._configuration = configuration.configuration;
this.cryptography = configuration.cryptography;
this.tokenManager = configuration.tokenManager;
this.transport = configuration.transport;
this.crypto = configuration.crypto;
this.logger.debug('PubNub', () => ({
messageType: 'object',
message: configuration.configuration as unknown as Record<string, unknown>,
details: 'Create with configuration:',
ignoredKeys(key: string, obj: Record<string, unknown>) {
return typeof obj[key] === 'function' || key.startsWith('_');
},
}));
// API group entry points initialization.
if (process.env.APP_CONTEXT_MODULE !== 'disabled')
this._objects = new PubNubObjects(this._configuration, this.sendRequest.bind(this));
if (process.env.CHANNEL_GROUPS_MODULE !== 'disabled')
this._channelGroups = new PubNubChannelGroups(
this._configuration.logger(),
this._configuration.keySet,
this.sendRequest.bind(this),
);
if (process.env.MOBILE_PUSH_MODULE !== 'disabled')
this._push = new PubNubPushNotifications(
this._configuration.logger(),
this._configuration.keySet,
this.sendRequest.bind(this),
);
if (process.env.SUBSCRIBE_MODULE !== 'disabled') {
// Prepare for a real-time events announcement.
this.eventDispatcher = new EventDispatcher();
if (this._configuration.enableEventEngine) {
if (process.env.SUBSCRIBE_EVENT_ENGINE_MODULE !== 'disabled') {
this.logger.debug('PubNub', 'Using new subscription loop management.');
let heartbeatInterval = this._configuration.getHeartbeatInterval();
this.presenceState = {};
if (process.env.PRESENCE_MODULE !== 'disabled') {
if (heartbeatInterval) {
this.presenceEventEngine = new PresenceEventEngine({
heartbeat: (parameters, callback) => {
this.logger.trace('PresenceEventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Heartbeat with parameters:',
}));
return this.heartbeat(parameters, callback);
},
leave: (parameters) => {
this.logger.trace('PresenceEventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Leave with parameters:',
}));
this.makeUnsubscribe(parameters, () => {});
},
heartbeatDelay: () =>
new Promise((resolve, reject) => {
heartbeatInterval = this._configuration.getHeartbeatInterval();
if (!heartbeatInterval) reject(new PubNubError('Heartbeat interval has been reset.'));
else setTimeout(resolve, heartbeatInterval * 1000);
}),
emitStatus: (status) => this.emitStatus(status),
config: this._configuration,
presenceState: this.presenceState,
});
}
}
this.eventEngine = new EventEngine({
handshake: (parameters) => {
this.logger.trace('EventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Handshake with parameters:',
ignoredKeys: ['abortSignal', 'crypto', 'timeout', 'keySet', 'getFileUrl'],
}));
return this.subscribeHandshake(parameters);
},
receiveMessages: (parameters) => {
this.logger.trace('EventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Receive messages with parameters:',
ignoredKeys: ['abortSignal', 'crypto', 'timeout', 'keySet', 'getFileUrl'],
}));
return this.subscribeReceiveMessages(parameters);
},
delay: (amount) => new Promise((resolve) => setTimeout(resolve, amount)),
join: (parameters) => {
this.logger.trace('EventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Join with parameters:',
}));
if (parameters && (parameters.channels ?? []).length === 0 && (parameters.groups ?? []).length === 0) {
this.logger.trace('EventEngine', "Ignoring 'join' announcement request.");
return;
}
this.join(parameters);
},
leave: (parameters) => {
this.logger.trace('EventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Leave with parameters:',
}));
if (parameters && (parameters.channels ?? []).length === 0 && (parameters.groups ?? []).length === 0) {
this.logger.trace('EventEngine', "Ignoring 'leave' announcement request.");
return;
}
this.leave(parameters);
},
leaveAll: (parameters) => {
this.logger.trace('EventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Leave all with parameters:',
}));
this.leaveAll(parameters);
},
presenceReconnect: (parameters) => {
this.logger.trace('EventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Reconnect with parameters:',
}));
this.presenceReconnect(parameters);
},
presenceDisconnect: (parameters) => {
this.logger.trace('EventEngine', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Disconnect with parameters:',
}));
this.presenceDisconnect(parameters);
},
presenceState: this.presenceState,
config: this._configuration,
emitMessages: (cursor, events) => {
try {
this.logger.debug('EventEngine', () => {
const hashedEvents = events.map((event) => {
const pn_mfp =
event.type === PubNubEventType.Message || event.type === PubNubEventType.Signal
? messageFingerprint(event.data.message)
: undefined;
return pn_mfp ? { type: event.type, data: { ...event.data, pn_mfp } } : event;
});
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
});
events.forEach((event) => this.emitEvent(cursor, event));
} catch (e) {
const errorStatus: Status = {
error: true,
category: StatusCategory.PNUnknownCategory,
errorData: e as Error,
statusCode: 0,
};
this.emitStatus(errorStatus);
}
},
emitStatus: (status) => this.emitStatus(status),
});
} else throw new Error('Event Engine error: subscription event engine module disabled');
} else {
if (process.env.SUBSCRIBE_MANAGER_MODULE !== 'disabled') {
this.logger.debug('PubNub', 'Using legacy subscription loop management.');
this.subscriptionManager = new SubscriptionManager(
this._configuration,
(cursor, event) => {
try {
this.emitEvent(cursor, event);
} catch (e) {
const errorStatus: Status = {
error: true,
category: StatusCategory.PNUnknownCategory,
errorData: e as Error,
statusCode: 0,
};
this.emitStatus(errorStatus);
}
},
this.emitStatus.bind(this),
(parameters, callback) => {
this.logger.trace('SubscriptionManager', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Subscribe with parameters:',
ignoredKeys: ['crypto', 'timeout', 'keySet', 'getFileUrl'],
}));
this.makeSubscribe(parameters, callback);
},
(parameters, callback) => {
this.logger.trace('SubscriptionManager', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Heartbeat with parameters:',
ignoredKeys: ['crypto', 'timeout', 'keySet', 'getFileUrl'],
}));
return this.heartbeat(parameters, callback);
},
(parameters, callback) => {
this.logger.trace('SubscriptionManager', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Leave with parameters:',
}));
this.makeUnsubscribe(parameters, callback);
},
this.time.bind(this),
);
} else throw new Error('Subscription Manager error: subscription manager module disabled');
}
}
}
// --------------------------------------------------------
// -------------------- Configuration ----------------------
// --------------------------------------------------------
// region Configuration
/**
* PubNub client configuration.
*
* @returns Currently user PubNub client configuration.
*/
public get configuration(): ClientConfiguration {
return this._configuration;
}
/**
* Current PubNub client configuration.
*
* @returns Currently user PubNub client configuration.
*
* @deprecated Use {@link configuration} getter instead.
*/
public get _config(): ClientConfiguration {
return this.configuration;
}
/**
* REST API endpoint access authorization key.
*
* It is required to have `authorization key` with required permissions to access REST API
* endpoints when `PAM` enabled for user key set.
*/
get authKey(): string | undefined {
return this._configuration.authKey ?? undefined;
}
/**
* REST API endpoint access authorization key.
*
* It is required to have `authorization key` with required permissions to access REST API
* endpoints when `PAM` enabled for user key set.
*/
getAuthKey(): string | undefined {
return this.authKey;
}
/**
* Change REST API endpoint access authorization key.
*
* @param authKey - New authorization key which should be used with new requests.
*/
setAuthKey(authKey: string): void {
this.logger.debug('PubNub', `Set auth key: ${authKey}`);
this._configuration.setAuthKey(authKey);
if (this.onAuthenticationChange) this.onAuthenticationChange(authKey);
}
/**
* Get a PubNub client user identifier.
*
* @returns Current PubNub client user identifier.
*/
get userId(): string {
return this._configuration.userId!;
}
/**
* Change the current PubNub client user identifier.
*
* **Important:** Change won't affect ongoing REST API calls.
* **Warning:** Because ongoing REST API calls won't be canceled there could happen unexpected events like implicit
* `join` event for the previous `userId` after a long-poll subscribe request will receive a response. To avoid this
* it is advised to unsubscribe from all/disconnect before changing `userId`.
*
* @param value - New PubNub client user identifier.
*
* @throws Error empty user identifier has been provided.
*/
set userId(value: string) {
if (!value || typeof value !== 'string' || value.trim().length === 0) {
const error = new Error('Missing or invalid userId parameter. Provide a valid string userId');
this.logger.error('PubNub', () => ({ messageType: 'error', message: error }));
throw error;
}
this.logger.debug('PubNub', `Set user ID: ${value}`);
this._configuration.userId = value;
if (this.onUserIdChange) this.onUserIdChange(this._configuration.userId);
}
/**
* Get a PubNub client user identifier.
*
* @returns Current PubNub client user identifier.
*/
getUserId(): string {
return this._configuration.userId!;
}
/**
* Change the current PubNub client user identifier.
*
* **Important:** Change won't affect ongoing REST API calls.
*
* @param value - New PubNub client user identifier.
*
* @throws Error empty user identifier has been provided.
*/
setUserId(value: string): void {
this.userId = value;
}
/**
* Real-time updates filtering expression.
*
* @returns Filtering expression.
*/
get filterExpression(): string | undefined {
return this._configuration.getFilterExpression() ?? undefined;
}
/**
* Real-time updates filtering expression.
*
* @returns Filtering expression.
*/
getFilterExpression(): string | undefined {
return this.filterExpression;
}
/**
* Update real-time updates filtering expression.
*
* @param expression - New expression which should be used or `undefined` to disable filtering.
*/
set filterExpression(expression: string | null | undefined) {
this.logger.debug('PubNub', `Set filter expression: ${expression}`);
this._configuration.setFilterExpression(expression);
}
/**
* Update real-time updates filtering expression.
*
* @param expression - New expression which should be used or `undefined` to disable filtering.
*/
setFilterExpression(expression: string | null): void {
this.logger.debug('PubNub', `Set filter expression: ${expression}`);
this.filterExpression = expression;
}
/**
* Dta encryption / decryption key.
*
* @returns Currently used key for data encryption / decryption.
*/
get cipherKey(): string | undefined {
return this._configuration.getCipherKey();
}
/**
* Change data encryption / decryption key.
*
* @param key - New key which should be used for data encryption / decryption.
*/
set cipherKey(key: string | undefined) {
this._configuration.setCipherKey(key);
}
/**
* Change data encryption / decryption key.
*
* @param key - New key which should be used for data encryption / decryption.
*/
setCipherKey(key: string): void {
this.logger.debug('PubNub', `Set cipher key: ${key}`);
this.cipherKey = key;
}
/**
* Change a heartbeat requests interval.
*
* @param interval - New presence request heartbeat intervals.
*/
set heartbeatInterval(interval: number) {
this.logger.debug('PubNub', `Set heartbeat interval: ${interval}`);
this._configuration.setHeartbeatInterval(interval);
if (this.onHeartbeatIntervalChange) this.onHeartbeatIntervalChange(this._configuration.getHeartbeatInterval() ?? 0);
}
/**
* Change a heartbeat requests interval.
*
* @param interval - New presence request heartbeat intervals.
*/
setHeartbeatInterval(interval: number): void {
this.heartbeatInterval = interval;
}
/**
* Get registered loggers' manager.
*
* @returns Registered loggers' manager.
*/
get logger(): LoggerManager {
return this._configuration.logger();
}
/**
* Get PubNub SDK version.
*
* @returns Current SDK version.
*/
getVersion(): string {
return this._configuration.getVersion();
}
/**
* Add framework's prefix.
*
* @param name - Name of the framework which would want to add own data into `pnsdk` suffix.
* @param suffix - Suffix with information about a framework.
*/
_addPnsdkSuffix(name: string, suffix: string | number) {
this.logger.debug('PubNub', `Add '${name}' 'pnsdk' suffix: ${suffix}`);
this._configuration._addPnsdkSuffix(name, suffix);
}
// --------------------------------------------------------
// ---------------------- Deprecated ----------------------
// --------------------------------------------------------
// region Deprecated
/**
* Get a PubNub client user identifier.
*
* @returns Current PubNub client user identifier.
*
* @deprecated Use the {@link getUserId} or {@link userId} getter instead.
*/
getUUID(): string {
return this.userId;
}
/**
* Change the current PubNub client user identifier.
*
* **Important:** Change won't affect ongoing REST API calls.
*
* @param value - New PubNub client user identifier.
*
* @throws Error empty user identifier has been provided.
*
* @deprecated Use the {@link PubNubCore#setUserId setUserId} or {@link PubNubCore#userId userId} setter instead.
*/
setUUID(value: string) {
this.logger.warn('PubNub', "'setUserId` is deprecated, please use 'setUserId' or 'userId' setter instead.");
this.logger.debug('PubNub', `Set UUID: ${value}`);
this.userId = value;
}
/**
* Custom data encryption method.
*
* @deprecated Instead use {@link cryptoModule} for data encryption.
*/
get customEncrypt(): ((data: string) => string) | undefined {
return this._configuration.getCustomEncrypt();
}
/**
* Custom data decryption method.
*
* @deprecated Instead use {@link cryptoModule} for data decryption.
*/
get customDecrypt(): ((data: string) => string) | undefined {
return this._configuration.getCustomDecrypt();
}
// endregion
// endregion
// --------------------------------------------------------
// ---------------------- Entities ------------------------
// --------------------------------------------------------
// region Entities
/**
* Create a `Channel` entity.
*
* Entity can be used for the interaction with the following API:
* - `subscribe`
*
* @param name - Unique channel name.
* @returns `Channel` entity.
*/
public channel(name: string): Channel {
let channel = this.entities[`${name}_ch`];
if (!channel) channel = this.entities[`${name}_ch`] = new Channel(name, this);
return channel as Channel;
}
/**
* Create a `ChannelGroup` entity.
*
* Entity can be used for the interaction with the following API:
* - `subscribe`
*
* @param name - Unique channel group name.
* @returns `ChannelGroup` entity.
*/
public channelGroup(name: string): ChannelGroup {
let channelGroup = this.entities[`${name}_chg`];
if (!channelGroup) channelGroup = this.entities[`${name}_chg`] = new ChannelGroup(name, this);
return channelGroup as ChannelGroup;
}
/**
* Create a `ChannelMetadata` entity.
*
* Entity can be used for the interaction with the following API:
* - `subscribe`
*
* @param id - Unique channel metadata object identifier.
* @returns `ChannelMetadata` entity.
*/
public channelMetadata(id: string): ChannelMetadata {
let metadata = this.entities[`${id}_chm`];
if (!metadata) metadata = this.entities[`${id}_chm`] = new ChannelMetadata(id, this);
return metadata as ChannelMetadata;
}
/**
* Create a `UserMetadata` entity.
*
* Entity can be used for the interaction with the following API:
* - `subscribe`
*
* @param id - Unique user metadata object identifier.
* @returns `UserMetadata` entity.
*/
public userMetadata(id: string): UserMetadata {
let metadata = this.entities[`${id}_um`];
if (!metadata) metadata = this.entities[`${id}_um`] = new UserMetadata(id, this);
return metadata as UserMetadata;
}
/**
* Create subscriptions set object.
*
* @param parameters - Subscriptions set configuration parameters.
*/
public subscriptionSet(parameters: {
channels?: string[];
channelGroups?: string[];
subscriptionOptions?: SubscriptionOptions;
}): SubscriptionSet {
if (process.env.SUBSCRIBE_MODULE !== 'disabled') {
// Prepare a list of entities for a set.
const entities: (EntityInterface & SubscriptionCapable)[] = [];
parameters.channels?.forEach((name) => entities.push(this.channel(name)));
parameters.channelGroups?.forEach((name) => entities.push(this.channelGroup(name)));
return new SubscriptionSet({ client: this, entities, options: parameters.subscriptionOptions });
} else throw new Error('Subscription set error: subscription module disabled');
}
// endregion
// --------------------------------------------------------
// ----------------------- Common -------------------------
// --------------------------------------------------------
// region Common
/**
* Schedule request execution.
*
* @param request - REST API request.
* @param callback - Request completion handler callback.
*
* @returns Asynchronous request execution and response parsing result.
*
* @internal
*/
private sendRequest<ResponseType, ServiceResponse extends object>(
request: AbstractRequest<ResponseType, ServiceResponse>,
callback: ResultCallback<ResponseType>,
): void;
/**
* Schedule request execution.
*
* @internal
*
* @param request - REST API request.
*
* @returns Asynchronous request execution and response parsing result.
*/
private async sendRequest<ResponseType, ServiceResponse extends object>(
request: AbstractRequest<ResponseType, ServiceResponse>,
): Promise<ResponseType>;
/**
* Schedule request execution.
*
* @internal
*
* @param request - REST API request.
* @param [callback] - Request completion handler callback.
*
* @returns Asynchronous request execution and response parsing result or `void` in case if
* `callback` provided.
*
* @throws PubNubError in case of request processing error.
*/
private async sendRequest<ResponseType, ServiceResponse extends object>(
request: AbstractRequest<ResponseType, ServiceResponse>,
callback?: ResultCallback<ResponseType>,
): Promise<ResponseType | void> {
// Validate user-input.
const validationResult = request.validate();
if (validationResult) {
const validationError = createValidationError(validationResult);
this.logger.error('PubNub', () => ({ messageType: 'error', message: validationError }));
if (callback) return callback(validationError, null);
throw new PubNubError('Validation failed, check status for details', validationError);
}
// Complete request configuration.
const transportRequest = request.request();
const operation = request.operation();
if (
(transportRequest.formData && transportRequest.formData.length > 0) ||
operation === RequestOperation.PNDownloadFileOperation
) {
// Set file upload / download request delay.
transportRequest.timeout = this._configuration.getFileTimeout();
} else {
if (
operation === RequestOperation.PNSubscribeOperation ||
operation === RequestOperation.PNReceiveMessagesOperation
)
transportRequest.timeout = this._configuration.getSubscribeTimeout();
else transportRequest.timeout = this._configuration.getTransactionTimeout();
}
// API request processing status.
const status: Status = {
error: false,
operation,
category: StatusCategory.PNAcknowledgmentCategory,
statusCode: 0,
};
const [sendableRequest, cancellationController] = this.transport.makeSendable(transportRequest);
/**
* **Important:** Because of multiple environments where JS SDK can be used, control over
* cancellation had to be inverted to let the transport provider solve a request cancellation task
* more efficiently. As a result, cancellation controller can be retrieved and used only after
* the request will be scheduled by the transport provider.
*/
request.cancellationController = cancellationController ? cancellationController : null;
return sendableRequest
.then((response) => {
status.statusCode = response.status;
// Handle a special case when request completed but not fully processed by PubNub service.
if (response.status !== 200 && response.status !== 204) {
const responseText = PubNubCore.decoder.decode(response.body);
const contentType = response.headers['content-type'];
if (contentType || contentType.indexOf('javascript') !== -1 || contentType.indexOf('json') !== -1) {
const json = JSON.parse(responseText) as Payload;
if (typeof json === 'object' && 'error' in json && json.error && typeof json.error === 'object')
status.errorData = json.error;
} else status.responseText = responseText;
}
return request.parse(response);
})
.then((parsed) => {
// Notify callback (if possible).
if (callback) return callback(status, parsed);
return parsed;
})
.catch((error: Error) => {
const apiError = !(error instanceof PubNubAPIError) ? PubNubAPIError.create(error) : error;
// Notify callback (if possible).
if (callback) {
if (apiError.category !== Categories.PNCancelledCategory) {
this.logger.error('PubNub', () => ({
messageType: 'error',
message: apiError.toPubNubError(operation, 'REST API request processing error, check status for details'),
}));
}
return callback(apiError.toStatus(operation), null);
}
const pubNubError = apiError.toPubNubError(
operation,
'REST API request processing error, check status for details',
);
if (apiError.category !== Categories.PNCancelledCategory)
this.logger.error('PubNub', () => ({ messageType: 'error', message: pubNubError }));
throw pubNubError;
});
}
/**
* Unsubscribe from all channels and groups.
*
* @param [isOffline] - Whether `offline` presence should be notified or not.
*/
public destroy(isOffline: boolean = false): void {
this.logger.info('PubNub', 'Destroying PubNub client.');
if (process.env.SUBSCRIBE_MODULE !== 'disabled') {
if (this._globalSubscriptionSet) {
this._globalSubscriptionSet.invalidate(true);
this._globalSubscriptionSet = undefined;
}
Object.values(this.eventHandleCapable).forEach((subscription) => subscription.invalidate(true));
this.eventHandleCapable = {};
if (this.subscriptionManager) {
this.subscriptionManager.unsubscribeAll(isOffline);
this.subscriptionManager.disconnect();
} else if (this.eventEngine) this.eventEngine.unsubscribeAll(isOffline);
}
if (process.env.PRESENCE_MODULE !== 'disabled') {
if (this.presenceEventEngine) this.presenceEventEngine.leaveAll(isOffline);
}
}
/**
* Unsubscribe from all channels and groups.
*
* @deprecated Use {@link destroy} method instead.
*/
public stop(): void {
this.logger.warn('PubNub', "'stop' is deprecated, please use 'destroy' instead.");
this.destroy();
}
// endregion
// --------------------------------------------------------
// ---------------------- Publish API ---------------------
// --------------------------------------------------------
// region Publish API
/**
* Publish data to a specific channel.
*
* @param parameters - Request configuration parameters.
* @param callback - Request completion handler callback.
*/
public publish(parameters: Publish.PublishParameters, callback: ResultCallback<Publish.PublishResponse>): void;
/**
* Publish data to a specific channel.
*
* @param parameters - Request configuration parameters.
*
* @returns Asynchronous publish data response.
*/
public async publish(parameters: Publish.PublishParameters): Promise<Publish.PublishResponse>;
/**
* Publish data to a specific channel.
*
* @param parameters - Request configuration parameters.
* @param [callback] - Request completion handler callback.
*
* @returns Asynchronous publish data response or `void` in case if `callback` provided.
*/
async publish(
parameters: Publish.PublishParameters,
callback?: ResultCallback<Publish.PublishResponse>,
): Promise<Publish.PublishResponse | void> {
if (process.env.PUBLISH_MODULE !== 'disabled') {
this.logger.debug('PubNub', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Publish with parameters:',
}));
const isFireRequest = parameters.replicate === false && parameters.storeInHistory === false;
const request = new Publish.PublishRequest({
...parameters,
keySet: this._configuration.keySet,
crypto: this._configuration.getCryptoModule(),
});
const logResponse = (response: Publish.PublishResponse | null) => {
if (!response) return;
this.logger.debug(
'PubNub',
`${isFireRequest ? 'Fire' : 'Publish'} success with timetoken: ${response.timetoken}`,
);
};
if (callback)
return this.sendRequest(request, (status, response) => {
logResponse(response);
callback(status, response);
});
return this.sendRequest(request).then((response) => {
logResponse(response);
return response;
});
} else throw new Error('Publish error: publish module disabled');
}
// endregion
// --------------------------------------------------------
// ---------------------- Signal API ----------------------
// --------------------------------------------------------
// region Signal API
/**
* Signal data to a specific channel.
*
* @param parameters - Request configuration parameters.
* @param callback - Request completion handler callback.
*/
public signal(parameters: Signal.SignalParameters, callback: ResultCallback<Signal.SignalResponse>): void;
/**
* Signal data to a specific channel.
*
* @param parameters - Request configuration parameters.
*
* @returns Asynchronous signal data response.
*/
public async signal(parameters: Signal.SignalParameters): Promise<Signal.SignalResponse>;
/**
* Signal data to a specific channel.
*
* @param parameters - Request configuration parameters.
* @param [callback] - Request completion handler callback.
*
* @returns Asynchronous signal data response or `void` in case if `callback` provided.
*/
async signal(
parameters: Signal.SignalParameters,
callback?: ResultCallback<Signal.SignalResponse>,
): Promise<Signal.SignalResponse | void> {
if (process.env.PUBLISH_MODULE !== 'disabled') {
this.logger.debug('PubNub', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Signal with parameters:',
}));
const request = new Signal.SignalRequest({
...parameters,
keySet: this._configuration.keySet,
});
const logResponse = (response: Signal.SignalResponse | null) => {
if (!response) return;
this.logger.debug('PubNub', `Publish success with timetoken: ${response.timetoken}`);
};
if (callback)
return this.sendRequest(request, (status, response) => {
logResponse(response);
callback(status, response);
});
return this.sendRequest(request).then((response) => {
logResponse(response);
return response;
});
} else throw new Error('Publish error: publish module disabled');
}
// endregion
// --------------------------------------------------------
// ----------------------- Fire API ----------------------
// --------------------------------------------------------
// region Fire API
/**
* `Fire` a data to a specific channel.
*
* @param parameters - Request configuration parameters.
* @param callback - Request completion handler callback.
*
* @deprecated Use {@link publish} method instead.
*/
public fire(parameters: Publish.PublishParameters, callback: ResultCallback<Publish.PublishResponse>): void;
/**
* `Fire` a data to a specific channel.
*
* @param parameters - Request configuration parameters.
*
* @returns Asynchronous signal data response.
*
* @deprecated Use {@link publish} method instead.
*/
public async fire(parameters: Publish.PublishParameters): Promise<Publish.PublishResponse>;
/**
* `Fire` a data to a specific channel.
*
* @param parameters - Request configuration parameters.
* @param [callback] - Request completion handler callback.
*
* @returns Asynchronous signal data response or `void` in case if `callback` provided.
*
* @deprecated Use {@link publish} method instead.
*/
async fire(
parameters: Publish.PublishParameters,
callback?: ResultCallback<Publish.PublishResponse>,
): Promise<Publish.PublishResponse | void> {
this.logger.debug('PubNub', () => ({
messageType: 'object',
message: { ...parameters },
details: 'Fire with parameters:',
}));
callback ??= () => {};
return this.publish({ ...parameters, replicate: false, storeInHistory: false }, callback);
}
// endregion
// --------------------------------------------------------
// -------------------- Subscribe API ---------------------
// --------------------------------------------------------
// region Subscribe API
/**
* Global subscription set which supports legacy subscription interface.
*
* @returns Global subscription set.
*
* @internal
*/
private get globalSubscriptionSet() {
if (!this._globalSubscriptionSet) this._globalSubscriptionSet = this.subscriptionSet({});
return this._globalSubscriptionSet;
}
/**
* Subscription-based current timetoken.
*
* @returns Timetoken based on current timetoken plus diff between current and loop start time.
*
* @internal
*/
get subscriptionTimetoken(): string | undefined {
if (process.env.SUBSCRIBE_MODULE !== 'disabled') {
if (this.subscriptionManager) return this.subscriptionManager.subscriptionTimetoken;
else if (this.eventEngine) return this.eventEngine.subscriptionTimetoken;
}
return undefined;
}
/**
* Get list of channels on which PubNub client currently subscribed.
*
* @returns List of active channels.
*/
public getSubscribedChannels(): string[] {
if (process.env.SUBSCRIBE_MODULE !== 'disabled') {
if (this.subscriptionManager) return this.subscriptionManager.subscribedChannels;
else if (this.eventEngine) return this.eventEngine.getSubscribedChannels();
} else throw new Error('Subscription error: subscription module disabled');
return [];
}
/**
* Get list of channel groups on which PubNub client currently subscribed.
*
* @returns List of active channel groups.
*/
public getSubscribedChannelGroups(): string[] {
if (process.env.SUBSCRIBE_MODULE !== 'disabled') {
if (this.subscriptionManager) return this.subscriptionManager.subscribedChannelGroups;
else if (this.eventEngine) return this.eventEngine.getSubscribedChannelGroups();
} else throw new Error('Subscription error: subscription module disabled');
return [];
}
/**
* Register an events handler object ({@link Subscription} or {@link SubscriptionSet}) with an active subscription.
*
* @param subscription - {@link Subscription} or {@link SubscriptionSet} object.
* @param [cursor] - Subscription catchup timetoken.
* @param [subscriptions] - List of subscriptions for partial subscription loop update.
*
* @internal
*/
public registerEventHandleCapable(
subscription: SubscriptionBase,
cursor?: Subscription.SubscriptionCursor,
subscriptions?: EventHandleCapable[],
) {
if (process.env.SUBSCRIBE_MODULE !== 'disabled') {
this.logger.trace('PubNub', () => ({
messageType: 'object',
message: {
subscription: subscription,
...(cursor ? { cursor } : []),
...(subscriptions ? { subscriptions } : {}),
},
details: `Register event handle capable:`,
}));
if (!this.eventHandleCapable[subscription.state.id])
this.eventHandleCapable[subscription.state.id] = subscription;
let subscriptionInput: SubscriptionInput;
if (!subscriptions || subscriptions.length === 0) subscriptionInput = subscription.subscriptionInput(false);
else {
subscriptionInput = new SubscriptionInput({});
subscriptions.forEach((subscription) => subscriptionInput.add(subscription.subscriptionInput(false)));
}
const parameters: Subscription.SubscribeParameters = {};
parameters.channels = subscriptionInput.channels;
parameters.channelGroups = subscriptionInput.channelGroups;
if (cursor) parameters.timetoken = cursor.t