UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

1,497 lines (1,331 loc) 181 kB
/** * 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