diff --git a/.pubnub.yml b/.pubnub.yml index 68725335d..60fce39be 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,5 +1,18 @@ --- changelog: + - date: 2025-08-25 + version: v9.9.0 + changes: + - type: bug + text: "Resolved the issue because of which requests that were too early received a response and still have been sent." + - type: improvement + text: "Decouple and re-organize `SharedWorker` code for better maintainability." + - type: improvement + text: "Additional query parameter (removed before sending) is added for requests triggered by user and state will be updated only for these requests." + - type: improvement + text: "Log entry timestamp will be altered on millisecond if multiple log entries have similar timestamp (logged in fraction of nanoseconds)." + - type: improvement + text: "Change the condition that is used to identify whether the `offline` detection timer has been suspended by the browser or not before trying to evict `offline` PubNub clients." - date: 2025-08-07 version: v9.8.4 changes: @@ -1313,7 +1326,7 @@ supported-platforms: - 'Ubuntu 14.04 and up' - 'Windows 7 and up' version: 'Pubnub Javascript for Node' -version: '9.8.4' +version: '9.9.0' sdks: - full-name: PubNub Javascript SDK short-name: Javascript @@ -1329,7 +1342,7 @@ sdks: - distribution-type: source distribution-repository: GitHub release package-name: pubnub.js - location: https://github.com/pubnub/javascript/archive/refs/tags/v9.8.4.zip + location: https://github.com/pubnub/javascript/archive/refs/tags/v9.9.0.zip requires: - name: 'agentkeepalive' min-version: '3.5.2' @@ -2000,7 +2013,7 @@ sdks: - distribution-type: library distribution-repository: GitHub release package-name: pubnub.js - location: https://github.com/pubnub/javascript/releases/download/v9.8.4/pubnub.9.8.4.js + location: https://github.com/pubnub/javascript/releases/download/v9.9.0/pubnub.9.9.0.js requires: - name: 'agentkeepalive' min-version: '3.5.2' diff --git a/CHANGELOG.md b/CHANGELOG.md index b1277289b..c47f2119e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## v9.9.0 +August 25 2025 + +#### Fixed +- Resolved the issue because of which requests that were too early received a response and still have been sent. + +#### Modified +- Decouple and re-organize `SharedWorker` code for better maintainability. +- Additional query parameter (removed before sending) is added for requests triggered by user and state will be updated only for these requests. +- Log entry timestamp will be altered on millisecond if multiple log entries have similar timestamp (logged in fraction of nanoseconds). +- Change the condition that is used to identify whether the `offline` detection timer has been suspended by the browser or not before trying to evict "offline" PubNub clients. + ## v9.8.4 August 07 2025 diff --git a/README.md b/README.md index a672d84a1..9a11229f6 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Watch [Getting Started with PubNub JS SDK](https://app.dashcam.io/replay/64ee0d2 npm install pubnub ``` * or download one of our builds from our CDN: - * https://cdn.pubnub.com/sdk/javascript/pubnub.9.8.4.js - * https://cdn.pubnub.com/sdk/javascript/pubnub.9.8.4.min.js + * https://cdn.pubnub.com/sdk/javascript/pubnub.9.9.0.js + * https://cdn.pubnub.com/sdk/javascript/pubnub.9.9.0.min.js 2. Configure your keys: diff --git a/dist/web/pubnub.js b/dist/web/pubnub.js index 8a19f3174..6d4af727f 100644 --- a/dist/web/pubnub.js +++ b/dist/web/pubnub.js @@ -919,6 +919,13 @@ * PubNub client unexpectedly disconnected from the real-time updates streams. */ StatusCategory["PNDisconnectedUnexpectedlyCategory"] = "PNDisconnectedUnexpectedlyCategory"; + // -------------------------------------------------------- + // ------------------ Shared worker events ---------------- + // -------------------------------------------------------- + /** + * SDK will announce when newer shared worker will be 'noticed'. + */ + StatusCategory["PNSharedWorkerUpdatedCategory"] = "PNSharedWorkerUpdatedCategory"; })(StatusCategory || (StatusCategory = {})); var StatusCategory$1 = StatusCategory; @@ -3585,6 +3592,7 @@ clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, userId: this.configuration.userId, + workerLogLevel: this.configuration.workerLogLevel, }); } /** @@ -3600,6 +3608,7 @@ clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, userId: this.configuration.userId, + workerLogLevel: this.configuration.workerLogLevel, }); } /** @@ -3614,6 +3623,7 @@ clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, userId: this.configuration.userId, + workerLogLevel: this.configuration.workerLogLevel, }; // Trigger request processing by Service Worker. this.parsedAccessToken(token) @@ -3623,6 +3633,17 @@ }) .then(() => this.scheduleEventPost(updateEvent)); } + /** + * Disconnect client and terminate ongoing long-poll requests (if needed). + */ + disconnect() { + this.scheduleEventPost({ + type: 'client-disconnect', + clientIdentifier: this.configuration.clientIdentifier, + subscriptionKey: this.configuration.subscriptionKey, + workerLogLevel: this.configuration.workerLogLevel, + }); + } /** * Terminate all ongoing long-poll requests. */ @@ -3631,6 +3652,7 @@ type: 'client-unregister', clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, + workerLogLevel: this.configuration.workerLogLevel, }); } makeSendable(req) { @@ -3644,6 +3666,7 @@ clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, request: req, + workerLogLevel: this.configuration.workerLogLevel, }; if (req.cancellable) { controller = { @@ -3653,6 +3676,7 @@ clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, identifier: req.identifier, + workerLogLevel: this.configuration.workerLogLevel, }; // Cancel active request with specified identifier. this.scheduleEventPost(cancelRequest); @@ -3758,9 +3782,17 @@ heartbeatInterval: this.configuration.heartbeatInterval, workerOfflineClientsCheckInterval: this.configuration.workerOfflineClientsCheckInterval, workerUnsubscribeOfflineClients: this.configuration.workerUnsubscribeOfflineClients, - workerLogVerbosity: this.configuration.workerLogVerbosity, + workerLogLevel: this.configuration.workerLogLevel, }, true); this.subscriptionWorker.port.onmessage = (event) => this.handleWorkerEvent(event); + if (this.shouldAnnounceNewerSharedWorkerVersionAvailability()) + localStorage.setItem('PNSubscriptionSharedWorkerVersion', this.configuration.sdkVersion); + window.addEventListener('storage', (event) => { + if (event.key !== 'PNSubscriptionSharedWorkerVersion' || !event.newValue) + return; + if (this._emitStatus && this.isNewerSharedWorkerVersion(event.newValue)) + this._emitStatus({ error: false, category: StatusCategory$1.PNSharedWorkerUpdatedCategory }); + }); } handleWorkerEvent(event) { const { data } = event; @@ -3798,7 +3830,12 @@ } else if (data.type === 'shared-worker-ping') { const { subscriptionKey, clientIdentifier } = this.configuration; - this.scheduleEventPost({ type: 'client-pong', subscriptionKey, clientIdentifier }); + this.scheduleEventPost({ + type: 'client-pong', + subscriptionKey, + clientIdentifier, + workerLogLevel: this.configuration.workerLogLevel, + }); } else if (data.type === 'request-process-success' || data.type === 'request-process-error') { if (this.callbacks.has(data.identifier)) { @@ -3933,6 +3970,29 @@ } return new PubNubAPIError(message, category, 0, new Error(message)); } + /** + * Check whether current subscription `SharedWorker` version should be announced or not. + * + * @returns `true` if local storage is empty (only newer version will add value) or stored version is smaller than + * current. + */ + shouldAnnounceNewerSharedWorkerVersionAvailability() { + const version = localStorage.getItem('PNSubscriptionSharedWorkerVersion'); + if (!version) + return true; + return !this.isNewerSharedWorkerVersion(version); + } + /** + * Check whether current subscription `SharedWorker` version should be announced or not. + * + * @param version - Stored (received on init or event) version of subscription shared worker. + * @returns `true` if provided `version` is newer than current client version. + */ + isNewerSharedWorkerVersion(version) { + const [currentMajor, currentMinor, currentPatch] = this.configuration.sdkVersion.split('.').map(Number); + const [storedMajor, storedMinor, storedPatch] = version.split('.').map(Number); + return storedMajor > currentMajor || storedMinor > currentMinor || storedPatch > currentPatch; + } } /** @@ -4455,7 +4515,7 @@ */ class ConsoleLogger { /** - * Process a `trace` level message. + * Process a `debug` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -4463,7 +4523,7 @@ this.log(message); } /** - * Process a `debug` level message. + * Process a `error` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -4479,7 +4539,7 @@ this.log(message); } /** - * Process a `warn` level message. + * Process a `trace` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -4487,13 +4547,21 @@ this.log(message); } /** - * Process an `error` level message. + * Process an `warn` level message. * * @param message - Message which should be handled by custom logger implementation. */ warn(message) { this.log(message); } + /** + * Stringify logger object. + * + * @returns Serialized logger object. + */ + toString() { + return `ConsoleLogger {}`; + } /** * Process log message object. * @@ -5026,6 +5094,15 @@ * @internal */ constructor(pubNubId, minLogLevel, loggers) { + /** + * Keeping track of previous entry timestamp. + * + * This information will be used to make sure that multiple sequential entries doesn't have same timestamp. Adjustment + * on .001 will be done to make it possible to properly stort entries. + * + * @internal + */ + this.previousEntryTimestamp = 0; this.pubNubId = pubNubId; this.minLogLevel = minLogLevel; this.loggers = loggers; @@ -5108,8 +5185,15 @@ // Check whether a log message should be handled at all or not. if (logLevel < this.minLogLevel || this.loggers.length === 0) return; + const date = new Date(); + if (date.getTime() <= this.previousEntryTimestamp) { + this.previousEntryTimestamp++; + date.setTime(this.previousEntryTimestamp); + } + else + this.previousEntryTimestamp = date.getTime(); const level = LogLevel[logLevel].toLowerCase(); - const message = Object.assign({ timestamp: new Date(), pubNubId: this.pubNubId, level: logLevel, minimumLevel: this.minLogLevel, location }, (typeof messageFactory === 'function' ? messageFactory() : { messageType: 'text', message: messageFactory })); + const message = Object.assign({ timestamp: date, pubNubId: this.pubNubId, level: logLevel, minimumLevel: this.minLogLevel, location }, (typeof messageFactory === 'function' ? messageFactory() : { messageType: 'text', message: messageFactory })); this.loggers.forEach((logger) => logger[level](message)); } } @@ -5303,6 +5387,10 @@ getUseRandomIVs() { return base.useRandomIVs; }, + isSharedWorkerEnabled() { + // @ts-expect-error: Access field from web-based SDK configuration. + return base.sdkFamily === 'Web' && base['subscriptionWorkerUrl']; + }, getKeepPresenceChannelsInPresenceRequests() { // @ts-expect-error: Access field from web-based SDK configuration. return base.sdkFamily === 'Web' && base['subscriptionWorkerUrl']; @@ -5333,7 +5421,7 @@ return base.PubNubFile; }, get version() { - return '9.8.4'; + return '9.9.0'; }, getVersion() { return this.version; @@ -6653,8 +6741,10 @@ return `/v2/subscribe/${subscribeKey}/${encodeNames((_a = channels === null || channels === void 0 ? void 0 : channels.sort()) !== null && _a !== void 0 ? _a : [], ',')}/0`; } get queryParameters() { - const { channelGroups, filterExpression, heartbeat, state, timetoken, region } = this.parameters; + const { channelGroups, filterExpression, heartbeat, state, timetoken, region, onDemand } = this.parameters; const query = {}; + if (onDemand) + query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) @@ -7023,6 +7113,13 @@ this.subscribeCall = subscribeCall; this.heartbeatCall = heartbeatCall; this.leaveCall = leaveCall; + /** + * Whether user code in event handlers requested disconnection or not. + * + * Won't continue subscription loop if user requested disconnection/unsubscribe from all in response to received + * event. + */ + this.disconnectedWhileHandledEvent = false; configuration.logger().trace('SubscriptionManager', 'Create manager.'); this.reconnectionManager = new ReconnectionManager(time); this.dedupingManager = new DedupingManager(this.configuration); @@ -7068,6 +7165,9 @@ // endregion // region Subscription disconnect() { + // Potentially called during received events handling. + // Mark to prevent subscription loop continuation in subscribe response handler. + this.disconnectedWhileHandledEvent = true; this.stopSubscribeLoop(); this.stopHeartbeatTimer(); this.reconnectionManager.stopPolling(); @@ -7153,6 +7253,22 @@ // There is no need to unsubscribe to empty list of data sources. if (actualChannels.size === 0 && actualChannelGroups.size === 0) return; + const lastTimetoken = this.lastTimetoken; + const currentTimetoken = this.currentTimetoken; + if (Object.keys(this.channels).length === 0 && + Object.keys(this.presenceChannels).length === 0 && + Object.keys(this.channelGroups).length === 0 && + Object.keys(this.presenceChannelGroups).length === 0) { + this.lastTimetoken = '0'; + this.currentTimetoken = '0'; + this.referenceTimetoken = null; + this.storedTimetoken = null; + this.region = null; + this.reconnectionManager.stopPolling(); + } + this.reconnect(true); + // Send leave request after long-poll connection closed and loop restarted (the same way as it happens in new + // subscription flow). if (this.configuration.suppressLeaveEvents === false && !isOffline) { channelGroups = Array.from(actualChannelGroups); channels = Array.from(actualChannels); @@ -7168,23 +7284,13 @@ else if ('message' in status && typeof status.message === 'string') errorMessage = status.message; } - this.emitStatus(Object.assign(Object.assign({}, restOfStatus), { error: errorMessage !== null && errorMessage !== void 0 ? errorMessage : false, affectedChannels: channels, affectedChannelGroups: channelGroups, currentTimetoken: this.currentTimetoken, lastTimetoken: this.lastTimetoken })); + this.emitStatus(Object.assign(Object.assign({}, restOfStatus), { error: errorMessage !== null && errorMessage !== void 0 ? errorMessage : false, affectedChannels: channels, affectedChannelGroups: channelGroups, currentTimetoken, + lastTimetoken })); }); } - if (Object.keys(this.channels).length === 0 && - Object.keys(this.presenceChannels).length === 0 && - Object.keys(this.channelGroups).length === 0 && - Object.keys(this.presenceChannelGroups).length === 0) { - this.lastTimetoken = '0'; - this.currentTimetoken = '0'; - this.referenceTimetoken = null; - this.storedTimetoken = null; - this.region = null; - this.reconnectionManager.stopPolling(); - } - this.reconnect(true); } unsubscribeAll(isOffline = false) { + this.disconnectedWhileHandledEvent = true; this.unsubscribe({ channels: this.subscribedChannels, channelGroups: this.subscribedChannelGroups, @@ -7199,6 +7305,7 @@ * @internal */ startSubscribeLoop(restartOnUnsubscribe = false) { + this.disconnectedWhileHandledEvent = false; this.stopSubscribeLoop(); const channelGroups = [...Object.keys(this.channelGroups)]; const channels = [...Object.keys(this.channels)]; @@ -7207,8 +7314,8 @@ // There is no need to start subscription loop for an empty list of data sources. if (channels.length === 0 && channelGroups.length === 0) return; - this.subscribeCall(Object.assign(Object.assign({ channels, - channelGroups, state: this.presenceState, heartbeat: this.configuration.getPresenceTimeout(), timetoken: this.currentTimetoken }, (this.region !== null ? { region: this.region } : {})), (this.configuration.filterExpression ? { filterExpression: this.configuration.filterExpression } : {})), (status, result) => { + this.subscribeCall(Object.assign(Object.assign(Object.assign({ channels, + channelGroups, state: this.presenceState, heartbeat: this.configuration.getPresenceTimeout(), timetoken: this.currentTimetoken }, (this.region !== null ? { region: this.region } : {})), (this.configuration.filterExpression ? { filterExpression: this.configuration.filterExpression } : {})), { onDemand: !this.subscriptionStatusAnnounced || restartOnUnsubscribe }), (status, result) => { this.processSubscribeResponse(status, result); }); if (!restartOnUnsubscribe && this.configuration.useSmartHeartbeat) @@ -7339,7 +7446,10 @@ this.emitStatus(errorStatus); } this.region = result.cursor.region; - this.startSubscribeLoop(); + if (!this.disconnectedWhileHandledEvent) + this.startSubscribeLoop(); + else + this.disconnectedWhileHandledEvent = false; } // endregion // region Presence @@ -8825,9 +8935,10 @@ * * @internal */ - const handshake = createManagedEffect('HANDSHAKE', (channels, groups) => ({ + const handshake = createManagedEffect('HANDSHAKE', (channels, groups, onDemand) => ({ channels, groups, + onDemand, })); /** * Real-time updates receive effect. @@ -8837,7 +8948,12 @@ * * @internal */ - const receiveMessages = createManagedEffect('RECEIVE_MESSAGES', (channels, groups, cursor) => ({ channels, groups, cursor })); + const receiveMessages = createManagedEffect('RECEIVE_MESSAGES', (channels, groups, cursor, onDemand) => ({ + channels, + groups, + cursor, + onDemand, + })); /** * Emit real-time updates effect. * @@ -8971,7 +9087,7 @@ UnsubscribedState.on(subscriptionChange.type, (_, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return UnsubscribedState.with(undefined); - return HandshakingState.with({ channels: payload.channels, groups: payload.groups }); + return HandshakingState.with({ channels: payload.channels, groups: payload.groups, onDemand: true }); }); UnsubscribedState.on(restore.type, (_, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) @@ -8980,6 +9096,7 @@ channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region }, + onDemand: true, }); }); @@ -9002,7 +9119,7 @@ return UnsubscribedState.with(undefined); return HandshakeStoppedState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); }); - HandshakeStoppedState.on(reconnect.type, (context, { payload }) => HandshakingState.with(Object.assign(Object.assign({}, context), { cursor: payload.cursor || context.cursor }))); + HandshakeStoppedState.on(reconnect.type, (context, { payload }) => HandshakingState.with(Object.assign(Object.assign({}, context), { cursor: payload.cursor || context.cursor, onDemand: true }))); HandshakeStoppedState.on(restore.type, (context, { payload }) => { var _a; if (payload.channels.length === 0 && payload.groups.length === 0) @@ -9032,9 +9149,14 @@ HandshakeFailedState.on(subscriptionChange.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return UnsubscribedState.with(undefined); - return HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); - HandshakeFailedState.on(reconnect.type, (context, { payload }) => HandshakingState.with(Object.assign(Object.assign({}, context), { cursor: payload.cursor || context.cursor }))); + HandshakeFailedState.on(reconnect.type, (context, { payload }) => HandshakingState.with(Object.assign(Object.assign({}, context), { cursor: payload.cursor || context.cursor, onDemand: true }))); HandshakeFailedState.on(restore.type, (context, { payload }) => { var _a, _b; if (payload.channels.length === 0 && payload.groups.length === 0) @@ -9046,6 +9168,7 @@ timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region ? payload.cursor.region : ((_b = (_a = context === null || context === void 0 ? void 0 : context.cursor) === null || _a === void 0 ? void 0 : _a.region) !== null && _b !== void 0 ? _b : 0), }, + onDemand: true, }); }); HandshakeFailedState.on(unsubscribeAll.type, (_) => UnsubscribedState.with()); @@ -9064,12 +9187,17 @@ * @internal */ const HandshakingState = new State('HANDSHAKING'); - HandshakingState.onEnter((context) => handshake(context.channels, context.groups)); + HandshakingState.onEnter((context) => { var _a; return handshake(context.channels, context.groups, (_a = context.onDemand) !== null && _a !== void 0 ? _a : false); }); HandshakingState.onExit(() => handshake.cancel); HandshakingState.on(subscriptionChange.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return UnsubscribedState.with(undefined); - return HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); HandshakingState.on(handshakeSuccess.type, (context, { payload }) => { var _a, _b, _c, _d, _e; @@ -9118,6 +9246,7 @@ channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region || ((_a = context === null || context === void 0 ? void 0 : context.cursor) === null || _a === void 0 ? void 0 : _a.region) || 0 }, + onDemand: true, }); }); HandshakingState.on(unsubscribeAll.type, (_) => UnsubscribedState.with()); @@ -9159,6 +9288,7 @@ timetoken: !!payload.cursor.timetoken ? (_a = payload.cursor) === null || _a === void 0 ? void 0 : _a.timetoken : context.cursor.timetoken, region: payload.cursor.region || context.cursor.region, }, + onDemand: true, }); }); ReceiveStoppedState.on(unsubscribeAll.type, () => UnsubscribedState.with(undefined)); @@ -9186,12 +9316,18 @@ timetoken: !!payload.cursor.timetoken ? (_a = payload.cursor) === null || _a === void 0 ? void 0 : _a.timetoken : context.cursor.timetoken, region: payload.cursor.region || context.cursor.region, }, + onDemand: true, }); }); ReceiveFailedState.on(subscriptionChange.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return UnsubscribedState.with(undefined); - return HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); ReceiveFailedState.on(restore.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) @@ -9200,6 +9336,7 @@ channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region || context.cursor.region }, + onDemand: true, }); }); ReceiveFailedState.on(unsubscribeAll.type, (_) => UnsubscribedState.with(undefined)); @@ -9217,7 +9354,7 @@ * @internal */ const ReceivingState = new State('RECEIVING'); - ReceivingState.onEnter((context) => receiveMessages(context.channels, context.groups, context.cursor)); + ReceivingState.onEnter((context) => { var _a; return receiveMessages(context.channels, context.groups, context.cursor, (_a = context.onDemand) !== null && _a !== void 0 ? _a : false); }); ReceivingState.onExit(() => receiveMessages.cancel); ReceivingState.on(receiveSuccess.type, (context, { payload }) => ReceivingState.with({ channels: context.channels, @@ -9242,6 +9379,7 @@ groups: payload.groups, cursor: context.cursor, referenceTimetoken: context.referenceTimetoken, + onDemand: true, }, [ emitStatus({ category: StatusCategory$1.PNSubscriptionChangedCategory, @@ -9259,6 +9397,7 @@ groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region || context.cursor.region }, referenceTimetoken: referenceSubscribeTimetoken(context.cursor.timetoken, `${payload.cursor.timetoken}`, context.referenceTimetoken), + onDemand: true, }, [ emitStatus({ category: StatusCategory$1.PNSubscriptionChangedCategory, @@ -9311,7 +9450,7 @@ this.on(handshake.type, asyncHandler((payload_1, abortSignal_1, _a) => __awaiter(this, [payload_1, abortSignal_1, _a], void 0, function* (payload, abortSignal, { handshake, presenceState, config }) { abortSignal.throwIfAborted(); try { - const result = yield handshake(Object.assign({ abortSignal: abortSignal, channels: payload.channels, channelGroups: payload.groups, filterExpression: config.filterExpression }, (config.maintainPresenceState && { state: presenceState }))); + const result = yield handshake(Object.assign(Object.assign({ abortSignal: abortSignal, channels: payload.channels, channelGroups: payload.groups, filterExpression: config.filterExpression }, (config.maintainPresenceState && { state: presenceState })), { onDemand: payload.onDemand })); return engine.transition(handshakeSuccess(result)); } catch (e) { @@ -9332,6 +9471,7 @@ timetoken: payload.cursor.timetoken, region: payload.cursor.region, filterExpression: config.filterExpression, + onDemand: payload.onDemand, }); engine.transition(receiveSuccess(result.cursor, result.messages)); } @@ -9660,8 +9800,11 @@ return `/v2/subscribe/${subscribeKey}/${encodeNames(channels.sort(), ',')}/0`; } get queryParameters() { - const { channelGroups, filterExpression, timetoken, region } = this.parameters; + const { channelGroups, filterExpression, timetoken, region, onDemand } = this + .parameters; const query = { ee: '' }; + if (onDemand) + query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) @@ -9699,8 +9842,11 @@ return `/v2/subscribe/${subscribeKey}/${encodeNames(channels.sort(), ',')}/0`; } get queryParameters() { - const { channelGroups, filterExpression, state } = this.parameters; + const { channelGroups, filterExpression, state, onDemand } = this + .parameters; const query = { ee: '' }; + if (onDemand) + query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) @@ -15399,6 +15545,9 @@ * 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. * @@ -16126,6 +16275,9 @@ */ makeSubscribe(parameters, callback) { { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) + parameters.onDemand = false; const request = new SubscribeRequest(Object.assign(Object.assign({}, parameters), { keySet: this._configuration.keySet, crypto: this._configuration.getCryptoModule(), getFileUrl: this.getFileUrl.bind(this) })); this.sendRequest(request, (status, result) => { var _a; @@ -16275,6 +16427,9 @@ subscribeHandshake(parameters) { return __awaiter(this, void 0, void 0, function* () { { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) + parameters.onDemand = false; const request = new HandshakeSubscribeRequest(Object.assign(Object.assign({}, parameters), { keySet: this._configuration.keySet, crypto: this._configuration.getCryptoModule(), getFileUrl: this.getFileUrl.bind(this) })); const abortUnsubscribe = parameters.abortSignal.subscribe((err) => { request.abort('Cancel subscribe handshake request'); @@ -16303,6 +16458,9 @@ subscribeReceiveMessages(parameters) { return __awaiter(this, void 0, void 0, function* () { { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) + parameters.onDemand = false; const request = new ReceiveMessagesSubscribeRequest(Object.assign(Object.assign({}, parameters), { keySet: this._configuration.keySet, crypto: this._configuration.getCryptoModule(), getFileUrl: this.getFileUrl.bind(this) })); const abortUnsubscribe = parameters.abortSignal.subscribe((err) => { request.abort('Cancel long-poll subscribe request'); @@ -16768,12 +16926,10 @@ // Filtering out presence channels and groups. let { channels, channelGroups } = parameters; // Remove `-pnpres` channels / groups if they not acceptable in the current PubNub client configuration. - if (!this._configuration.getKeepPresenceChannelsInPresenceRequests()) { - if (channelGroups) - channelGroups = channelGroups.filter((channelGroup) => !channelGroup.endsWith('-pnpres')); - if (channels) - channels = channels.filter((channel) => !channel.endsWith('-pnpres')); - } + if (channelGroups) + channelGroups = channelGroups.filter((channelGroup) => !channelGroup.endsWith('-pnpres')); + if (channels) + channels = channels.filter((channel) => !channel.endsWith('-pnpres')); // Complete immediately request only for presence channels. if ((channelGroups !== null && channelGroups !== void 0 ? channelGroups : []).length === 0 && (channels !== null && channels !== void 0 ? channels : []).length === 0) { const responseStatus = { @@ -18055,6 +18211,15 @@ return cryptoModule; } }); + if (configuration.subscriptionWorkerLogVerbosity) + configuration.subscriptionWorkerLogLevel = LogLevel.Debug; + else if (configuration.subscriptionWorkerLogLevel === undefined) + configuration.subscriptionWorkerLogLevel = LogLevel.None; + if (configuration.subscriptionWorkerLogVerbosity !== undefined) { + clientConfiguration + .logger() + .warn('Configuration', "'subscriptionWorkerLogVerbosity' is deprecated. Use 'subscriptionWorkerLogLevel' instead."); + } { // Ensure that the logger has been passed to the user-provided crypto module. if (clientConfiguration.getCryptoModule()) @@ -18102,7 +18267,7 @@ announceFailedHeartbeats: clientConfiguration.announceFailedHeartbeats, workerOfflineClientsCheckInterval: platformConfiguration.subscriptionWorkerOfflineClientsCheckInterval, workerUnsubscribeOfflineClients: platformConfiguration.subscriptionWorkerUnsubscribeOfflineClients, - workerLogVerbosity: platformConfiguration.subscriptionWorkerLogVerbosity, + workerLogLevel: platformConfiguration.subscriptionWorkerLogLevel, tokenManager, transport, logger: clientConfiguration.logger(), @@ -18147,8 +18312,14 @@ this.onAuthenticationChange = authenticationChangeHandler; this.onUserIdChange = userIdChangeHandler; { - if (transport instanceof SubscriptionWorkerMiddleware) + if (transport instanceof SubscriptionWorkerMiddleware) { transport.emitStatus = this.emitStatus.bind(this); + const disconnect = this.disconnect.bind(this); + this.disconnect = (isOffline) => { + transport.disconnect(); + disconnect(); + }; + } } if ((_a = configuration.listenToBrowserNetworkEvents) !== null && _a !== void 0 ? _a : true) { window.addEventListener('offline', () => { diff --git a/dist/web/pubnub.min.js b/dist/web/pubnub.min.js index de4ef7501..0889b9da9 100644 --- a/dist/web/pubnub.min.js +++ b/dist/web/pubnub.min.js @@ -1,2 +1,2 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).PubNub=t()}(this,(function(){"use strict";var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s={exports:{}};!function(t){!function(e,s){var n=Math.pow(2,-24),r=Math.pow(2,32),i=Math.pow(2,53);var a={encode:function(e){var t,n=new ArrayBuffer(256),a=new DataView(n),o=0;function c(e){for(var s=n.byteLength,r=o+e;s>2,u=0;u>6),r.push(128|63&a)):a<55296?(r.push(224|a>>12),r.push(128|a>>6&63),r.push(128|63&a)):(a=(1023&a)<<10,a|=1023&t.charCodeAt(++n),a+=65536,r.push(240|a>>18),r.push(128|a>>12&63),r.push(128|a>>6&63),r.push(128|63&a))}return d(3,r.length),h(r);default:var p;if(Array.isArray(t))for(d(4,p=t.length),n=0;n>5!==e)throw"Invalid indefinite length element";return s}function y(e,t){for(var s=0;s>10),e.push(56320|1023&n))}}"function"!=typeof t&&(t=function(e){return e}),"function"!=typeof i&&(i=function(){return s});var m=function e(){var r,d,m=l(),f=m>>5,v=31&m;if(7===f)switch(v){case 25:return function(){var e=new ArrayBuffer(4),t=new DataView(e),s=h(),r=32768&s,i=31744&s,a=1023&s;if(31744===i)i=261120;else if(0!==i)i+=114688;else if(0!==a)return a*n;return t.setUint32(0,r<<16|i<<13|a<<13),t.getFloat32(0)}();case 26:return c(a.getFloat32(o),4);case 27:return c(a.getFloat64(o),8)}if((d=g(v))<0&&(f<2||6=0;)w+=d,S.push(u(d));var O=new Uint8Array(w),k=0;for(r=0;r=0;)y(C,d);else y(C,d);return String.fromCharCode.apply(null,C);case 4:var P;if(d<0)for(P=[];!p();)P.push(e());else for(P=new Array(d),r=0;re.toString())).join(", ")}]}`}}a.encoder=new TextEncoder,a.decoder=new TextDecoder;class o{static create(e){return new o(e)}constructor(e){let t,s,n,r;if(e instanceof File)r=e,n=e.name,s=e.type,t=e.size;else if("data"in e){const i=e.data;s=e.mimeType,n=e.name,r=new File([i],n,{type:s}),t=r.size}if(void 0===r)throw new Error("Couldn't construct a file out of supplied options.");if(void 0===n)throw new Error("Couldn't guess filename out of the options. Please provide one.");t&&(this.contentLength=t),this.mimeType=s,this.data=r,this.name=n}toBuffer(){return i(this,void 0,void 0,(function*(){throw new Error("This feature is only supported in Node.js environments.")}))}toArrayBuffer(){return i(this,void 0,void 0,(function*(){return new Promise(((e,t)=>{const s=new FileReader;s.addEventListener("load",(()=>{if(s.result instanceof ArrayBuffer)return e(s.result)})),s.addEventListener("error",(()=>t(s.error))),s.readAsArrayBuffer(this.data)}))}))}toString(){return i(this,void 0,void 0,(function*(){return new Promise(((e,t)=>{const s=new FileReader;s.addEventListener("load",(()=>{if("string"==typeof s.result)return e(s.result)})),s.addEventListener("error",(()=>{t(s.error)})),s.readAsBinaryString(this.data)}))}))}toStream(){return i(this,void 0,void 0,(function*(){throw new Error("This feature is only supported in Node.js environments.")}))}toFile(){return i(this,void 0,void 0,(function*(){return this.data}))}toFileUri(){return i(this,void 0,void 0,(function*(){throw new Error("This feature is only supported in React Native environments.")}))}toBlob(){return i(this,void 0,void 0,(function*(){return this.data}))}}o.supportsBlob="undefined"!=typeof Blob,o.supportsFile="undefined"!=typeof File,o.supportsBuffer=!1,o.supportsStream=!1,o.supportsString=!0,o.supportsArrayBuffer=!0,o.supportsEncryptFile=!0,o.supportsFileUri=!1;function c(e){const t=e.replace(/==?$/,""),s=Math.floor(t.length/4*3),n=new ArrayBuffer(s),r=new Uint8Array(n);let i=0;function a(){const e=t.charAt(i++),s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(e);if(-1===s)throw new Error(`Illegal character at ${i}: ${t.charAt(i-1)}`);return s}for(let e=0;e>4,c=(15&s)<<4|n>>2,u=(3&n)<<6|i;r[e]=o,64!=n&&(r[e+1]=c),64!=i&&(r[e+2]=u)}return n}function u(e){let t="";const s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(e),r=n.byteLength,i=r%3,a=r-i;let o,c,u,l,h;for(let e=0;e>18,c=(258048&h)>>12,u=(4032&h)>>6,l=63&h,t+=s[o]+s[c]+s[u]+s[l];return 1==i?(h=n[a],o=(252&h)>>2,c=(3&h)<<4,t+=s[o]+s[c]+"=="):2==i&&(h=n[a]<<8|n[a+1],o=(64512&h)>>10,c=(1008&h)>>4,u=(15&h)<<2,t+=s[o]+s[c]+s[u]+"="),t}var l;!function(e){e.PNNetworkIssuesCategory="PNNetworkIssuesCategory",e.PNTimeoutCategory="PNTimeoutCategory",e.PNCancelledCategory="PNCancelledCategory",e.PNBadRequestCategory="PNBadRequestCategory",e.PNAccessDeniedCategory="PNAccessDeniedCategory",e.PNValidationErrorCategory="PNValidationErrorCategory",e.PNAcknowledgmentCategory="PNAcknowledgmentCategory",e.PNMalformedResponseCategory="PNMalformedResponseCategory",e.PNServerErrorCategory="PNServerErrorCategory",e.PNUnknownCategory="PNUnknownCategory",e.PNNetworkUpCategory="PNNetworkUpCategory",e.PNNetworkDownCategory="PNNetworkDownCategory",e.PNReconnectedCategory="PNReconnectedCategory",e.PNConnectedCategory="PNConnectedCategory",e.PNSubscriptionChangedCategory="PNSubscriptionChangedCategory",e.PNRequestMessageCountExceededCategory="PNRequestMessageCountExceededCategory",e.PNDisconnectedCategory="PNDisconnectedCategory",e.PNConnectionErrorCategory="PNConnectionErrorCategory",e.PNDisconnectedUnexpectedlyCategory="PNDisconnectedUnexpectedlyCategory"}(l||(l={}));var h=l;class d extends Error{constructor(e,t){super(e),this.status=t,this.name="PubNubError",this.message=e,Object.setPrototypeOf(this,new.target.prototype)}}function p(e,t){var s;return null!==(s=e.statusCode)&&void 0!==s||(e.statusCode=0),Object.assign(Object.assign({},e),{statusCode:e.statusCode,category:t,error:!0})}function g(e,t){return p(Object.assign(Object.assign({message:"Unable to deserialize service response"},void 0!==e?{responseText:e}:{}),void 0!==t?{statusCode:t}:{}),h.PNMalformedResponseCategory)}var b,y,m,f,v,S=S||function(e){var t={},s=t.lib={},n=function(){},r=s.Base={extend:function(e){n.prototype=this;var t=new n;return e&&t.mixIn(e),t.hasOwnProperty("init")||(t.init=function(){t.$super.init.apply(this,arguments)}),t.init.prototype=t,t.$super=this,t},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}},i=s.WordArray=r.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||o).stringify(this)},concat:function(e){var t=this.words,s=e.words,n=this.sigBytes;if(e=e.sigBytes,this.clamp(),n%4)for(var r=0;r>>2]|=(s[r>>>2]>>>24-r%4*8&255)<<24-(n+r)%4*8;else if(65535>>2]=s[r>>>2];else t.push.apply(t,s);return this.sigBytes+=e,this},clamp:function(){var t=this.words,s=this.sigBytes;t[s>>>2]&=4294967295<<32-s%4*8,t.length=e.ceil(s/4)},clone:function(){var e=r.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var s=[],n=0;n>>2]>>>24-n%4*8&255;s.push((r>>>4).toString(16)),s.push((15&r).toString(16))}return s.join("")},parse:function(e){for(var t=e.length,s=[],n=0;n>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new i.init(s,t/2)}},c=a.Latin1={stringify:function(e){var t=e.words;e=e.sigBytes;for(var s=[],n=0;n>>2]>>>24-n%4*8&255));return s.join("")},parse:function(e){for(var t=e.length,s=[],n=0;n>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new i.init(s,t)}},u=a.Utf8={stringify:function(e){try{return decodeURIComponent(escape(c.stringify(e)))}catch(e){throw Error("Malformed UTF-8 data")}},parse:function(e){return c.parse(unescape(encodeURIComponent(e)))}},l=s.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new i.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=u.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var s=this._data,n=s.words,r=s.sigBytes,a=this.blockSize,o=r/(4*a);if(t=(o=t?e.ceil(o):e.max((0|o)-this._minBufferSize,0))*a,r=e.min(4*t,r),t){for(var c=0;cu;){var l;e:{l=c;for(var h=e.sqrt(l),d=2;d<=h;d++)if(!(l%d)){l=!1;break e}l=!0}l&&(8>u&&(i[u]=o(e.pow(c,.5))),a[u]=o(e.pow(c,1/3)),u++),c++}var p=[];r=r.SHA256=n.extend({_doReset:function(){this._hash=new s.init(i.slice(0))},_doProcessBlock:function(e,t){for(var s=this._hash.words,n=s[0],r=s[1],i=s[2],o=s[3],c=s[4],u=s[5],l=s[6],h=s[7],d=0;64>d;d++){if(16>d)p[d]=0|e[t+d];else{var g=p[d-15],b=p[d-2];p[d]=((g<<25|g>>>7)^(g<<14|g>>>18)^g>>>3)+p[d-7]+((b<<15|b>>>17)^(b<<13|b>>>19)^b>>>10)+p[d-16]}g=h+((c<<26|c>>>6)^(c<<21|c>>>11)^(c<<7|c>>>25))+(c&u^~c&l)+a[d]+p[d],b=((n<<30|n>>>2)^(n<<19|n>>>13)^(n<<10|n>>>22))+(n&r^n&i^r&i),h=l,l=u,u=c,c=o+g|0,o=i,i=r,r=n,n=g+b|0}s[0]=s[0]+n|0,s[1]=s[1]+r|0,s[2]=s[2]+i|0,s[3]=s[3]+o|0,s[4]=s[4]+c|0,s[5]=s[5]+u|0,s[6]=s[6]+l|0,s[7]=s[7]+h|0},_doFinalize:function(){var t=this._data,s=t.words,n=8*this._nDataBytes,r=8*t.sigBytes;return s[r>>>5]|=128<<24-r%32,s[14+(r+64>>>9<<4)]=e.floor(n/4294967296),s[15+(r+64>>>9<<4)]=n,t.sigBytes=4*s.length,this._process(),this._hash},clone:function(){var e=n.clone.call(this);return e._hash=this._hash.clone(),e}});t.SHA256=n._createHelper(r),t.HmacSHA256=n._createHmacHelper(r)}(Math),y=(b=S).enc.Utf8,b.algo.HMAC=b.lib.Base.extend({init:function(e,t){e=this._hasher=new e.init,"string"==typeof t&&(t=y.parse(t));var s=e.blockSize,n=4*s;t.sigBytes>n&&(t=e.finalize(t)),t.clamp();for(var r=this._oKey=t.clone(),i=this._iKey=t.clone(),a=r.words,o=i.words,c=0;c>>2]>>>24-r%4*8&255)<<16|(t[r+1>>>2]>>>24-(r+1)%4*8&255)<<8|t[r+2>>>2]>>>24-(r+2)%4*8&255,a=0;4>a&&r+.75*a>>6*(3-a)&63));if(t=n.charAt(64))for(;e.length%4;)e.push(t);return e.join("")},parse:function(e){var t=e.length,s=this._map;(n=s.charAt(64))&&-1!=(n=e.indexOf(n))&&(t=n);for(var n=[],r=0,i=0;i>>6-i%4*2;n[r>>>2]|=(a|o)<<24-r%4*8,r++}return f.create(n,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="},function(e){function t(e,t,s,n,r,i,a){return((e=e+(t&s|~t&n)+r+a)<>>32-i)+t}function s(e,t,s,n,r,i,a){return((e=e+(t&n|s&~n)+r+a)<>>32-i)+t}function n(e,t,s,n,r,i,a){return((e=e+(t^s^n)+r+a)<>>32-i)+t}function r(e,t,s,n,r,i,a){return((e=e+(s^(t|~n))+r+a)<>>32-i)+t}for(var i=S,a=(c=i.lib).WordArray,o=c.Hasher,c=i.algo,u=[],l=0;64>l;l++)u[l]=4294967296*e.abs(e.sin(l+1))|0;c=c.MD5=o.extend({_doReset:function(){this._hash=new a.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(e,i){for(var a=0;16>a;a++){var o=e[c=i+a];e[c]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8)}a=this._hash.words;var c=e[i+0],l=(o=e[i+1],e[i+2]),h=e[i+3],d=e[i+4],p=e[i+5],g=e[i+6],b=e[i+7],y=e[i+8],m=e[i+9],f=e[i+10],v=e[i+11],S=e[i+12],w=e[i+13],O=e[i+14],k=e[i+15],C=t(C=a[0],E=a[1],j=a[2],P=a[3],c,7,u[0]),P=t(P,C,E,j,o,12,u[1]),j=t(j,P,C,E,l,17,u[2]),E=t(E,j,P,C,h,22,u[3]);C=t(C,E,j,P,d,7,u[4]),P=t(P,C,E,j,p,12,u[5]),j=t(j,P,C,E,g,17,u[6]),E=t(E,j,P,C,b,22,u[7]),C=t(C,E,j,P,y,7,u[8]),P=t(P,C,E,j,m,12,u[9]),j=t(j,P,C,E,f,17,u[10]),E=t(E,j,P,C,v,22,u[11]),C=t(C,E,j,P,S,7,u[12]),P=t(P,C,E,j,w,12,u[13]),j=t(j,P,C,E,O,17,u[14]),C=s(C,E=t(E,j,P,C,k,22,u[15]),j,P,o,5,u[16]),P=s(P,C,E,j,g,9,u[17]),j=s(j,P,C,E,v,14,u[18]),E=s(E,j,P,C,c,20,u[19]),C=s(C,E,j,P,p,5,u[20]),P=s(P,C,E,j,f,9,u[21]),j=s(j,P,C,E,k,14,u[22]),E=s(E,j,P,C,d,20,u[23]),C=s(C,E,j,P,m,5,u[24]),P=s(P,C,E,j,O,9,u[25]),j=s(j,P,C,E,h,14,u[26]),E=s(E,j,P,C,y,20,u[27]),C=s(C,E,j,P,w,5,u[28]),P=s(P,C,E,j,l,9,u[29]),j=s(j,P,C,E,b,14,u[30]),C=n(C,E=s(E,j,P,C,S,20,u[31]),j,P,p,4,u[32]),P=n(P,C,E,j,y,11,u[33]),j=n(j,P,C,E,v,16,u[34]),E=n(E,j,P,C,O,23,u[35]),C=n(C,E,j,P,o,4,u[36]),P=n(P,C,E,j,d,11,u[37]),j=n(j,P,C,E,b,16,u[38]),E=n(E,j,P,C,f,23,u[39]),C=n(C,E,j,P,w,4,u[40]),P=n(P,C,E,j,c,11,u[41]),j=n(j,P,C,E,h,16,u[42]),E=n(E,j,P,C,g,23,u[43]),C=n(C,E,j,P,m,4,u[44]),P=n(P,C,E,j,S,11,u[45]),j=n(j,P,C,E,k,16,u[46]),C=r(C,E=n(E,j,P,C,l,23,u[47]),j,P,c,6,u[48]),P=r(P,C,E,j,b,10,u[49]),j=r(j,P,C,E,O,15,u[50]),E=r(E,j,P,C,p,21,u[51]),C=r(C,E,j,P,S,6,u[52]),P=r(P,C,E,j,h,10,u[53]),j=r(j,P,C,E,f,15,u[54]),E=r(E,j,P,C,o,21,u[55]),C=r(C,E,j,P,y,6,u[56]),P=r(P,C,E,j,k,10,u[57]),j=r(j,P,C,E,g,15,u[58]),E=r(E,j,P,C,w,21,u[59]),C=r(C,E,j,P,d,6,u[60]),P=r(P,C,E,j,v,10,u[61]),j=r(j,P,C,E,l,15,u[62]),E=r(E,j,P,C,m,21,u[63]);a[0]=a[0]+C|0,a[1]=a[1]+E|0,a[2]=a[2]+j|0,a[3]=a[3]+P|0},_doFinalize:function(){var t=this._data,s=t.words,n=8*this._nDataBytes,r=8*t.sigBytes;s[r>>>5]|=128<<24-r%32;var i=e.floor(n/4294967296);for(s[15+(r+64>>>9<<4)]=16711935&(i<<8|i>>>24)|4278255360&(i<<24|i>>>8),s[14+(r+64>>>9<<4)]=16711935&(n<<8|n>>>24)|4278255360&(n<<24|n>>>8),t.sigBytes=4*(s.length+1),this._process(),s=(t=this._hash).words,n=0;4>n;n++)r=s[n],s[n]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8);return t},clone:function(){var e=o.clone.call(this);return e._hash=this._hash.clone(),e}}),i.MD5=o._createHelper(c),i.HmacMD5=o._createHmacHelper(c)}(Math),function(){var e,t=S,s=(e=t.lib).Base,n=e.WordArray,r=(e=t.algo).EvpKDF=s.extend({cfg:s.extend({keySize:4,hasher:e.MD5,iterations:1}),init:function(e){this.cfg=this.cfg.extend(e)},compute:function(e,t){for(var s=(o=this.cfg).hasher.create(),r=n.create(),i=r.words,a=o.keySize,o=o.iterations;i.length>>2]}},e.BlockCipher=a.extend({cfg:a.cfg.extend({mode:o,padding:u}),reset:function(){a.reset.call(this);var e=(t=this.cfg).iv,t=t.mode;if(this._xformMode==this._ENC_XFORM_MODE)var s=t.createEncryptor;else s=t.createDecryptor,this._minBufferSize=1;this._mode=s.call(t,this,e&&e.words)},_doProcessBlock:function(e,t){this._mode.processBlock(e,t)},_doFinalize:function(){var e=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){e.pad(this._data,this.blockSize);var t=this._process(!0)}else t=this._process(!0),e.unpad(t);return t},blockSize:4});var l=e.CipherParams=t.extend({init:function(e){this.mixIn(e)},toString:function(e){return(e||this.formatter).stringify(this)}}),h=(o=(d.format={}).OpenSSL={stringify:function(e){var t=e.ciphertext;return((e=e.salt)?s.create([1398893684,1701076831]).concat(e).concat(t):t).toString(r)},parse:function(e){var t=(e=r.parse(e)).words;if(1398893684==t[0]&&1701076831==t[1]){var n=s.create(t.slice(2,4));t.splice(0,4),e.sigBytes-=16}return l.create({ciphertext:e,salt:n})}},e.SerializableCipher=t.extend({cfg:t.extend({format:o}),encrypt:function(e,t,s,n){n=this.cfg.extend(n);var r=e.createEncryptor(s,n);return t=r.finalize(t),r=r.cfg,l.create({ciphertext:t,key:s,iv:r.iv,algorithm:e,mode:r.mode,padding:r.padding,blockSize:e.blockSize,formatter:n.format})},decrypt:function(e,t,s,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),e.createDecryptor(s,n).finalize(t.ciphertext)},_parse:function(e,t){return"string"==typeof e?t.parse(e,this):e}})),d=(d.kdf={}).OpenSSL={execute:function(e,t,n,r){return r||(r=s.random(8)),e=i.create({keySize:t+n}).compute(e,r),n=s.create(e.words.slice(t),4*n),e.sigBytes=4*t,l.create({key:e,iv:n,salt:r})}},p=e.PasswordBasedCipher=h.extend({cfg:h.cfg.extend({kdf:d}),encrypt:function(e,t,s,n){return s=(n=this.cfg.extend(n)).kdf.execute(s,e.keySize,e.ivSize),n.iv=s.iv,(e=h.encrypt.call(this,e,t,s.key,n)).mixIn(s),e},decrypt:function(e,t,s,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),s=n.kdf.execute(s,e.keySize,e.ivSize,t.salt),n.iv=s.iv,h.decrypt.call(this,e,t,s.key,n)}})}(),function(){for(var e=S,t=e.lib.BlockCipher,s=e.algo,n=[],r=[],i=[],a=[],o=[],c=[],u=[],l=[],h=[],d=[],p=[],g=0;256>g;g++)p[g]=128>g?g<<1:g<<1^283;var b=0,y=0;for(g=0;256>g;g++){var m=(m=y^y<<1^y<<2^y<<3^y<<4)>>>8^255&m^99;n[b]=m,r[m]=b;var f=p[b],v=p[f],w=p[v],O=257*p[m]^16843008*m;i[b]=O<<24|O>>>8,a[b]=O<<16|O>>>16,o[b]=O<<8|O>>>24,c[b]=O,O=16843009*w^65537*v^257*f^16843008*b,u[m]=O<<24|O>>>8,l[m]=O<<16|O>>>16,h[m]=O<<8|O>>>24,d[m]=O,b?(b=f^p[p[p[w^f]]],y^=p[p[y]]):b=y=1}var k=[0,1,2,4,8,16,32,64,128,27,54];s=s.AES=t.extend({_doReset:function(){for(var e=(s=this._key).words,t=s.sigBytes/4,s=4*((this._nRounds=t+6)+1),r=this._keySchedule=[],i=0;i>>24]<<24|n[a>>>16&255]<<16|n[a>>>8&255]<<8|n[255&a]):(a=n[(a=a<<8|a>>>24)>>>24]<<24|n[a>>>16&255]<<16|n[a>>>8&255]<<8|n[255&a],a^=k[i/t|0]<<24),r[i]=r[i-t]^a}for(e=this._invKeySchedule=[],t=0;tt||4>=i?a:u[n[a>>>24]]^l[n[a>>>16&255]]^h[n[a>>>8&255]]^d[n[255&a]]},encryptBlock:function(e,t){this._doCryptBlock(e,t,this._keySchedule,i,a,o,c,n)},decryptBlock:function(e,t){var s=e[t+1];e[t+1]=e[t+3],e[t+3]=s,this._doCryptBlock(e,t,this._invKeySchedule,u,l,h,d,r),s=e[t+1],e[t+1]=e[t+3],e[t+3]=s},_doCryptBlock:function(e,t,s,n,r,i,a,o){for(var c=this._nRounds,u=e[t]^s[0],l=e[t+1]^s[1],h=e[t+2]^s[2],d=e[t+3]^s[3],p=4,g=1;g>>24]^r[l>>>16&255]^i[h>>>8&255]^a[255&d]^s[p++],y=n[l>>>24]^r[h>>>16&255]^i[d>>>8&255]^a[255&u]^s[p++],m=n[h>>>24]^r[d>>>16&255]^i[u>>>8&255]^a[255&l]^s[p++];d=n[d>>>24]^r[u>>>16&255]^i[l>>>8&255]^a[255&h]^s[p++],u=b,l=y,h=m}b=(o[u>>>24]<<24|o[l>>>16&255]<<16|o[h>>>8&255]<<8|o[255&d])^s[p++],y=(o[l>>>24]<<24|o[h>>>16&255]<<16|o[d>>>8&255]<<8|o[255&u])^s[p++],m=(o[h>>>24]<<24|o[d>>>16&255]<<16|o[u>>>8&255]<<8|o[255&l])^s[p++],d=(o[d>>>24]<<24|o[u>>>16&255]<<16|o[l>>>8&255]<<8|o[255&h])^s[p++],e[t]=b,e[t+1]=y,e[t+2]=m,e[t+3]=d},keySize:8});e.AES=t._createHelper(s)}(),S.mode.ECB=((v=S.lib.BlockCipherMode.extend()).Encryptor=v.extend({processBlock:function(e,t){this._cipher.encryptBlock(e,t)}}),v.Decryptor=v.extend({processBlock:function(e,t){this._cipher.decryptBlock(e,t)}}),v);var w,O=t(S);class k{constructor({cipherKey:e}){this.cipherKey=e,this.CryptoJS=O,this.encryptedKey=this.CryptoJS.SHA256(e)}encrypt(e){if(0===("string"==typeof e?e:k.decoder.decode(e)).length)throw new Error("encryption error. empty content");const t=this.getIv();return{metadata:t,data:c(this.CryptoJS.AES.encrypt(e,this.encryptedKey,{iv:this.bufferToWordArray(t),mode:this.CryptoJS.mode.CBC}).ciphertext.toString(this.CryptoJS.enc.Base64))}}encryptFileData(e){return i(this,void 0,void 0,(function*(){const t=yield this.getKey(),s=this.getIv();return{data:yield crypto.subtle.encrypt({name:this.algo,iv:s},t,e),metadata:s}}))}decrypt(e){if("string"==typeof e.data)throw new Error("Decryption error: data for decryption should be ArrayBuffed.");const t=this.bufferToWordArray(new Uint8ClampedArray(e.metadata)),s=this.bufferToWordArray(new Uint8ClampedArray(e.data));return k.encoder.encode(this.CryptoJS.AES.decrypt({ciphertext:s},this.encryptedKey,{iv:t,mode:this.CryptoJS.mode.CBC}).toString(this.CryptoJS.enc.Utf8)).buffer}decryptFileData(e){return i(this,void 0,void 0,(function*(){if("string"==typeof e.data)throw new Error("Decryption error: data for decryption should be ArrayBuffed.");const t=yield this.getKey();return crypto.subtle.decrypt({name:this.algo,iv:e.metadata},t,e.data)}))}get identifier(){return"ACRH"}get algo(){return"AES-CBC"}getIv(){return crypto.getRandomValues(new Uint8Array(k.BLOCK_SIZE))}getKey(){return i(this,void 0,void 0,(function*(){const e=k.encoder.encode(this.cipherKey),t=yield crypto.subtle.digest("SHA-256",e.buffer);return crypto.subtle.importKey("raw",t,this.algo,!0,["encrypt","decrypt"])}))}bufferToWordArray(e){const t=[];let s;for(s=0;s({messageType:"object",message:this.configuration,details:"Create with configuration:",ignoredKeys:(e,t)=>"function"==typeof t[e]||"logger"===e})))}get logger(){return this._logger}HMACSHA256(e){return O.HmacSHA256(e,this.configuration.secretKey).toString(O.enc.Base64)}SHA256(e){return O.SHA256(e).toString(O.enc.Hex)}encrypt(e,t,s){return this.configuration.customEncrypt?(this.logger&&this.logger.warn("Crypto","'customEncrypt' is deprecated. Consult docs for better alternative."),this.configuration.customEncrypt(e)):this.pnEncrypt(e,t,s)}decrypt(e,t,s){return this.configuration.customDecrypt?(this.logger&&this.logger.warn("Crypto","'customDecrypt' is deprecated. Consult docs for better alternative."),this.configuration.customDecrypt(e)):this.pnDecrypt(e,t,s)}pnEncrypt(e,t,s){const n=null!=t?t:this.configuration.cipherKey;if(!n)return e;this.logger&&this.logger.debug("Crypto",(()=>({messageType:"object",message:Object.assign({data:e,cipherKey:n},null!=s?s:{}),details:"Encrypt with parameters:"}))),s=this.parseOptions(s);const r=this.getMode(s),i=this.getPaddedKey(n,s);if(this.configuration.useRandomIVs){const t=this.getRandomIV(),s=O.AES.encrypt(e,i,{iv:t,mode:r}).ciphertext;return t.clone().concat(s.clone()).toString(O.enc.Base64)}const a=this.getIV(s);return O.AES.encrypt(e,i,{iv:a,mode:r}).ciphertext.toString(O.enc.Base64)||e}pnDecrypt(e,t,s){const n=null!=t?t:this.configuration.cipherKey;if(!n)return e;this.logger&&this.logger.debug("Crypto",(()=>({messageType:"object",message:Object.assign({data:e,cipherKey:n},null!=s?s:{}),details:"Decrypt with parameters:"}))),s=this.parseOptions(s);const r=this.getMode(s),i=this.getPaddedKey(n,s);if(this.configuration.useRandomIVs){const t=new Uint8ClampedArray(c(e)),s=C(t.slice(0,16)),n=C(t.slice(16));try{const e=O.AES.decrypt({ciphertext:n},i,{iv:s,mode:r}).toString(O.enc.Utf8);return JSON.parse(e)}catch(e){return this.logger&&this.logger.error("Crypto",(()=>({messageType:"error",message:e}))),null}}else{const t=this.getIV(s);try{const s=O.enc.Base64.parse(e),n=O.AES.decrypt({ciphertext:s},i,{iv:t,mode:r}).toString(O.enc.Utf8);return JSON.parse(n)}catch(e){return this.logger&&this.logger.error("Crypto",(()=>({messageType:"error",message:e}))),null}}}parseOptions(e){var t,s,n,r;if(!e)return this.defaultOptions;const i={encryptKey:null!==(t=e.encryptKey)&&void 0!==t?t:this.defaultOptions.encryptKey,keyEncoding:null!==(s=e.keyEncoding)&&void 0!==s?s:this.defaultOptions.keyEncoding,keyLength:null!==(n=e.keyLength)&&void 0!==n?n:this.defaultOptions.keyLength,mode:null!==(r=e.mode)&&void 0!==r?r:this.defaultOptions.mode};return-1===this.allowedKeyEncodings.indexOf(i.keyEncoding.toLowerCase())&&(i.keyEncoding=this.defaultOptions.keyEncoding),-1===this.allowedKeyLengths.indexOf(i.keyLength)&&(i.keyLength=this.defaultOptions.keyLength),-1===this.allowedModes.indexOf(i.mode.toLowerCase())&&(i.mode=this.defaultOptions.mode),i}decodeKey(e,t){return"base64"===t.keyEncoding?O.enc.Base64.parse(e):"hex"===t.keyEncoding?O.enc.Hex.parse(e):e}getPaddedKey(e,t){return e=this.decodeKey(e,t),t.encryptKey?O.enc.Utf8.parse(this.SHA256(e).slice(0,32)):e}getMode(e){return"ecb"===e.mode?O.mode.ECB:O.mode.CBC}getIV(e){return"cbc"===e.mode?O.enc.Utf8.parse(this.iv):null}getRandomIV(){return O.lib.WordArray.random(16)}}class j{encrypt(e,t){return i(this,void 0,void 0,(function*(){if(!(t instanceof ArrayBuffer)&&"string"!=typeof t)throw new Error("Cannot encrypt this file. In browsers file encryption supports only string or ArrayBuffer");const s=yield this.getKey(e);return t instanceof ArrayBuffer?this.encryptArrayBuffer(s,t):this.encryptString(s,t)}))}encryptArrayBuffer(e,t){return i(this,void 0,void 0,(function*(){const s=crypto.getRandomValues(new Uint8Array(16));return this.concatArrayBuffer(s.buffer,yield crypto.subtle.encrypt({name:"AES-CBC",iv:s},e,t))}))}encryptString(e,t){return i(this,void 0,void 0,(function*(){const s=crypto.getRandomValues(new Uint8Array(16)),n=j.encoder.encode(t).buffer,r=yield crypto.subtle.encrypt({name:"AES-CBC",iv:s},e,n),i=this.concatArrayBuffer(s.buffer,r);return j.decoder.decode(i)}))}encryptFile(e,t,s){return i(this,void 0,void 0,(function*(){var n,r;if((null!==(n=t.contentLength)&&void 0!==n?n:0)<=0)throw new Error("encryption error. empty content");const i=yield this.getKey(e),a=yield t.toArrayBuffer(),o=yield this.encryptArrayBuffer(i,a);return s.create({name:t.name,mimeType:null!==(r=t.mimeType)&&void 0!==r?r:"application/octet-stream",data:o})}))}decrypt(e,t){return i(this,void 0,void 0,(function*(){if(!(t instanceof ArrayBuffer)&&"string"!=typeof t)throw new Error("Cannot decrypt this file. In browsers file decryption supports only string or ArrayBuffer");const s=yield this.getKey(e);return t instanceof ArrayBuffer?this.decryptArrayBuffer(s,t):this.decryptString(s,t)}))}decryptArrayBuffer(e,t){return i(this,void 0,void 0,(function*(){const s=t.slice(0,16);if(t.slice(j.IV_LENGTH).byteLength<=0)throw new Error("decryption error: empty content");return yield crypto.subtle.decrypt({name:"AES-CBC",iv:s},e,t.slice(j.IV_LENGTH))}))}decryptString(e,t){return i(this,void 0,void 0,(function*(){const s=j.encoder.encode(t).buffer,n=s.slice(0,16),r=s.slice(16),i=yield crypto.subtle.decrypt({name:"AES-CBC",iv:n},e,r);return j.decoder.decode(i)}))}decryptFile(e,t,s){return i(this,void 0,void 0,(function*(){const n=yield this.getKey(e),r=yield t.toArrayBuffer(),i=yield this.decryptArrayBuffer(n,r);return s.create({name:t.name,mimeType:t.mimeType,data:i})}))}getKey(e){return i(this,void 0,void 0,(function*(){const t=yield crypto.subtle.digest("SHA-256",j.encoder.encode(e)),s=Array.from(new Uint8Array(t)).map((e=>e.toString(16).padStart(2,"0"))).join(""),n=j.encoder.encode(s.slice(0,32)).buffer;return crypto.subtle.importKey("raw",n,"AES-CBC",!0,["encrypt","decrypt"])}))}concatArrayBuffer(e,t){const s=new Uint8Array(e.byteLength+t.byteLength);return s.set(new Uint8Array(e),0),s.set(new Uint8Array(t),e.byteLength),s.buffer}}j.IV_LENGTH=16,j.encoder=new TextEncoder,j.decoder=new TextDecoder;class E{constructor(e){this.config=e,this.cryptor=new P(Object.assign({},e)),this.fileCryptor=new j}set logger(e){this.cryptor.logger=e}encrypt(e){const t="string"==typeof e?e:E.decoder.decode(e);return{data:this.cryptor.encrypt(t),metadata:null}}encryptFile(e,t){return i(this,void 0,void 0,(function*(){var s;if(!this.config.cipherKey)throw new d("File encryption error: cipher key not set.");return this.fileCryptor.encryptFile(null===(s=this.config)||void 0===s?void 0:s.cipherKey,e,t)}))}decrypt(e){const t="string"==typeof e.data?e.data:u(e.data);return this.cryptor.decrypt(t)}decryptFile(e,t){return i(this,void 0,void 0,(function*(){if(!this.config.cipherKey)throw new d("File encryption error: cipher key not set.");return this.fileCryptor.decryptFile(this.config.cipherKey,e,t)}))}get identifier(){return""}toString(){return`AesCbcCryptor { ${Object.entries(this.config).reduce(((e,[t,s])=>("logger"===t||e.push(`${t}: ${"function"==typeof s?"":s}`),e)),[]).join(", ")} }`}}E.encoder=new TextEncoder,E.decoder=new TextDecoder;class N extends a{set logger(e){if(this.defaultCryptor.identifier===N.LEGACY_IDENTIFIER)this.defaultCryptor.logger=e;else{const t=this.cryptors.find((e=>e.identifier===N.LEGACY_IDENTIFIER));t&&(t.logger=e)}}static legacyCryptoModule(e){var t;if(!e.cipherKey)throw new d("Crypto module error: cipher key not set.");return new N({default:new E(Object.assign(Object.assign({},e),{useRandomIVs:null===(t=e.useRandomIVs)||void 0===t||t})),cryptors:[new k({cipherKey:e.cipherKey})]})}static aesCbcCryptoModule(e){var t;if(!e.cipherKey)throw new d("Crypto module error: cipher key not set.");return new N({default:new k({cipherKey:e.cipherKey}),cryptors:[new E(Object.assign(Object.assign({},e),{useRandomIVs:null===(t=e.useRandomIVs)||void 0===t||t}))]})}static withDefaultCryptor(e){return new this({default:e})}encrypt(e){const t=e instanceof ArrayBuffer&&this.defaultCryptor.identifier===N.LEGACY_IDENTIFIER?this.defaultCryptor.encrypt(N.decoder.decode(e)):this.defaultCryptor.encrypt(e);if(!t.metadata)return t.data;if("string"==typeof t.data)throw new Error("Encryption error: encrypted data should be ArrayBuffed.");const s=this.getHeaderData(t);return this.concatArrayBuffer(s,t.data)}encryptFile(e,t){return i(this,void 0,void 0,(function*(){if(this.defaultCryptor.identifier===T.LEGACY_IDENTIFIER)return this.defaultCryptor.encryptFile(e,t);const s=yield this.getFileData(e),n=yield this.defaultCryptor.encryptFileData(s);if("string"==typeof n.data)throw new Error("Encryption error: encrypted data should be ArrayBuffed.");return t.create({name:e.name,mimeType:"application/octet-stream",data:this.concatArrayBuffer(this.getHeaderData(n),n.data)})}))}decrypt(e){const t="string"==typeof e?c(e):e,s=T.tryParse(t),n=this.getCryptor(s),r=s.length>0?t.slice(s.length-s.metadataLength,s.length):null;if(t.slice(s.length).byteLength<=0)throw new Error("Decryption error: empty content");return n.decrypt({data:t.slice(s.length),metadata:r})}decryptFile(e,t){return i(this,void 0,void 0,(function*(){const s=yield e.data.arrayBuffer(),n=T.tryParse(s),r=this.getCryptor(n);if((null==r?void 0:r.identifier)===T.LEGACY_IDENTIFIER)return r.decryptFile(e,t);const i=(yield this.getFileData(s)).slice(n.length-n.metadataLength,n.length);return t.create({name:e.name,data:yield this.defaultCryptor.decryptFileData({data:s.slice(n.length),metadata:i})})}))}getCryptorFromId(e){const t=this.getAllCryptors().find((t=>e===t.identifier));if(t)return t;throw Error("Unknown cryptor error")}getCryptor(e){if("string"==typeof e){const t=this.getAllCryptors().find((t=>t.identifier===e));if(t)return t;throw new Error("Unknown cryptor error")}if(e instanceof _)return this.getCryptorFromId(e.identifier)}getHeaderData(e){if(!e.metadata)return;const t=T.from(this.defaultCryptor.identifier,e.metadata),s=new Uint8Array(t.length);let n=0;return s.set(t.data,n),n+=t.length-e.metadata.byteLength,s.set(new Uint8Array(e.metadata),n),s.buffer}concatArrayBuffer(e,t){const s=new Uint8Array(e.byteLength+t.byteLength);return s.set(new Uint8Array(e),0),s.set(new Uint8Array(t),e.byteLength),s.buffer}getFileData(e){return i(this,void 0,void 0,(function*(){if(e instanceof ArrayBuffer)return e;if(e instanceof o)return e.toArrayBuffer();throw new Error("Cannot decrypt/encrypt file. In browsers file encrypt/decrypt supported for string, ArrayBuffer or Blob")}))}}N.LEGACY_IDENTIFIER="";class T{static from(e,t){if(e!==T.LEGACY_IDENTIFIER)return new _(e,t.byteLength)}static tryParse(e){const t=new Uint8Array(e);let s,n,r=null;if(t.byteLength>=4&&(s=t.slice(0,4),this.decoder.decode(s)!==T.SENTINEL))return N.LEGACY_IDENTIFIER;if(!(t.byteLength>=5))throw new Error("Decryption error: invalid header version");if(r=t[4],r>T.MAX_VERSION)throw new Error("Decryption error: Unknown cryptor error");let i=5+T.IDENTIFIER_LENGTH;if(!(t.byteLength>=i))throw new Error("Decryption error: invalid crypto identifier");n=t.slice(5,i);let a=null;if(!(t.byteLength>=i+1))throw new Error("Decryption error: invalid metadata length");return a=t[i],i+=1,255===a&&t.byteLength>=i+2&&(a=new Uint16Array(t.slice(i,i+2)).reduce(((e,t)=>(e<<8)+t),0)),new _(this.decoder.decode(n),a)}}T.SENTINEL="PNED",T.LEGACY_IDENTIFIER="",T.IDENTIFIER_LENGTH=4,T.VERSION=1,T.MAX_VERSION=1,T.decoder=new TextDecoder;class _{constructor(e,t){this._identifier=e,this._metadataLength=t}get identifier(){return this._identifier}set identifier(e){this._identifier=e}get metadataLength(){return this._metadataLength}set metadataLength(e){this._metadataLength=e}get version(){return T.VERSION}get length(){return T.SENTINEL.length+1+T.IDENTIFIER_LENGTH+(this.metadataLength<255?1:3)+this.metadataLength}get data(){let e=0;const t=new Uint8Array(this.length),s=new TextEncoder;t.set(s.encode(T.SENTINEL)),e+=T.SENTINEL.length,t[e]=this.version,e++,this.identifier&&t.set(s.encode(this.identifier),e);const n=this.metadataLength;return e+=T.IDENTIFIER_LENGTH,n<255?t[e]=n:t.set([255,n>>8,255&n],e),t}}_.IDENTIFIER_LENGTH=4,_.SENTINEL="PNED";class I extends Error{static create(e,t){return I.isErrorObject(e)?I.createFromError(e):I.createFromServiceResponse(e,t)}static createFromError(e){let t=h.PNUnknownCategory,s="Unknown error",n="Error";if(!e)return new I(s,t,0);if(e instanceof I)return e;if(I.isErrorObject(e)&&(s=e.message,n=e.name),"AbortError"===n||-1!==s.indexOf("Aborted"))t=h.PNCancelledCategory,s="Request cancelled";else if(-1!==s.toLowerCase().indexOf("timeout"))t=h.PNTimeoutCategory,s="Request timeout";else if(-1!==s.toLowerCase().indexOf("network"))t=h.PNNetworkIssuesCategory,s="Network issues";else if("TypeError"===n)t=-1!==s.indexOf("Load failed")||-1!=s.indexOf("Failed to fetch")?h.PNNetworkIssuesCategory:h.PNBadRequestCategory;else if("FetchError"===n){const n=e.code;["ECONNREFUSED","ENETUNREACH","ENOTFOUND","ECONNRESET","EAI_AGAIN"].includes(n)&&(t=h.PNNetworkIssuesCategory),"ECONNREFUSED"===n?s="Connection refused":"ENETUNREACH"===n?s="Network not reachable":"ENOTFOUND"===n?s="Server not found":"ECONNRESET"===n?s="Connection reset by peer":"EAI_AGAIN"===n?s="Name resolution error":"ETIMEDOUT"===n?(t=h.PNTimeoutCategory,s="Request timeout"):s=`Unknown system error: ${e}`}else"Request timeout"===s&&(t=h.PNTimeoutCategory);return new I(s,t,0,e)}static createFromServiceResponse(e,t){let s,n=h.PNUnknownCategory,r="Unknown error",{status:i}=e;if(null!=t||(t=e.body),402===i?r="Not available for used key set. Contact support@pubnub.com":400===i?(n=h.PNBadRequestCategory,r="Bad request"):403===i?(n=h.PNAccessDeniedCategory,r="Access denied"):i>=500&&(n=h.PNServerErrorCategory,r="Internal server error"),"object"==typeof e&&0===Object.keys(e).length&&(n=h.PNMalformedResponseCategory,r="Malformed response (network issues)",i=400),t&&t.byteLength>0){const n=(new TextDecoder).decode(t);if(-1!==e.headers["content-type"].indexOf("text/javascript")||-1!==e.headers["content-type"].indexOf("application/json"))try{const e=JSON.parse(n);"object"==typeof e&&(Array.isArray(e)?"number"==typeof e[0]&&0===e[0]&&e.length>1&&"string"==typeof e[1]&&(s=e[1]):("error"in e&&(1===e.error||!0===e.error)&&"status"in e&&"number"==typeof e.status&&"message"in e&&"service"in e?(s=e,i=e.status):s=e,"error"in e&&e.error instanceof Error&&(s=e.error)))}catch(e){s=n}else if(-1!==e.headers["content-type"].indexOf("xml")){const e=/(.*)<\/Message>/gi.exec(n);r=e?`Upload to bucket failed: ${e[1]}`:"Upload to bucket failed."}else s=n}return new I(r,n,i,s)}constructor(e,t,s,n){super(e),this.category=t,this.statusCode=s,this.errorData=n,this.name="PubNubAPIError"}toStatus(e){return{error:!0,category:this.category,operation:e,statusCode:this.statusCode,errorData:this.errorData,toJSON:function(){let e;const t=this.errorData;if(t)try{if("object"==typeof t){const s=Object.assign(Object.assign(Object.assign(Object.assign({},"name"in t?{name:t.name}:{}),"message"in t?{message:t.message}:{}),"stack"in t?{stack:t.stack}:{}),t);e=JSON.parse(JSON.stringify(s,I.circularReplacer()))}else e=t}catch(t){e={error:"Could not serialize the error object"}}const s=r(this,["toJSON"]);return JSON.stringify(Object.assign(Object.assign({},s),{errorData:e}))}}}toPubNubError(e,t){return new d(null!=t?t:this.message,this.toStatus(e))}static circularReplacer(){const e=new WeakSet;return function(t,s){if("object"==typeof s&&null!==s){if(e.has(s))return"[Circular]";e.add(s)}return s}}static isErrorObject(e){return!(!e||"object"!=typeof e)&&(e instanceof Error||("name"in e&&"message"in e&&"string"==typeof e.name&&"string"==typeof e.message||"[object Error]"===Object.prototype.toString.call(e)))}}!function(e){e.PNPublishOperation="PNPublishOperation",e.PNSignalOperation="PNSignalOperation",e.PNSubscribeOperation="PNSubscribeOperation",e.PNUnsubscribeOperation="PNUnsubscribeOperation",e.PNWhereNowOperation="PNWhereNowOperation",e.PNHereNowOperation="PNHereNowOperation",e.PNGlobalHereNowOperation="PNGlobalHereNowOperation",e.PNSetStateOperation="PNSetStateOperation",e.PNGetStateOperation="PNGetStateOperation",e.PNHeartbeatOperation="PNHeartbeatOperation",e.PNAddMessageActionOperation="PNAddActionOperation",e.PNRemoveMessageActionOperation="PNRemoveMessageActionOperation",e.PNGetMessageActionsOperation="PNGetMessageActionsOperation",e.PNTimeOperation="PNTimeOperation",e.PNHistoryOperation="PNHistoryOperation",e.PNDeleteMessagesOperation="PNDeleteMessagesOperation",e.PNFetchMessagesOperation="PNFetchMessagesOperation",e.PNMessageCounts="PNMessageCountsOperation",e.PNGetAllUUIDMetadataOperation="PNGetAllUUIDMetadataOperation",e.PNGetUUIDMetadataOperation="PNGetUUIDMetadataOperation",e.PNSetUUIDMetadataOperation="PNSetUUIDMetadataOperation",e.PNRemoveUUIDMetadataOperation="PNRemoveUUIDMetadataOperation",e.PNGetAllChannelMetadataOperation="PNGetAllChannelMetadataOperation",e.PNGetChannelMetadataOperation="PNGetChannelMetadataOperation",e.PNSetChannelMetadataOperation="PNSetChannelMetadataOperation",e.PNRemoveChannelMetadataOperation="PNRemoveChannelMetadataOperation",e.PNGetMembersOperation="PNGetMembersOperation",e.PNSetMembersOperation="PNSetMembersOperation",e.PNGetMembershipsOperation="PNGetMembershipsOperation",e.PNSetMembershipsOperation="PNSetMembershipsOperation",e.PNListFilesOperation="PNListFilesOperation",e.PNGenerateUploadUrlOperation="PNGenerateUploadUrlOperation",e.PNPublishFileOperation="PNPublishFileOperation",e.PNPublishFileMessageOperation="PNPublishFileMessageOperation",e.PNGetFileUrlOperation="PNGetFileUrlOperation",e.PNDownloadFileOperation="PNDownloadFileOperation",e.PNDeleteFileOperation="PNDeleteFileOperation",e.PNAddPushNotificationEnabledChannelsOperation="PNAddPushNotificationEnabledChannelsOperation",e.PNRemovePushNotificationEnabledChannelsOperation="PNRemovePushNotificationEnabledChannelsOperation",e.PNPushNotificationEnabledChannelsOperation="PNPushNotificationEnabledChannelsOperation",e.PNRemoveAllPushNotificationsOperation="PNRemoveAllPushNotificationsOperation",e.PNChannelGroupsOperation="PNChannelGroupsOperation",e.PNRemoveGroupOperation="PNRemoveGroupOperation",e.PNChannelsForGroupOperation="PNChannelsForGroupOperation",e.PNAddChannelsToGroupOperation="PNAddChannelsToGroupOperation",e.PNRemoveChannelsFromGroupOperation="PNRemoveChannelsFromGroupOperation",e.PNAccessManagerGrant="PNAccessManagerGrant",e.PNAccessManagerGrantToken="PNAccessManagerGrantToken",e.PNAccessManagerAudit="PNAccessManagerAudit",e.PNAccessManagerRevokeToken="PNAccessManagerRevokeToken",e.PNHandshakeOperation="PNHandshakeOperation",e.PNReceiveMessagesOperation="PNReceiveMessagesOperation"}(w||(w={}));var M=w;class A{constructor(e){this.configuration=e,this.subscriptionWorkerReady=!1,this.accessTokensMap={},this.workerEventsQueue=[],this.callbacks=new Map,this.setupSubscriptionWorker()}set emitStatus(e){this._emitStatus=e}onUserIdChange(e){this.configuration.userId=e,this.scheduleEventPost({type:"client-update",heartbeatInterval:this.configuration.heartbeatInterval,clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,userId:this.configuration.userId})}onHeartbeatIntervalChange(e){this.configuration.heartbeatInterval=e,this.scheduleEventPost({type:"client-update",heartbeatInterval:this.configuration.heartbeatInterval,clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,userId:this.configuration.userId})}onTokenChange(e){const t={type:"client-update",heartbeatInterval:this.configuration.heartbeatInterval,clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,userId:this.configuration.userId};this.parsedAccessToken(e).then((s=>{t.preProcessedToken=s,t.accessToken=e})).then((()=>this.scheduleEventPost(t)))}terminate(){this.scheduleEventPost({type:"client-unregister",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey})}makeSendable(e){if(!e.path.startsWith("/v2/subscribe")&&!e.path.endsWith("/heartbeat")&&!e.path.endsWith("/leave"))return this.configuration.transport.makeSendable(e);let t;this.configuration.logger.debug("SubscriptionWorkerMiddleware","Process request with SharedWorker transport.");const s={type:"send-request",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,request:e};return e.cancellable&&(t={abort:()=>{const t={type:"cancel-request",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,identifier:e.identifier};this.scheduleEventPost(t)}}),[new Promise(((t,n)=>{this.callbacks.set(e.identifier,{resolve:t,reject:n}),this.parsedAccessTokenForRequest(e).then((e=>s.preProcessedToken=e)).then((()=>this.scheduleEventPost(s)))})),t]}request(e){return e}scheduleEventPost(e,t=!1){const s=this.sharedSubscriptionWorker;s?s.port.postMessage(e):t?this.workerEventsQueue.splice(0,0,e):this.workerEventsQueue.push(e)}flushScheduledEvents(){const e=this.sharedSubscriptionWorker;if(!e||0===this.workerEventsQueue.length)return;const t=[];for(let e=0;e!t.includes(e))),this.workerEventsQueue.forEach((t=>e.port.postMessage(t))),this.workerEventsQueue=[]}get sharedSubscriptionWorker(){return this.subscriptionWorkerReady?this.subscriptionWorker:null}setupSubscriptionWorker(){if("undefined"!=typeof SharedWorker){try{this.subscriptionWorker=new SharedWorker(this.configuration.workerUrl,`/pubnub-${this.configuration.sdkVersion}`)}catch(e){throw this.configuration.logger.error("SubscriptionWorkerMiddleware",(()=>({messageType:"error",message:e}))),e}this.subscriptionWorker.port.start(),this.scheduleEventPost({type:"client-register",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,userId:this.configuration.userId,heartbeatInterval:this.configuration.heartbeatInterval,workerOfflineClientsCheckInterval:this.configuration.workerOfflineClientsCheckInterval,workerUnsubscribeOfflineClients:this.configuration.workerUnsubscribeOfflineClients,workerLogVerbosity:this.configuration.workerLogVerbosity},!0),this.subscriptionWorker.port.onmessage=e=>this.handleWorkerEvent(e)}}handleWorkerEvent(e){const{data:t}=e;if("shared-worker-ping"===t.type||"shared-worker-connected"===t.type||"shared-worker-console-log"===t.type||"shared-worker-console-dir"===t.type||t.clientIdentifier===this.configuration.clientIdentifier)if("shared-worker-connected"===t.type)this.configuration.logger.trace("SharedWorker","Ready for events processing."),this.subscriptionWorkerReady=!0,this.flushScheduledEvents();else if("shared-worker-console-log"===t.type)this.configuration.logger.debug("SharedWorker",(()=>"string"==typeof t.message||"number"==typeof t.message||"boolean"==typeof t.message?{messageType:"text",message:t.message}:t.message));else if("shared-worker-console-dir"===t.type)this.configuration.logger.debug("SharedWorker",(()=>({messageType:"object",message:t.data,details:t.message?t.message:void 0})));else if("shared-worker-ping"===t.type){const{subscriptionKey:e,clientIdentifier:t}=this.configuration;this.scheduleEventPost({type:"client-pong",subscriptionKey:e,clientIdentifier:t})}else if("request-process-success"===t.type||"request-process-error"===t.type)if(this.callbacks.has(t.identifier)){const{resolve:e,reject:s}=this.callbacks.get(t.identifier);this.callbacks.delete(t.identifier),"request-process-success"===t.type?e({status:t.response.status,url:t.url,headers:t.response.headers,body:t.response.body}):s(this.errorFromRequestSendingError(t))}else this._emitStatus&&t.url.indexOf("/v2/presence")>=0&&t.url.indexOf("/heartbeat")>=0&&("request-process-success"===t.type&&this.configuration.announceSuccessfulHeartbeats?this._emitStatus({statusCode:t.response.status,error:!1,operation:M.PNHeartbeatOperation,category:h.PNAcknowledgmentCategory}):"request-process-error"===t.type&&this.configuration.announceFailedHeartbeats&&this._emitStatus(this.errorFromRequestSendingError(t).toStatus(M.PNHeartbeatOperation)))}parsedAccessTokenForRequest(e){return i(this,void 0,void 0,(function*(){var t;return this.parsedAccessToken(e.queryParameters?null!==(t=e.queryParameters.auth)&&void 0!==t?t:"":void 0)}))}parsedAccessToken(e){return i(this,void 0,void 0,(function*(){if(e)return this.accessTokensMap[e]?this.accessTokensMap[e]:this.stringifyAccessToken(e).then((([t,s])=>{if(t&&s)return(this.accessTokensMap={[e]:{token:s,expiration:t.timestamp*t.ttl*60}})[e]}))}))}stringifyAccessToken(e){return i(this,void 0,void 0,(function*(){if(!this.configuration.tokenManager)return[void 0,void 0];const t=this.configuration.tokenManager.parseToken(e);if(!t)return[void 0,void 0];const s=e=>e?Object.entries(e).sort((([e],[t])=>e.localeCompare(t))).map((([e,t])=>Object.entries(t||{}).sort((([e],[t])=>e.localeCompare(t))).map((([t,s])=>{return`${e}:${t}=${s?(n=s,Object.entries(n).filter((([e,t])=>t)).map((([e])=>e[0])).sort().join("")):""}`;var n})).join(","))).join(";"):"";let n=[s(t.resources),s(t.patterns),t.authorized_uuid].filter(Boolean).join("|");if("undefined"!=typeof crypto&&crypto.subtle){const e=yield crypto.subtle.digest("SHA-256",(new TextEncoder).encode(n));n=String.fromCharCode(...Array.from(new Uint8Array(e)))}return[t,"undefined"!=typeof btoa?btoa(n):n]}))}errorFromRequestSendingError(e){let t=h.PNUnknownCategory,s="Unknown error";if(e.error)"NETWORK_ISSUE"===e.error.type?t=h.PNNetworkIssuesCategory:"TIMEOUT"===e.error.type?t=h.PNTimeoutCategory:"ABORTED"===e.error.type&&(t=h.PNCancelledCategory),s=`${e.error.message} (${e.identifier})`;else if(e.response){const{url:t,response:s}=e;return I.create({url:t,headers:s.headers,body:s.body,status:s.status},s.body)}return new I(s,t,0,new Error(s))}}function U(e,t=0){const s=e=>"object"==typeof e&&null!==e&&e.constructor===Object,n=e=>"number"==typeof e&&isFinite(e);if(!s(e))return e;const r={};return Object.keys(e).forEach((i=>{const a=(e=>"string"==typeof e||e instanceof String)(i);let o=i;const c=e[i];if(t<2)if(a&&i.indexOf(",")>=0){o=i.split(",").map(Number).reduce(((e,t)=>e+String.fromCharCode(t)),"")}else(n(i)||a&&!isNaN(Number(i)))&&(o=String.fromCharCode(n(i)?i:parseInt(i,10)));r[o]=s(c)?U(c,t+1):c})),r}const R=e=>{var t,s,n,r,i,a;return e.subscriptionWorkerUrl&&"undefined"==typeof SharedWorker&&(e.subscriptionWorkerUrl=null),Object.assign(Object.assign({},(e=>{var t,s,n,r,i,a,o,c,u,l,h,p,g,b,y;const m=Object.assign({},e);if(null!==(t=m.ssl)&&void 0!==t||(m.ssl=!0),null!==(s=m.transactionalRequestTimeout)&&void 0!==s||(m.transactionalRequestTimeout=15),null!==(n=m.subscribeRequestTimeout)&&void 0!==n||(m.subscribeRequestTimeout=310),null!==(r=m.fileRequestTimeout)&&void 0!==r||(m.fileRequestTimeout=300),null!==(i=m.restore)&&void 0!==i||(m.restore=!1),null!==(a=m.useInstanceId)&&void 0!==a||(m.useInstanceId=!1),null!==(o=m.suppressLeaveEvents)&&void 0!==o||(m.suppressLeaveEvents=!1),null!==(c=m.requestMessageCountThreshold)&&void 0!==c||(m.requestMessageCountThreshold=100),null!==(u=m.autoNetworkDetection)&&void 0!==u||(m.autoNetworkDetection=!1),null!==(l=m.enableEventEngine)&&void 0!==l||(m.enableEventEngine=!1),null!==(h=m.maintainPresenceState)&&void 0!==h||(m.maintainPresenceState=!0),null!==(p=m.useSmartHeartbeat)&&void 0!==p||(m.useSmartHeartbeat=!1),null!==(g=m.keepAlive)&&void 0!==g||(m.keepAlive=!1),m.userId&&m.uuid)throw new d("PubNub client configuration error: use only 'userId'");if(null!==(b=m.userId)&&void 0!==b||(m.userId=m.uuid),!m.userId)throw new d("PubNub client configuration error: 'userId' not set");if(0===(null===(y=m.userId)||void 0===y?void 0:y.trim().length))throw new d("PubNub client configuration error: 'userId' is empty");m.origin||(m.origin=Array.from({length:20},((e,t)=>`ps${t+1}.pndsn.com`)));const f={subscribeKey:m.subscribeKey,publishKey:m.publishKey,secretKey:m.secretKey};void 0!==m.presenceTimeout&&(m.presenceTimeout>320?(m.presenceTimeout=320,console.warn("WARNING: Presence timeout is larger than the maximum. Using maximum value: ",320)):m.presenceTimeout<=0&&(console.warn("WARNING: Presence timeout should be larger than zero."),delete m.presenceTimeout)),void 0!==m.presenceTimeout?m.heartbeatInterval=m.presenceTimeout/2-1:m.presenceTimeout=300;let v=!1,S=!0,w=5,O=!1,k=100,C=!0;return void 0!==m.dedupeOnSubscribe&&"boolean"==typeof m.dedupeOnSubscribe&&(O=m.dedupeOnSubscribe),void 0!==m.maximumCacheSize&&"number"==typeof m.maximumCacheSize&&(k=m.maximumCacheSize),void 0!==m.useRequestId&&"boolean"==typeof m.useRequestId&&(C=m.useRequestId),void 0!==m.announceSuccessfulHeartbeats&&"boolean"==typeof m.announceSuccessfulHeartbeats&&(v=m.announceSuccessfulHeartbeats),void 0!==m.announceFailedHeartbeats&&"boolean"==typeof m.announceFailedHeartbeats&&(S=m.announceFailedHeartbeats),void 0!==m.fileUploadPublishRetryLimit&&"number"==typeof m.fileUploadPublishRetryLimit&&(w=m.fileUploadPublishRetryLimit),Object.assign(Object.assign({},m),{keySet:f,dedupeOnSubscribe:O,maximumCacheSize:k,useRequestId:C,announceSuccessfulHeartbeats:v,announceFailedHeartbeats:S,fileUploadPublishRetryLimit:w})})(e)),{listenToBrowserNetworkEvents:null===(t=e.listenToBrowserNetworkEvents)||void 0===t||t,subscriptionWorkerUrl:e.subscriptionWorkerUrl,subscriptionWorkerOfflineClientsCheckInterval:null!==(s=e.subscriptionWorkerOfflineClientsCheckInterval)&&void 0!==s?s:10,subscriptionWorkerUnsubscribeOfflineClients:null!==(n=e.subscriptionWorkerUnsubscribeOfflineClients)&&void 0!==n&&n,subscriptionWorkerLogVerbosity:null!==(r=e.subscriptionWorkerLogVerbosity)&&void 0!==r&&r,transport:null!==(i=e.transport)&&void 0!==i?i:"fetch",keepAlive:null===(a=e.keepAlive)||void 0===a||a})};var F;!function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Info=2]="Info",e[e.Warn=3]="Warn",e[e.Error=4]="Error",e[e.None=5]="None"}(F||(F={}));const $=e=>encodeURIComponent(e).replace(/[!~*'()]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`)),D=(e,t)=>{const s=e.map((e=>$(e)));return s.length?s.join(","):null!=t?t:""},x=(e,t)=>{const s=Object.fromEntries(t.map((e=>[e,!1])));return e.filter((e=>!(t.includes(e)&&!s[e])||(s[e]=!0,!1)))},q=(e,t)=>[...e].filter((s=>t.includes(s)&&e.indexOf(s)===e.lastIndexOf(s)&&t.indexOf(s)===t.lastIndexOf(s))),G=e=>Object.keys(e).map((t=>{const s=e[t];return Array.isArray(s)?s.map((e=>`${t}=${$(e)}`)).join("&"):`${t}=${$(s)}`})).join("&"),K=(e,t)=>{if("0"===t||"0"===e)return;const s=H(`${Date.now()}0000`,t,!1);return H(e,s,!0)},L=(e,t,s)=>{if(e&&0!==e.length){if(t&&t.length>0&&"0"!==t){const n=H(e,t,!1);return H(null!=s?s:`${Date.now()}0000`,n.replace("-",""),Number(n)<0)}return s&&s.length>0&&"0"!==s?s:`${Date.now()}0000`}},H=(e,t,s)=>{t.startsWith("-")&&(t=t.replace("-",""),s=!1),t=t.padStart(17,"0");const n=e.slice(0,10),r=e.slice(10,17),i=t.slice(0,10),a=t.slice(10,17);let o=Number(n),c=Number(r);return o+=Number(i)*(s?1:-1),c+=Number(a)*(s?1:-1),c>=1e7?(o+=Math.floor(c/1e7),c%=1e7):c<0?o>0?(o-=1,c+=1e7):o<0&&(c*=-1):o<0&&c>0&&(o+=1,c=1e7-c),0!==o?`${o}${`${c}`.padStart(7,"0")}`:`${c}`},B=e=>{const t="string"!=typeof e?JSON.stringify(e):e,s=new Uint32Array(1);let n=0,r=t.length;for(;r-- >0;)s[0]=(s[0]<<5)-s[0]+t.charCodeAt(n++);return s[0].toString(16).padStart(8,"0")};class W{debug(e){this.log(e)}error(e){this.log(e)}info(e){this.log(e)}trace(e){this.log(e)}warn(e){this.log(e)}log(e){const t=F[e.level],s=t.toLowerCase();console["trace"===s?"debug":s](`${e.timestamp.toISOString()} PubNub-${e.pubNubId} ${t.padEnd(5," ")}${e.location?` ${e.location}`:""} ${this.logMessage(e)}`)}logMessage(e){if("text"===e.messageType)return e.message;if("object"===e.messageType)return`${e.details?`${e.details}\n`:""}${this.formattedObject(e)}`;if("network-request"===e.messageType){const t=!!e.canceled||!!e.failed,s=e.minimumLevel!==F.Trace||t?void 0:this.formattedHeaders(e),n=e.message,r=n.queryParameters&&Object.keys(n.queryParameters).length>0?G(n.queryParameters):void 0,i=`${n.origin}${n.path}${r?`?${r}`:""}`,a=t?void 0:this.formattedBody(e);let o="Sending";t&&(o=`${e.canceled?"Canceled":"Failed"}${e.details?` (${e.details})`:""}`);const c=((null==a?void 0:a.formData)?"FormData":"Method").length;return`${o} HTTP request:\n ${this.paddedString("Method",c)}: ${n.method}\n ${this.paddedString("URL",c)}: ${i}${s?`\n ${this.paddedString("Headers",c)}:\n${s}`:""}${(null==a?void 0:a.formData)?`\n ${this.paddedString("FormData",c)}:\n${a.formData}`:""}${(null==a?void 0:a.body)?`\n ${this.paddedString("Body",c)}:\n${a.body}`:""}`}if("network-response"===e.messageType){const t=e.minimumLevel===F.Trace?this.formattedHeaders(e):void 0,s=this.formattedBody(e),n=((null==s?void 0:s.formData)?"Headers":"Status").length,r=e.message;return`Received HTTP response:\n ${this.paddedString("URL",n)}: ${r.url}\n ${this.paddedString("Status",n)}: ${r.status}${t?`\n ${this.paddedString("Headers",n)}:\n${t}`:""}${(null==s?void 0:s.body)?`\n ${this.paddedString("Body",n)}:\n${s.body}`:""}`}if("error"===e.messageType){const t=this.formattedErrorStatus(e),s=e.message;return`${s.name}: ${s.message}${t?`\n${t}`:""}`}return""}formattedObject(e){const t=(s,n=1,r=!1)=>{const i=10===n,a=" ".repeat(2*n),o=[],c=(t,s)=>!!e.ignoredKeys&&("function"==typeof e.ignoredKeys?e.ignoredKeys(t,s):e.ignoredKeys.includes(t));if("string"==typeof s)o.push(`${a}- ${s}`);else if("number"==typeof s)o.push(`${a}- ${s}`);else if("boolean"==typeof s)o.push(`${a}- ${s}`);else if(null===s)o.push(`${a}- null`);else if(void 0===s)o.push(`${a}- undefined`);else if("function"==typeof s)o.push(`${a}- `);else if("object"==typeof s)if(Array.isArray(s)||"function"!=typeof s.toString||0===s.toString().indexOf("[object"))if(Array.isArray(s))for(const e of s){const s=r?"":a;if(null===e)o.push(`${s}- null`);else if(void 0===e)o.push(`${s}- undefined`);else if("function"==typeof e)o.push(`${s}- `);else if("object"==typeof e){const r=Array.isArray(e),a=i?"...":t(e,n+1,!r);o.push(`${s}-${r&&!i?"\n":" "}${a}`)}else o.push(`${s}- ${e}`);r=!1}else{const e=s,u=Object.keys(e),l=u.reduce(((t,s)=>Math.max(t,c(s,e)?t:s.length)),0);for(const s of u){if(c(s,e))continue;const u=r?"":a,h=e[s],d=s.padEnd(l," ");if(null===h)o.push(`${u}${d}: null`);else if(void 0===h)o.push(`${u}${d}: undefined`);else if("function"==typeof h)o.push(`${u}${d}: `);else if("object"==typeof h){const e=Array.isArray(h),s=e&&0===h.length,r=!(e||h instanceof String||0!==Object.keys(h).length),a=!e&&"function"==typeof h.toString&&0!==h.toString().indexOf("[object"),c=i?"...":s?"[]":r?"{}":t(h,n+1,a);o.push(`${u}${d}:${i||a||s||r?" ":"\n"}${c}`)}else o.push(`${u}${d}: ${h}`);r=!1}}else o.push(`${r?"":a}${s.toString()}`),r=!1;return o.join("\n")};return t(e.message)}formattedHeaders(e){if(!e.message.headers)return;const t=e.message.headers,s=Object.keys(t).reduce(((e,t)=>Math.max(e,t.length)),0);return Object.keys(t).map((e=>` - ${e.toLowerCase().padEnd(s," ")}: ${t[e]}`)).join("\n")}formattedBody(e){var t;if(!e.message.headers)return;let s,n;const r=e.message.headers,i=null!==(t=r["content-type"])&&void 0!==t?t:r["Content-Type"],a="formData"in e.message?e.message.formData:void 0,o=e.message.body;if(a){const e=a.reduce(((e,{key:t})=>Math.max(e,t.length)),0);s=a.map((({key:t,value:s})=>` - ${t.padEnd(e," ")}: ${s}`)).join("\n")}return o?(n="string"==typeof o?` ${o}`:o instanceof ArrayBuffer||"[object ArrayBuffer]"===Object.prototype.toString.call(o)?!i||-1===i.indexOf("javascript")&&-1===i.indexOf("json")?` ArrayBuffer { byteLength: ${o.byteLength} }`:` ${W.decoder.decode(o)}`:` File { name: ${o.name}${o.contentLength?`, contentLength: ${o.contentLength}`:""}${o.mimeType?`, mimeType: ${o.mimeType}`:""} }`,{body:n,formData:s}):{formData:s}}formattedErrorStatus(e){if(!e.message.status)return;const t=e.message.status,s=t.errorData;let n;if(W.isError(s))n=` ${s.name}: ${s.message}`,s.stack&&(n+=`\n${s.stack.split("\n").map((e=>` ${e}`)).join("\n")}`);else if(s)try{n=` ${JSON.stringify(s)}`}catch(e){n=` ${s}`}return` Category : ${t.category}\n Operation : ${t.operation}\n Status : ${t.statusCode}${n?`\n Error data:\n${n}`:""}`}paddedString(e,t){return e.padEnd(t-e.length," ")}static isError(e){return!!e&&(e instanceof Error||"[object Error]"===Object.prototype.toString.call(e))}}var z;W.decoder=new TextDecoder,function(e){e.Unknown="UnknownEndpoint",e.MessageSend="MessageSendEndpoint",e.Subscribe="SubscribeEndpoint",e.Presence="PresenceEndpoint",e.Files="FilesEndpoint",e.MessageStorage="MessageStorageEndpoint",e.ChannelGroups="ChannelGroupsEndpoint",e.DevicePushNotifications="DevicePushNotificationsEndpoint",e.AppContext="AppContextEndpoint",e.MessageReactions="MessageReactionsEndpoint"}(z||(z={}));class V{static None(){return{shouldRetry:(e,t,s,n)=>!1,getDelay:(e,t)=>-1,validate:()=>!0}}static LinearRetryPolicy(e){var t;return{delay:e.delay,maximumRetry:e.maximumRetry,excluded:null!==(t=e.excluded)&&void 0!==t?t:[],shouldRetry(e,t,s,n){return J(e,t,s,null!=n?n:0,this.maximumRetry,this.excluded)},getDelay(e,t){let s=-1;return t&&void 0!==t.headers["retry-after"]&&(s=parseInt(t.headers["retry-after"],10)),-1===s&&(s=this.delay),1e3*(s+Math.random())},validate(){if(this.delay<2)throw new Error("Delay can not be set less than 2 seconds for retry");if(this.maximumRetry>10)throw new Error("Maximum retry for linear retry policy can not be more than 10")}}}static ExponentialRetryPolicy(e){var t;return{minimumDelay:e.minimumDelay,maximumDelay:e.maximumDelay,maximumRetry:e.maximumRetry,excluded:null!==(t=e.excluded)&&void 0!==t?t:[],shouldRetry(e,t,s,n){return J(e,t,s,null!=n?n:0,this.maximumRetry,this.excluded)},getDelay(e,t){let s=-1;return t&&void 0!==t.headers["retry-after"]&&(s=parseInt(t.headers["retry-after"],10)),-1===s&&(s=Math.min(Math.pow(2,e),this.maximumDelay)),1e3*(s+Math.random())},validate(){if(this.minimumDelay<2)throw new Error("Minimum delay can not be set less than 2 seconds for retry");if(this.maximumDelay>150)throw new Error("Maximum delay can not be set more than 150 seconds for retry");if(this.maximumRetry>6)throw new Error("Maximum retry for exponential retry policy can not be more than 6")}}}}const J=(e,t,s,n,r,i)=>(!s||s!==h.PNCancelledCategory&&s!==h.PNBadRequestCategory&&s!==h.PNAccessDeniedCategory)&&(!X(e,i)&&(!(n>r)&&(!t||(429===t.status||t.status>=500)))),X=(e,t)=>!!(t&&t.length>0)&&t.includes(Q(e)),Q=e=>{let t=z.Unknown;return e.path.startsWith("/v2/subscribe")?t=z.Subscribe:e.path.startsWith("/publish/")||e.path.startsWith("/signal/")?t=z.MessageSend:e.path.startsWith("/v2/presence")?t=z.Presence:e.path.startsWith("/v2/history")||e.path.startsWith("/v3/history")?t=z.MessageStorage:e.path.startsWith("/v1/message-actions/")?t=z.MessageReactions:e.path.startsWith("/v1/channel-registration/")||e.path.startsWith("/v2/objects/")?t=z.ChannelGroups:e.path.startsWith("/v1/push/")||e.path.startsWith("/v2/push/")?t=z.DevicePushNotifications:e.path.startsWith("/v1/files/")&&(t=z.Files),t};class Y{constructor(e,t,s){this.pubNubId=e,this.minLogLevel=t,this.loggers=s}get logLevel(){return this.minLogLevel}trace(e,t){this.log(F.Trace,e,t)}debug(e,t){this.log(F.Debug,e,t)}info(e,t){this.log(F.Info,e,t)}warn(e,t){this.log(F.Warn,e,t)}error(e,t){this.log(F.Error,e,t)}log(e,t,s){if(ee[n](r)))}}var Z={exports:{}}; -/*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */!function(e,t){!function(e){var t="0.1.0",s={3:/^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,4:/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,5:/^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,all:/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i};function n(){var e,t,s="";for(e=0;e<32;e++)t=16*Math.random()|0,8!==e&&12!==e&&16!==e&&20!==e||(s+="-"),s+=(12===e?4:16===e?3&t|8:t).toString(16);return s}function r(e,t){var n=s[t||"all"];return n&&n.test(e)||!1}n.isUUID=r,n.VERSION=t,e.uuid=n,e.isUUID=r}(t),null!==e&&(e.exports=t.uuid)}(Z,Z.exports);var ee=t(Z.exports),te={createUUID:()=>ee.uuid?ee.uuid():ee()};const se=(e,t)=>{var s,n,r,i;!e.retryConfiguration&&e.enableEventEngine&&(e.retryConfiguration=V.ExponentialRetryPolicy({minimumDelay:2,maximumDelay:150,maximumRetry:6,excluded:[z.MessageSend,z.Presence,z.Files,z.MessageStorage,z.ChannelGroups,z.DevicePushNotifications,z.AppContext,z.MessageReactions]}));const a=`pn-${te.createUUID()}`;e.logVerbosity?e.logLevel=F.Debug:void 0===e.logLevel&&(e.logLevel=F.None);const o=new Y(re(a),e.logLevel,[...null!==(s=e.loggers)&&void 0!==s?s:[],new W]);void 0!==e.logVerbosity&&o.warn("Configuration","'logVerbosity' is deprecated. Use 'logLevel' instead."),null===(n=e.retryConfiguration)||void 0===n||n.validate(),null!==(r=e.useRandomIVs)&&void 0!==r||(e.useRandomIVs=true),e.useRandomIVs&&o.warn("Configuration","'useRandomIVs' is deprecated. Use 'cryptoModule' instead."),e.origin=ne(null!==(i=e.ssl)&&void 0!==i&&i,e.origin);const c=e.cryptoModule;c&&delete e.cryptoModule;const u=Object.assign(Object.assign({},e),{_pnsdkSuffix:{},_loggerManager:o,_instanceId:a,_cryptoModule:void 0,_cipherKey:void 0,_setupCryptoModule:t,get instanceId(){if(e.useInstanceId)return this._instanceId},getInstanceId(){if(e.useInstanceId)return this._instanceId},getUserId(){return this.userId},setUserId(e){if(!e||"string"!=typeof e||0===e.trim().length)throw new Error("Missing or invalid userId parameter. Provide a valid string userId");this.userId=e},logger(){return this._loggerManager},getAuthKey(){return this.authKey},setAuthKey(e){this.authKey=e},getFilterExpression(){return this.filterExpression},setFilterExpression(e){this.filterExpression=e},getCipherKey(){return this._cipherKey},setCipherKey(t){this._cipherKey=t,t||!this._cryptoModule?t&&this._setupCryptoModule&&(this._cryptoModule=this._setupCryptoModule({cipherKey:t,useRandomIVs:e.useRandomIVs,customEncrypt:this.getCustomEncrypt(),customDecrypt:this.getCustomDecrypt(),logger:this.logger()})):this._cryptoModule=void 0},getCryptoModule(){return this._cryptoModule},getUseRandomIVs:()=>e.useRandomIVs,getKeepPresenceChannelsInPresenceRequests:()=>"Web"===e.sdkFamily&&e.subscriptionWorkerUrl,setPresenceTimeout(e){this.heartbeatInterval=e/2-1,this.presenceTimeout=e},getPresenceTimeout(){return this.presenceTimeout},getHeartbeatInterval(){return this.heartbeatInterval},setHeartbeatInterval(e){this.heartbeatInterval=e},getTransactionTimeout(){return this.transactionalRequestTimeout},getSubscribeTimeout(){return this.subscribeRequestTimeout},getFileTimeout(){return this.fileRequestTimeout},get PubNubFile(){return e.PubNubFile},get version(){return"9.8.4"},getVersion(){return this.version},_addPnsdkSuffix(e,t){this._pnsdkSuffix[e]=`${t}`},_getPnsdkSuffix(e){const t=Object.values(this._pnsdkSuffix).join(e);return t.length>0?e+t:""},getUUID(){return this.getUserId()},setUUID(e){this.setUserId(e)},getCustomEncrypt:()=>e.customEncrypt,getCustomDecrypt:()=>e.customDecrypt});return e.cipherKey?(o.warn("Configuration","'cipherKey' is deprecated. Use 'cryptoModule' instead."),u.setCipherKey(e.cipherKey)):c&&(u._cryptoModule=c),u},ne=(e,t)=>{const s=e?"https://":"http://";return"string"==typeof t?`${s}${t}`:`${s}${t[Math.floor(Math.random()*t.length)]}`},re=e=>{let t=2166136261;for(let s=0;s>>0;return t.toString(16).padStart(8,"0")};class ie{constructor(e){this.cbor=e}setToken(e){e&&e.length>0?this.token=e:this.token=void 0}getToken(){return this.token}parseToken(e){const t=this.cbor.decodeToken(e);if(void 0!==t){const e=t.res.uuid?Object.keys(t.res.uuid):[],s=Object.keys(t.res.chan),n=Object.keys(t.res.grp),r=t.pat.uuid?Object.keys(t.pat.uuid):[],i=Object.keys(t.pat.chan),a=Object.keys(t.pat.grp),o={version:t.v,timestamp:t.t,ttl:t.ttl,authorized_uuid:t.uuid,signature:t.sig},c=e.length>0,u=s.length>0,l=n.length>0;if(c||u||l){if(o.resources={},c){const s=o.resources.uuids={};e.forEach((e=>s[e]=this.extractPermissions(t.res.uuid[e])))}if(u){const e=o.resources.channels={};s.forEach((s=>e[s]=this.extractPermissions(t.res.chan[s])))}if(l){const e=o.resources.groups={};n.forEach((s=>e[s]=this.extractPermissions(t.res.grp[s])))}}const h=r.length>0,d=i.length>0,p=a.length>0;if(h||d||p){if(o.patterns={},h){const e=o.patterns.uuids={};r.forEach((s=>e[s]=this.extractPermissions(t.pat.uuid[s])))}if(d){const e=o.patterns.channels={};i.forEach((s=>e[s]=this.extractPermissions(t.pat.chan[s])))}if(p){const e=o.patterns.groups={};a.forEach((s=>e[s]=this.extractPermissions(t.pat.grp[s])))}}return t.meta&&Object.keys(t.meta).length>0&&(o.meta=t.meta),o}}extractPermissions(e){const t={read:!1,write:!1,manage:!1,delete:!1,get:!1,update:!1,join:!1};return 128&~e||(t.join=!0),64&~e||(t.update=!0),32&~e||(t.get=!0),8&~e||(t.delete=!0),4&~e||(t.manage=!0),2&~e||(t.write=!0),1&~e||(t.read=!0),t}}var ae;!function(e){e.GET="GET",e.POST="POST",e.PATCH="PATCH",e.DELETE="DELETE",e.LOCAL="LOCAL"}(ae||(ae={}));class oe{constructor(e,t,s,n){this.publishKey=e,this.secretKey=t,this.hasher=s,this.logger=n}signature(e){const t=e.path.startsWith("/publish")?ae.GET:e.method;let s=`${t}\n${this.publishKey}\n${e.path}\n${this.queryParameters(e.queryParameters)}\n`;if(t===ae.POST||t===ae.PATCH){const t=e.body;let n;t&&t instanceof ArrayBuffer?n=oe.textDecoder.decode(t):t&&"object"!=typeof t&&(n=t),n&&(s+=n)}return this.logger.trace("RequestSignature",(()=>({messageType:"text",message:`Request signature input:\n${s}`}))),`v2.${this.hasher(s,this.secretKey)}`.replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}queryParameters(e){return Object.keys(e).sort().map((t=>{const s=e[t];return Array.isArray(s)?s.sort().map((e=>`${t}=${$(e)}`)).join("&"):`${t}=${$(s)}`})).join("&")}}oe.textDecoder=new TextDecoder("utf-8");class ce{constructor(e){this.configuration=e;const{clientConfiguration:{keySet:t},shaHMAC:s}=e;t.secretKey&&s&&(this.signatureGenerator=new oe(t.publishKey,t.secretKey,s,this.logger))}get logger(){return this.configuration.clientConfiguration.logger()}makeSendable(e){const t=this.configuration.clientConfiguration.retryConfiguration,s=this.configuration.transport;if(void 0!==t){let n,r,i=!1,a=0;const o={abort:e=>{i=!0,n&&clearTimeout(n),r&&r.abort(e)}};return[new Promise(((o,c)=>{const u=()=>{if(i)return;const[l,d]=s.makeSendable(this.request(e));r=d;const p=(s,r)=>{const i=!r||r.category!==h.PNCancelledCategory,l=!s||s.status>=400;let d=-1;i&&l&&t.shouldRetry(e,s,null==r?void 0:r.category,a+1)&&(d=t.getDelay(a,s)),d>0?(a++,this.logger.warn("PubNubMiddleware",`HTTP request retry #${a} in ${d}ms.`),n=setTimeout((()=>u()),d)):s?o(s):r&&c(r)};l.then((e=>p(e))).catch((e=>p(void 0,e)))};u()})),r?o:void 0]}return s.makeSendable(this.request(e))}request(e){var t;const{clientConfiguration:s}=this.configuration;return(e=this.configuration.transport.request(e)).queryParameters||(e.queryParameters={}),s.useInstanceId&&(e.queryParameters.instanceid=s.getInstanceId()),e.queryParameters.uuid||(e.queryParameters.uuid=s.userId),s.useRequestId&&(e.queryParameters.requestid=e.identifier),e.queryParameters.pnsdk=this.generatePNSDK(),null!==(t=e.origin)&&void 0!==t||(e.origin=s.origin),this.authenticateRequest(e),this.signRequest(e),e}authenticateRequest(e){var t;if(e.path.startsWith("/v2/auth/")||e.path.startsWith("/v3/pam/")||e.path.startsWith("/time"))return;const{clientConfiguration:s,tokenManager:n}=this.configuration,r=null!==(t=n&&n.getToken())&&void 0!==t?t:s.authKey;r&&(e.queryParameters.auth=r)}signRequest(e){this.signatureGenerator&&!e.path.startsWith("/time")&&(e.queryParameters.timestamp=String(Math.floor((new Date).getTime()/1e3)),e.queryParameters.signature=this.signatureGenerator.signature(e))}generatePNSDK(){const{clientConfiguration:e}=this.configuration;if(e.sdkName)return e.sdkName;let t=`PubNub-JS-${e.sdkFamily}`;e.partnerId&&(t+=`-${e.partnerId}`),t+=`/${e.getVersion()}`;const s=e._getPnsdkSuffix(" ");return s.length>0&&(t+=s),t}}class ue{constructor(e,t="fetch"){this.logger=e,this.transport=t,e.debug("WebTransport",`Create with configuration:\n - transport: ${t}`),"fetch"!==t||window&&window.fetch||(e.warn("WebTransport",`'${t}' not supported in this browser. Fallback to the 'xhr' transport.`),this.transport="xhr"),"fetch"===this.transport&&(ue.originalFetch=fetch.bind(window),this.isFetchMonkeyPatched()&&(ue.originalFetch=ue.getOriginalFetch(),e.warn("WebTransport","Native Web Fetch API 'fetch' function monkey patched."),this.isFetchMonkeyPatched(ue.originalFetch)?e.warn("WebTransport","Unable receive native Web Fetch API. There can be issues with subscribe long-poll cancellation"):e.info("WebTransport","Use native Web Fetch API 'fetch' implementation from iframe as APM workaround.")))}makeSendable(e){const t=new AbortController,s={abortController:t,abort:e=>{t.signal.aborted||(this.logger.trace("WebTransport",`On-demand request aborting: ${e}`),t.abort(e))}};return[this.webTransportRequestFromTransportRequest(e).then((t=>(this.logger.debug("WebTransport",(()=>({messageType:"network-request",message:e}))),this.sendRequest(t,s).then((e=>e.arrayBuffer().then((t=>[e,t])))).then((e=>{const s=e[1].byteLength>0?e[1]:void 0,{status:n,headers:r}=e[0],i={};r.forEach(((e,t)=>i[t]=e.toLowerCase()));const a={status:n,url:t.url,headers:i,body:s};if(this.logger.debug("WebTransport",(()=>({messageType:"network-response",message:a}))),n>=400)throw I.create(a);return a})).catch((t=>{const s=("string"==typeof t?t:t.message).toLowerCase();let n="string"==typeof t?new Error(t):t;throw s.includes("timeout")?this.logger.warn("WebTransport",(()=>({messageType:"network-request",message:e,details:"Timeout",canceled:!0}))):s.includes("cancel")||s.includes("abort")?(this.logger.debug("WebTransport",(()=>({messageType:"network-request",message:e,details:"Aborted",canceled:!0}))),n=new Error("Aborted"),n.name="AbortError"):s.includes("network")?this.logger.warn("WebTransport",(()=>({messageType:"network-request",message:e,details:"Network error",failed:!0}))):this.logger.warn("WebTransport",(()=>({messageType:"network-request",message:e,details:I.create(n).message,failed:!0}))),I.create(n)}))))),s]}request(e){return e}sendRequest(e,t){return i(this,void 0,void 0,(function*(){return"fetch"===this.transport?this.sendFetchRequest(e,t):this.sendXHRRequest(e,t)}))}sendFetchRequest(e,t){return i(this,void 0,void 0,(function*(){let s;const n=new Promise(((n,r)=>{s=setTimeout((()=>{clearTimeout(s),r(new Error("Request timeout")),t.abort("Cancel because of timeout")}),1e3*e.timeout)})),r=new Request(e.url,{method:e.method,headers:e.headers,redirect:"follow",body:e.body});return Promise.race([ue.originalFetch(r,{signal:t.abortController.signal,credentials:"omit",cache:"no-cache"}).then((e=>(s&&clearTimeout(s),e))),n])}))}sendXHRRequest(e,t){return i(this,void 0,void 0,(function*(){return new Promise(((s,n)=>{var r;const i=new XMLHttpRequest;i.open(e.method,e.url,!0);let a=!1;i.responseType="arraybuffer",i.timeout=1e3*e.timeout,t.abortController.signal.onabort=()=>{i.readyState!=XMLHttpRequest.DONE&&i.readyState!=XMLHttpRequest.UNSENT&&(a=!0,i.abort())},Object.entries(null!==(r=e.headers)&&void 0!==r?r:{}).forEach((([e,t])=>i.setRequestHeader(e,t))),i.onabort=()=>{n(new Error("Aborted"))},i.ontimeout=()=>{n(new Error("Request timeout"))},i.onerror=()=>{if(!a){const t=this.transportResponseFromXHR(e.url,i);n(new Error(I.create(t).message))}},i.onload=()=>{const e=new Headers;i.getAllResponseHeaders().split("\r\n").forEach((t=>{const[s,n]=t.split(": ");s.length>1&&n.length>1&&e.append(s,n)})),s(new Response(i.response,{status:i.status,headers:e,statusText:i.statusText}))},i.send(e.body)}))}))}webTransportRequestFromTransportRequest(e){return i(this,void 0,void 0,(function*(){let t,s=e.path;if(e.formData&&e.formData.length>0){e.queryParameters={};const s=e.body,n=new FormData;for(const{key:t,value:s}of e.formData)n.append(t,s);try{const e=yield s.toArrayBuffer();n.append("file",new Blob([e],{type:"application/octet-stream"}),s.name)}catch(e){this.logger.warn("WebTransport",(()=>({messageType:"error",message:e})));try{const e=yield s.toFileUri();n.append("file",e,s.name)}catch(e){this.logger.error("WebTransport",(()=>({messageType:"error",message:e})))}}t=n}else if(e.body&&("string"==typeof e.body||e.body instanceof ArrayBuffer))if(e.compressible&&"undefined"!=typeof CompressionStream){const s="string"==typeof e.body?ue.encoder.encode(e.body):e.body,n=s.byteLength,r=new ReadableStream({start(e){e.enqueue(s),e.close()}});t=yield new Response(r.pipeThrough(new CompressionStream("deflate"))).arrayBuffer(),this.logger.trace("WebTransport",(()=>{const e=t.byteLength,s=(e/n).toFixed(2);return{messageType:"text",message:`Body of ${n} bytes, compressed by ${s}x to ${e} bytes.`}}))}else t=e.body;return e.queryParameters&&0!==Object.keys(e.queryParameters).length&&(s=`${s}?${G(e.queryParameters)}`),{url:`${e.origin}${s}`,method:e.method,headers:e.headers,timeout:e.timeout,body:t}}))}isFetchMonkeyPatched(e){return!(null!=e?e:fetch).toString().includes("[native code]")&&"fetch"!==fetch.name}transportResponseFromXHR(e,t){const s=t.getAllResponseHeaders().split("\n"),n={};for(const e of s){const[t,s]=e.trim().split(":");t&&s&&(n[t.toLowerCase()]=s.trim())}return{status:t.status,url:e,headers:n,body:t.response}}static getOriginalFetch(){let e=document.querySelector('iframe[name="pubnub-context-unpatched-fetch"]');return e||(e=document.createElement("iframe"),e.style.display="none",e.name="pubnub-context-unpatched-fetch",e.src="about:blank",document.body.appendChild(e)),e.contentWindow?e.contentWindow.fetch.bind(e.contentWindow):fetch}}ue.encoder=new TextEncoder,ue.decoder=new TextDecoder;class le{constructor(e){this.params=e,this.requestIdentifier=te.createUUID(),this._cancellationController=null}get cancellationController(){return this._cancellationController}set cancellationController(e){this._cancellationController=e}abort(e){this&&this.cancellationController&&this.cancellationController.abort(e)}operation(){throw Error("Should be implemented by subclass.")}validate(){}parse(e){return i(this,void 0,void 0,(function*(){return this.deserializeResponse(e)}))}request(){var e,t,s,n,r,i;const a={method:null!==(t=null===(e=this.params)||void 0===e?void 0:e.method)&&void 0!==t?t:ae.GET,path:this.path,queryParameters:this.queryParameters,cancellable:null!==(n=null===(s=this.params)||void 0===s?void 0:s.cancellable)&&void 0!==n&&n,compressible:null!==(i=null===(r=this.params)||void 0===r?void 0:r.compressible)&&void 0!==i&&i,timeout:10,identifier:this.requestIdentifier},o=this.headers;if(o&&(a.headers=o),a.method===ae.POST||a.method===ae.PATCH){const[e,t]=[this.body,this.formData];t&&(a.formData=t),e&&(a.body=e)}return a}get headers(){var e,t;return Object.assign({"Accept-Encoding":"gzip, deflate"},null!==(t=null===(e=this.params)||void 0===e?void 0:e.compressible)&&void 0!==t&&t?{"Content-Encoding":"deflate"}:{})}get path(){throw Error("`path` getter should be implemented by subclass.")}get queryParameters(){return{}}get formData(){}get body(){}deserializeResponse(e){const t=le.decoder.decode(e.body),s=e.headers["content-type"];let n;if(!s||-1===s.indexOf("javascript")&&-1===s.indexOf("json"))throw new d("Service response error, check status for details",g(t,e.status));try{n=JSON.parse(t)}catch(s){throw console.error("Error parsing JSON response:",s),new d("Service response error, check status for details",g(t,e.status))}if("status"in n&&"number"==typeof n.status&&n.status>=400)throw I.create(e);return n}}le.decoder=new TextDecoder;var he;!function(e){e[e.Presence=-2]="Presence",e[e.Message=-1]="Message",e[e.Signal=1]="Signal",e[e.AppContext=2]="AppContext",e[e.MessageAction=3]="MessageAction",e[e.Files=4]="Files"}(he||(he={}));class de extends le{constructor(e){var t,s,n,r,i,a;super({cancellable:!0}),this.parameters=e,null!==(t=(r=this.parameters).withPresence)&&void 0!==t||(r.withPresence=false),null!==(s=(i=this.parameters).channelGroups)&&void 0!==s||(i.channelGroups=[]),null!==(n=(a=this.parameters).channels)&&void 0!==n||(a.channels=[])}operation(){return M.PNSubscribeOperation}validate(){const{keySet:{subscribeKey:e},channels:t,channelGroups:s}=this.parameters;return e?t||s?void 0:"`channels` and `channelGroups` both should not be empty":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){let t,s;try{s=le.decoder.decode(e.body);t=JSON.parse(s)}catch(e){console.error("Error parsing JSON response:",e)}if(!t)throw new d("Service response error, check status for details",g(s,e.status));const n=t.m.filter((e=>{const t=void 0===e.b?e.c:e.b;return this.parameters.channels&&this.parameters.channels.includes(t)||this.parameters.channelGroups&&this.parameters.channelGroups.includes(t)})).map((e=>{let{e:t}=e;return null!=t||(t=e.c.endsWith("-pnpres")?he.Presence:he.Message),t!=he.Signal&&"string"==typeof e.d?t==he.Message?{type:he.Message,data:this.messageFromEnvelope(e)}:{type:he.Files,data:this.fileFromEnvelope(e)}:t==he.Message?{type:he.Message,data:this.messageFromEnvelope(e)}:t===he.Presence?{type:he.Presence,data:this.presenceEventFromEnvelope(e)}:t==he.Signal?{type:he.Signal,data:this.signalFromEnvelope(e)}:t===he.AppContext?{type:he.AppContext,data:this.appContextFromEnvelope(e)}:t===he.MessageAction?{type:he.MessageAction,data:this.messageActionFromEnvelope(e)}:{type:he.Files,data:this.fileFromEnvelope(e)}}));return{cursor:{timetoken:t.t.t,region:t.t.r},messages:n}}))}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{accept:"text/javascript"})}presenceEventFromEnvelope(e){var t;const{d:s}=e,[n,r]=this.subscriptionChannelFromEnvelope(e),i=n.replace("-pnpres",""),a=null!==r?i:null,o=null!==r?r:i;return"string"!=typeof s&&("data"in s?(s.state=s.data,delete s.data):"action"in s&&"interval"===s.action&&(s.hereNowRefresh=null!==(t=s.here_now_refresh)&&void 0!==t&&t,delete s.here_now_refresh)),Object.assign({channel:i,subscription:r,actualChannel:a,subscribedChannel:o,timetoken:e.p.t},s)}messageFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),[n,r]=this.decryptedData(e.d),i={channel:t,subscription:s,actualChannel:null!==s?t:null,subscribedChannel:null!==s?s:t,timetoken:e.p.t,publisher:e.i,message:n};return e.u&&(i.userMetadata=e.u),e.cmt&&(i.customMessageType=e.cmt),r&&(i.error=r),i}signalFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),n={channel:t,subscription:s,timetoken:e.p.t,publisher:e.i,message:e.d};return e.u&&(n.userMetadata=e.u),e.cmt&&(n.customMessageType=e.cmt),n}messageActionFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),n=e.d;return{channel:t,subscription:s,timetoken:e.p.t,publisher:e.i,event:n.event,data:Object.assign(Object.assign({},n.data),{uuid:e.i})}}appContextFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),n=e.d;return{channel:t,subscription:s,timetoken:e.p.t,message:n}}fileFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),[n,r]=this.decryptedData(e.d);let i=r;const a={channel:t,subscription:s,timetoken:e.p.t,publisher:e.i};return e.u&&(a.userMetadata=e.u),n?"string"==typeof n?null!=i||(i="Unexpected file information payload data type."):(a.message=n.message,n.file&&(a.file={id:n.file.id,name:n.file.name,url:this.parameters.getFileUrl({id:n.file.id,name:n.file.name,channel:t})})):null!=i||(i="File information payload is missing."),e.cmt&&(a.customMessageType=e.cmt),i&&(a.error=i),a}subscriptionChannelFromEnvelope(e){return[e.c,void 0===e.b?e.c:e.b]}decryptedData(e){if(!this.parameters.crypto||"string"!=typeof e)return[e,void 0];let t,s;try{const s=this.parameters.crypto.decrypt(e);t=s instanceof ArrayBuffer?JSON.parse(pe.decoder.decode(s)):s}catch(e){t=null,s=`Error while decrypting message content: ${e.message}`}return[null!=t?t:e,s]}}class pe extends de{get path(){var e;const{keySet:{subscribeKey:t},channels:s}=this.parameters;return`/v2/subscribe/${t}/${D(null!==(e=null==s?void 0:s.sort())&&void 0!==e?e:[],",")}/0`}get queryParameters(){const{channelGroups:e,filterExpression:t,heartbeat:s,state:n,timetoken:r,region:i}=this.parameters,a={};return e&&e.length>0&&(a["channel-group"]=e.sort().join(",")),t&&t.length>0&&(a["filter-expr"]=t),s&&(a.heartbeat=s),n&&Object.keys(n).length>0&&(a.state=JSON.stringify(n)),void 0!==r&&"string"==typeof r?r.length>0&&"0"!==r&&(a.tt=r):void 0!==r&&r>0&&(a.tt=r),i&&(a.tr=i),a}}class ge{constructor(){this.hasListeners=!1,this.listeners=[{count:-1,listener:{}}]}set onStatus(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"status"})}set onMessage(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"message"})}set onPresence(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"presence"})}set onSignal(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"signal"})}set onObjects(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"objects"})}set onMessageAction(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"messageAction"})}set onFile(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"file"})}handleEvent(e){if(this.hasListeners)if(e.type===he.Message)this.announce("message",e.data);else if(e.type===he.Signal)this.announce("signal",e.data);else if(e.type===he.Presence)this.announce("presence",e.data);else if(e.type===he.AppContext){const{data:t}=e,{message:s}=t;if(this.announce("objects",t),"uuid"===s.type){const{message:e,channel:n}=t,i=r(t,["message","channel"]),{event:a,type:o}=s,c=r(s,["event","type"]),u=Object.assign(Object.assign({},i),{spaceId:n,message:Object.assign(Object.assign({},c),{event:"set"===a?"updated":"removed",type:"user"})});this.announce("user",u)}else if("channel"===s.type){const{message:e,channel:n}=t,i=r(t,["message","channel"]),{event:a,type:o}=s,c=r(s,["event","type"]),u=Object.assign(Object.assign({},i),{spaceId:n,message:Object.assign(Object.assign({},c),{event:"set"===a?"updated":"removed",type:"space"})});this.announce("space",u)}else if("membership"===s.type){const{message:e,channel:n}=t,i=r(t,["message","channel"]),{event:a,data:o}=s,c=r(s,["event","data"]),{uuid:u,channel:l}=o,h=r(o,["uuid","channel"]),d=Object.assign(Object.assign({},i),{spaceId:n,message:Object.assign(Object.assign({},c),{event:"set"===a?"updated":"removed",data:Object.assign(Object.assign({},h),{user:u,space:l})})});this.announce("membership",d)}}else e.type===he.MessageAction?this.announce("messageAction",e.data):e.type===he.Files&&this.announce("file",e.data)}handleStatus(e){this.hasListeners&&this.announce("status",e)}addListener(e){this.updateTypeOrObjectListener({add:!0,listener:e})}removeListener(e){this.updateTypeOrObjectListener({add:!1,listener:e})}removeAllListeners(){this.listeners=[{count:-1,listener:{}}],this.hasListeners=!1}updateTypeOrObjectListener(e){if(e.type)"function"==typeof e.listener?this.listeners[0].listener[e.type]=e.listener:delete this.listeners[0].listener[e.type];else if(e.listener&&"function"!=typeof e.listener){let t,s=!1;for(t of this.listeners)if(t.listener===e.listener){e.add?(t.count++,s=!0):(t.count--,0===t.count&&this.listeners.splice(this.listeners.indexOf(t),1));break}e.add&&!s&&this.listeners.push({count:1,listener:e.listener})}this.hasListeners=this.listeners.length>1||Object.keys(this.listeners[0]).length>0}announce(e,t){this.listeners.forEach((({listener:s})=>{const n=s[e];n&&n(t)}))}}class be{constructor(e){this.time=e}onReconnect(e){this.callback=e}startPolling(){this.timeTimer=setInterval((()=>this.callTime()),3e3)}stopPolling(){this.timeTimer&&clearInterval(this.timeTimer),this.timeTimer=null}callTime(){this.time((e=>{e.error||(this.stopPolling(),this.callback&&this.callback())}))}}class ye{constructor(e){this.config=e,e.logger().debug("DedupingManager",(()=>({messageType:"object",message:{maximumCacheSize:e.maximumCacheSize},details:"Create with configuration:"}))),this.maximumCacheSize=e.maximumCacheSize,this.hashHistory=[]}getKey(e){var t;return`${e.timetoken}-${this.hashCode(JSON.stringify(null!==(t=e.message)&&void 0!==t?t:"")).toString()}`}isDuplicate(e){return this.hashHistory.includes(this.getKey(e))}addEntry(e){this.hashHistory.length>=this.maximumCacheSize&&this.hashHistory.shift(),this.hashHistory.push(this.getKey(e))}clearHistory(){this.hashHistory=[]}hashCode(e){let t=0;if(0===e.length)return t;for(let s=0;s{this.pendingChannelSubscriptions.add(e),this.channels[e]={},r&&(this.presenceChannels[e]={}),(i||this.configuration.getHeartbeatInterval())&&(this.heartbeatChannels[e]={})})),null==s||s.forEach((e=>{this.pendingChannelGroupSubscriptions.add(e),this.channelGroups[e]={},r&&(this.presenceChannelGroups[e]={}),(i||this.configuration.getHeartbeatInterval())&&(this.heartbeatChannelGroups[e]={})})),this.subscriptionStatusAnnounced=!1,this.reconnect()}unsubscribe(e,t=!1){let{channels:s,channelGroups:n}=e;const i=new Set,a=new Set;null==s||s.forEach((e=>{e in this.channels&&(delete this.channels[e],a.add(e),e in this.heartbeatChannels&&delete this.heartbeatChannels[e]),e in this.presenceState&&delete this.presenceState[e],e in this.presenceChannels&&(delete this.presenceChannels[e],a.add(e))})),null==n||n.forEach((e=>{e in this.channelGroups&&(delete this.channelGroups[e],i.add(e),e in this.heartbeatChannelGroups&&delete this.heartbeatChannelGroups[e]),e in this.presenceState&&delete this.presenceState[e],e in this.presenceChannelGroups&&(delete this.presenceChannelGroups[e],i.add(e))})),0===a.size&&0===i.size||(!1!==this.configuration.suppressLeaveEvents||t||(n=Array.from(i),s=Array.from(a),this.leaveCall({channels:s,channelGroups:n},(e=>{const{error:t}=e,i=r(e,["error"]);let a;t&&(e.errorData&&"object"==typeof e.errorData&&"message"in e.errorData&&"string"==typeof e.errorData.message?a=e.errorData.message:"message"in e&&"string"==typeof e.message&&(a=e.message)),this.emitStatus(Object.assign(Object.assign({},i),{error:null!=a&&a,affectedChannels:s,affectedChannelGroups:n,currentTimetoken:this.currentTimetoken,lastTimetoken:this.lastTimetoken}))}))),0===Object.keys(this.channels).length&&0===Object.keys(this.presenceChannels).length&&0===Object.keys(this.channelGroups).length&&0===Object.keys(this.presenceChannelGroups).length&&(this.lastTimetoken="0",this.currentTimetoken="0",this.referenceTimetoken=null,this.storedTimetoken=null,this.region=null,this.reconnectionManager.stopPolling()),this.reconnect(!0))}unsubscribeAll(e=!1){this.unsubscribe({channels:this.subscribedChannels,channelGroups:this.subscribedChannelGroups},e)}startSubscribeLoop(e=!1){this.stopSubscribeLoop();const t=[...Object.keys(this.channelGroups)],s=[...Object.keys(this.channels)];Object.keys(this.presenceChannelGroups).forEach((e=>t.push(`${e}-pnpres`))),Object.keys(this.presenceChannels).forEach((e=>s.push(`${e}-pnpres`))),0===s.length&&0===t.length||(this.subscribeCall(Object.assign(Object.assign({channels:s,channelGroups:t,state:this.presenceState,heartbeat:this.configuration.getPresenceTimeout(),timetoken:this.currentTimetoken},null!==this.region?{region:this.region}:{}),this.configuration.filterExpression?{filterExpression:this.configuration.filterExpression}:{}),((e,t)=>{this.processSubscribeResponse(e,t)})),!e&&this.configuration.useSmartHeartbeat&&this.startHeartbeatTimer())}stopSubscribeLoop(){this._subscribeAbort&&(this._subscribeAbort(),this._subscribeAbort=null)}processSubscribeResponse(e,t){if(e.error){if("object"==typeof e.errorData&&"name"in e.errorData&&"AbortError"===e.errorData.name||e.category===h.PNCancelledCategory)return;return void(e.category===h.PNTimeoutCategory?this.startSubscribeLoop():e.category===h.PNNetworkIssuesCategory||e.category===h.PNMalformedResponseCategory?(this.disconnect(),e.error&&this.configuration.autoNetworkDetection&&this.isOnline&&(this.isOnline=!1,this.emitStatus({category:h.PNNetworkDownCategory})),this.reconnectionManager.onReconnect((()=>{this.configuration.autoNetworkDetection&&!this.isOnline&&(this.isOnline=!0,this.emitStatus({category:h.PNNetworkUpCategory})),this.reconnect(),this.subscriptionStatusAnnounced=!0;const t={category:h.PNReconnectedCategory,operation:e.operation,lastTimetoken:this.lastTimetoken,currentTimetoken:this.currentTimetoken};this.emitStatus(t)})),this.reconnectionManager.startPolling(),this.emitStatus(Object.assign(Object.assign({},e),{category:h.PNNetworkIssuesCategory}))):e.category===h.PNBadRequestCategory?(this.stopHeartbeatTimer(),this.emitStatus(e)):this.emitStatus(e))}if(this.referenceTimetoken=L(t.cursor.timetoken,this.storedTimetoken),this.storedTimetoken?(this.currentTimetoken=this.storedTimetoken,this.storedTimetoken=null):(this.lastTimetoken=this.currentTimetoken,this.currentTimetoken=t.cursor.timetoken),!this.subscriptionStatusAnnounced){const t={category:h.PNConnectedCategory,operation:e.operation,affectedChannels:Array.from(this.pendingChannelSubscriptions),subscribedChannels:this.subscribedChannels,affectedChannelGroups:Array.from(this.pendingChannelGroupSubscriptions),lastTimetoken:this.lastTimetoken,currentTimetoken:this.currentTimetoken};this.subscriptionStatusAnnounced=!0,this.emitStatus(t),this.pendingChannelGroupSubscriptions.clear(),this.pendingChannelSubscriptions.clear()}const{messages:s}=t,{requestMessageCountThreshold:n,dedupeOnSubscribe:r}=this.configuration;n&&s.length>=n&&this.emitStatus({category:h.PNRequestMessageCountExceededCategory,operation:e.operation});try{const e={timetoken:this.currentTimetoken,region:this.region?this.region:void 0};this.configuration.logger().debug("SubscriptionManager",(()=>({messageType:"object",message:s.map((e=>{const t=e.type===he.Message||e.type===he.Signal?B(e.data.message):void 0;return t?{type:e.type,data:Object.assign(Object.assign({},e.data),{pn_mfp:t})}:e})),details:"Received events:"}))),s.forEach((t=>{if(r&&"message"in t.data&&"timetoken"in t.data){if(this.dedupingManager.isDuplicate(t.data))return void this.configuration.logger().warn("SubscriptionManager",(()=>({messageType:"object",message:t.data,details:"Duplicate message detected (skipped):"})));this.dedupingManager.addEntry(t.data)}this.emitEvent(e,t)}))}catch(e){const t={error:!0,category:h.PNUnknownCategory,errorData:e,statusCode:0};this.emitStatus(t)}this.region=t.cursor.region,this.startSubscribeLoop()}setState(e){const{state:t,channels:s,channelGroups:n}=e;null==s||s.forEach((e=>e in this.channels&&(this.presenceState[e]=t))),null==n||n.forEach((e=>e in this.channelGroups&&(this.presenceState[e]=t)))}changePresence(e){const{connected:t,channels:s,channelGroups:n}=e;t?(null==s||s.forEach((e=>this.heartbeatChannels[e]={})),null==n||n.forEach((e=>this.heartbeatChannelGroups[e]={}))):(null==s||s.forEach((e=>{e in this.heartbeatChannels&&delete this.heartbeatChannels[e]})),null==n||n.forEach((e=>{e in this.heartbeatChannelGroups&&delete this.heartbeatChannelGroups[e]})),!1===this.configuration.suppressLeaveEvents&&this.leaveCall({channels:s,channelGroups:n},(e=>this.emitStatus(e)))),this.reconnect()}startHeartbeatTimer(){this.stopHeartbeatTimer();const e=this.configuration.getHeartbeatInterval();e&&0!==e&&(this.configuration.useSmartHeartbeat||this.sendHeartbeat(),this.heartbeatTimer=setInterval((()=>this.sendHeartbeat()),1e3*e))}stopHeartbeatTimer(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}sendHeartbeat(){const e=Object.keys(this.heartbeatChannelGroups),t=Object.keys(this.heartbeatChannels);0===t.length&&0===e.length||this.heartbeatCall({channels:t,channelGroups:e,heartbeat:this.configuration.getPresenceTimeout(),state:this.presenceState},(e=>{e.error&&this.configuration.announceFailedHeartbeats&&this.emitStatus(e),e.error&&this.configuration.autoNetworkDetection&&this.isOnline&&(this.isOnline=!1,this.disconnect(),this.emitStatus({category:h.PNNetworkDownCategory}),this.reconnect()),!e.error&&this.configuration.announceSuccessfulHeartbeats&&this.emitStatus(e)}))}}class fe{constructor(e,t,s){this._payload=e,this.setDefaultPayloadStructure(),this.title=t,this.body=s}get payload(){return this._payload}set title(e){this._title=e}set subtitle(e){this._subtitle=e}set body(e){this._body=e}set badge(e){this._badge=e}set sound(e){this._sound=e}setDefaultPayloadStructure(){}toObject(){return{}}}class ve extends fe{constructor(){super(...arguments),this._apnsPushType="apns",this._isSilent=!1}get payload(){return this._payload}set configurations(e){e&&e.length&&(this._configurations=e)}get notification(){return this.payload.aps}get title(){return this._title}set title(e){e&&e.length&&(this.payload.aps.alert.title=e,this._title=e)}get subtitle(){return this._subtitle}set subtitle(e){e&&e.length&&(this.payload.aps.alert.subtitle=e,this._subtitle=e)}get body(){return this._body}set body(e){e&&e.length&&(this.payload.aps.alert.body=e,this._body=e)}get badge(){return this._badge}set badge(e){null!=e&&(this.payload.aps.badge=e,this._badge=e)}get sound(){return this._sound}set sound(e){e&&e.length&&(this.payload.aps.sound=e,this._sound=e)}set silent(e){this._isSilent=e}setDefaultPayloadStructure(){this.payload.aps={alert:{}}}toObject(){const e=Object.assign({},this.payload),{aps:t}=e;let{alert:s}=t;if(this._isSilent&&(t["content-available"]=1),"apns2"===this._apnsPushType){if(!this._configurations||!this._configurations.length)throw new ReferenceError("APNS2 configuration is missing");const t=[];this._configurations.forEach((e=>{t.push(this.objectFromAPNS2Configuration(e))})),t.length&&(e.pn_push=t)}return s&&Object.keys(s).length||delete t.alert,this._isSilent&&(delete t.alert,delete t.badge,delete t.sound,s={}),this._isSilent||s&&Object.keys(s).length?e:null}objectFromAPNS2Configuration(e){if(!e.targets||!e.targets.length)throw new ReferenceError("At least one APNS2 target should be provided");const{collapseId:t,expirationDate:s}=e,n={auth_method:"token",targets:e.targets.map((e=>this.objectFromAPNSTarget(e))),version:"v2"};return t&&t.length&&(n.collapse_id=t),s&&(n.expiration=s.toISOString()),n}objectFromAPNSTarget(e){if(!e.topic||!e.topic.length)throw new TypeError("Target 'topic' undefined.");const{topic:t,environment:s="development",excludedDevices:n=[]}=e,r={topic:t,environment:s};return n.length&&(r.excluded_devices=n),r}}class Se extends fe{get payload(){return this._payload}get notification(){return this.payload.notification}get data(){return this.payload.data}get title(){return this._title}set title(e){e&&e.length&&(this.payload.notification.title=e,this._title=e)}get body(){return this._body}set body(e){e&&e.length&&(this.payload.notification.body=e,this._body=e)}get sound(){return this._sound}set sound(e){e&&e.length&&(this.payload.notification.sound=e,this._sound=e)}get icon(){return this._icon}set icon(e){e&&e.length&&(this.payload.notification.icon=e,this._icon=e)}get tag(){return this._tag}set tag(e){e&&e.length&&(this.payload.notification.tag=e,this._tag=e)}set silent(e){this._isSilent=e}setDefaultPayloadStructure(){this.payload.notification={},this.payload.data={}}toObject(){let e=Object.assign({},this.payload.data),t=null;const s={};if(Object.keys(this.payload).length>2){const t=r(this.payload,["notification","data"]);e=Object.assign(Object.assign({},e),t)}return this._isSilent?e.notification=this.payload.notification:t=this.payload.notification,Object.keys(e).length&&(s.data=e),t&&Object.keys(t).length&&(s.notification=t),Object.keys(s).length?s:null}}class we{constructor(e,t){this._payload={apns:{},fcm:{}},this._title=e,this._body=t,this.apns=new ve(this._payload.apns,e,t),this.fcm=new Se(this._payload.fcm,e,t)}set debugging(e){this._debugging=e}get title(){return this._title}get subtitle(){return this._subtitle}set subtitle(e){this._subtitle=e,this.apns.subtitle=e,this.fcm.subtitle=e}get body(){return this._body}get badge(){return this._badge}set badge(e){this._badge=e,this.apns.badge=e,this.fcm.badge=e}get sound(){return this._sound}set sound(e){this._sound=e,this.apns.sound=e,this.fcm.sound=e}buildPayload(e){const t={};if(e.includes("apns")||e.includes("apns2")){this.apns._apnsPushType=e.includes("apns")?"apns":"apns2";const s=this.apns.toObject();s&&Object.keys(s).length&&(t.pn_apns=s)}if(e.includes("fcm")){const e=this.fcm.toObject();e&&Object.keys(e).length&&(t.pn_gcm=e)}return Object.keys(t).length&&this._debugging&&(t.pn_debug=!0),t}}class Oe{constructor(e=!1){this.sync=e,this.listeners=new Set}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}notify(e){const t=()=>{this.listeners.forEach((t=>{t(e)}))};this.sync?t():setTimeout(t,0)}}class ke{transition(e,t){var s;if(this.transitionMap.has(t.type))return null===(s=this.transitionMap.get(t.type))||void 0===s?void 0:s(e,t)}constructor(e){this.label=e,this.transitionMap=new Map,this.enterEffects=[],this.exitEffects=[]}on(e,t){return this.transitionMap.set(e,t),this}with(e,t){return[this,e,null!=t?t:[]]}onEnter(e){return this.enterEffects.push(e),this}onExit(e){return this.exitEffects.push(e),this}}class Ce extends Oe{constructor(e){super(!0),this.logger=e,this._pendingEvents=[],this._inTransition=!1}get currentState(){return this._currentState}get currentContext(){return this._currentContext}describe(e){return new ke(e)}start(e,t){this._currentState=e,this._currentContext=t,this.notify({type:"engineStarted",state:e,context:t})}transition(e){if(!this._currentState)throw this.logger.error("Engine","Finite state machine is not started"),new Error("Start the engine first");if(this._inTransition)return this.logger.trace("Engine",(()=>({messageType:"object",message:e,details:"Event engine in transition. Enqueue received event:"}))),void this._pendingEvents.push(e);this._inTransition=!0,this.logger.trace("Engine",(()=>({messageType:"object",message:e,details:"Event engine received event:"}))),this.notify({type:"eventReceived",event:e});const t=this._currentState.transition(this._currentContext,e);if(t){const[s,n,r]=t;this.logger.trace("Engine",`Exiting state: ${this._currentState.label}`);for(const e of this._currentState.exitEffects)this.notify({type:"invocationDispatched",invocation:e(this._currentContext)});this.logger.trace("Engine",(()=>({messageType:"object",details:`Entering '${s.label}' state with context:`,message:n})));const i=this._currentState;this._currentState=s;const a=this._currentContext;this._currentContext=n,this.notify({type:"transitionDone",fromState:i,fromContext:a,toState:s,toContext:n,event:e});for(const e of r)this.notify({type:"invocationDispatched",invocation:e});for(const e of this._currentState.enterEffects)this.notify({type:"invocationDispatched",invocation:e(this._currentContext)})}else this.logger.warn("Engine",`No transition from '${this._currentState.label}' found for event: ${e.type}`);if(this._inTransition=!1,this._pendingEvents.length>0){const e=this._pendingEvents.shift();e&&(this.logger.trace("Engine",(()=>({messageType:"object",message:e,details:"De-queueing pending event:"}))),this.transition(e))}}}class Pe{constructor(e,t){this.dependencies=e,this.logger=t,this.instances=new Map,this.handlers=new Map}on(e,t){this.handlers.set(e,t)}dispatch(e){if(this.logger.trace("Dispatcher",`Process invocation: ${e.type}`),"CANCEL"===e.type){if(this.instances.has(e.payload)){const t=this.instances.get(e.payload);null==t||t.cancel(),this.instances.delete(e.payload)}return}const t=this.handlers.get(e.type);if(!t)throw this.logger.error("Dispatcher",`Unhandled invocation '${e.type}'`),new Error(`Unhandled invocation '${e.type}'`);const s=t(e.payload,this.dependencies);this.logger.trace("Dispatcher",(()=>({messageType:"object",details:"Call invocation handler with parameters:",message:e.payload,ignoredKeys:["abortSignal"]}))),e.managed&&this.instances.set(e.type,s),s.start()}dispose(){for(const[e,t]of this.instances.entries())t.cancel(),this.instances.delete(e)}}function je(e,t){const s=function(...s){return{type:e,payload:null==t?void 0:t(...s)}};return s.type=e,s}function Ee(e,t){const s=(...s)=>({type:e,payload:t(...s),managed:!1});return s.type=e,s}function Ne(e,t){const s=(...s)=>({type:e,payload:t(...s),managed:!0});return s.type=e,s.cancel={type:"CANCEL",payload:e,managed:!1},s}class Te extends Error{constructor(){super("The operation was aborted."),this.name="AbortError",Object.setPrototypeOf(this,new.target.prototype)}}class _e extends Oe{constructor(){super(...arguments),this._aborted=!1}get aborted(){return this._aborted}throwIfAborted(){if(this._aborted)throw new Te}abort(){this._aborted=!0,this.notify(new Te)}}class Ie{constructor(e,t){this.payload=e,this.dependencies=t}}class Me extends Ie{constructor(e,t,s){super(e,t),this.asyncFunction=s,this.abortSignal=new _e}start(){this.asyncFunction(this.payload,this.abortSignal,this.dependencies).catch((e=>{}))}cancel(){this.abortSignal.abort()}}const Ae=e=>(t,s)=>new Me(t,s,e),Ue=Ne("HEARTBEAT",((e,t)=>({channels:e,groups:t}))),Re=Ee("LEAVE",((e,t)=>({channels:e,groups:t}))),Fe=Ee("EMIT_STATUS",(e=>e)),$e=Ne("WAIT",(()=>({}))),De=je("RECONNECT",(()=>({}))),xe=je("DISCONNECT",((e=!1)=>({isOffline:e}))),qe=je("JOINED",((e,t)=>({channels:e,groups:t}))),Ge=je("LEFT",((e,t)=>({channels:e,groups:t}))),Ke=je("LEFT_ALL",((e=!1)=>({isOffline:e}))),Le=je("HEARTBEAT_SUCCESS",(e=>({statusCode:e}))),He=je("HEARTBEAT_FAILURE",(e=>e)),Be=je("TIMES_UP",(()=>({})));class We extends Pe{constructor(e,t){super(t,t.config.logger()),this.on(Ue.type,Ae(((t,s,n)=>i(this,[t,s,n],void 0,(function*(t,s,{heartbeat:n,presenceState:r,config:i}){s.throwIfAborted();try{yield n(Object.assign(Object.assign({abortSignal:s,channels:t.channels,channelGroups:t.groups},i.maintainPresenceState&&{state:r}),{heartbeat:i.presenceTimeout}));e.transition(Le(200))}catch(t){if(t instanceof d){if(t.status&&t.status.category==h.PNCancelledCategory)return;e.transition(He(t))}}}))))),this.on(Re.type,Ae(((e,t,s)=>i(this,[e,t,s],void 0,(function*(e,t,{leave:s,config:n}){if(!n.suppressLeaveEvents)try{s({channels:e.channels,channelGroups:e.groups})}catch(e){}}))))),this.on($e.type,Ae(((t,s,n)=>i(this,[t,s,n],void 0,(function*(t,s,{heartbeatDelay:n}){return s.throwIfAborted(),yield n(),s.throwIfAborted(),e.transition(Be())}))))),this.on(Fe.type,Ae(((e,t,s)=>i(this,[e,t,s],void 0,(function*(e,t,{emitStatus:s,config:n}){n.announceFailedHeartbeats&&!0===(null==e?void 0:e.error)?s(Object.assign(Object.assign({},e),{operation:M.PNHeartbeatOperation})):n.announceSuccessfulHeartbeats&&200===e.statusCode&&s(Object.assign(Object.assign({},e),{error:!1,operation:M.PNHeartbeatOperation,category:h.PNAcknowledgmentCategory}))})))))}}const ze=new ke("HEARTBEAT_STOPPED");ze.on(qe.type,((e,t)=>ze.with({channels:[...e.channels,...t.payload.channels.filter((t=>!e.channels.includes(t)))],groups:[...e.groups,...t.payload.groups.filter((t=>!e.groups.includes(t)))]}))),ze.on(Ge.type,((e,t)=>ze.with({channels:e.channels.filter((e=>!t.payload.channels.includes(e))),groups:e.groups.filter((e=>!t.payload.groups.includes(e)))}))),ze.on(De.type,((e,t)=>Xe.with({channels:e.channels,groups:e.groups}))),ze.on(Ke.type,((e,t)=>Qe.with(void 0)));const Ve=new ke("HEARTBEAT_COOLDOWN");Ve.onEnter((()=>$e())),Ve.onExit((()=>$e.cancel)),Ve.on(Be.type,((e,t)=>Xe.with({channels:e.channels,groups:e.groups}))),Ve.on(qe.type,((e,t)=>Xe.with({channels:[...e.channels,...t.payload.channels.filter((t=>!e.channels.includes(t)))],groups:[...e.groups,...t.payload.groups.filter((t=>!e.groups.includes(t)))]}))),Ve.on(Ge.type,((e,t)=>Xe.with({channels:e.channels.filter((e=>!t.payload.channels.includes(e))),groups:e.groups.filter((e=>!t.payload.groups.includes(e)))},[Re(t.payload.channels,t.payload.groups)]))),Ve.on(xe.type,((e,t)=>ze.with({channels:e.channels,groups:e.groups},[...t.payload.isOffline?[]:[Re(e.channels,e.groups)]]))),Ve.on(Ke.type,((e,t)=>Qe.with(void 0,[...t.payload.isOffline?[]:[Re(e.channels,e.groups)]])));const Je=new ke("HEARTBEAT_FAILED");Je.on(qe.type,((e,t)=>Xe.with({channels:[...e.channels,...t.payload.channels.filter((t=>!e.channels.includes(t)))],groups:[...e.groups,...t.payload.groups.filter((t=>!e.groups.includes(t)))]}))),Je.on(Ge.type,((e,t)=>Xe.with({channels:e.channels.filter((e=>!t.payload.channels.includes(e))),groups:e.groups.filter((e=>!t.payload.groups.includes(e)))},[Re(t.payload.channels,t.payload.groups)]))),Je.on(De.type,((e,t)=>Xe.with({channels:e.channels,groups:e.groups}))),Je.on(xe.type,((e,t)=>ze.with({channels:e.channels,groups:e.groups},[...t.payload.isOffline?[]:[Re(e.channels,e.groups)]]))),Je.on(Ke.type,((e,t)=>Qe.with(void 0,[...t.payload.isOffline?[]:[Re(e.channels,e.groups)]])));const Xe=new ke("HEARTBEATING");Xe.onEnter((e=>Ue(e.channels,e.groups))),Xe.onExit((()=>Ue.cancel)),Xe.on(Le.type,((e,t)=>Ve.with({channels:e.channels,groups:e.groups},[Fe(Object.assign({},t.payload))]))),Xe.on(qe.type,((e,t)=>Xe.with({channels:[...e.channels,...t.payload.channels.filter((t=>!e.channels.includes(t)))],groups:[...e.groups,...t.payload.groups.filter((t=>!e.groups.includes(t)))]}))),Xe.on(Ge.type,((e,t)=>Xe.with({channels:e.channels.filter((e=>!t.payload.channels.includes(e))),groups:e.groups.filter((e=>!t.payload.groups.includes(e)))},[Re(t.payload.channels,t.payload.groups)]))),Xe.on(He.type,((e,t)=>Je.with(Object.assign({},e),[...t.payload.status?[Fe(Object.assign({},t.payload.status))]:[]]))),Xe.on(xe.type,((e,t)=>ze.with({channels:e.channels,groups:e.groups},[...t.payload.isOffline?[]:[Re(e.channels,e.groups)]]))),Xe.on(Ke.type,((e,t)=>Qe.with(void 0,[...t.payload.isOffline?[]:[Re(e.channels,e.groups)]])));const Qe=new ke("HEARTBEAT_INACTIVE");Qe.on(qe.type,((e,t)=>Xe.with({channels:t.payload.channels,groups:t.payload.groups})));class Ye{get _engine(){return this.engine}constructor(e){this.dependencies=e,this.channels=[],this.groups=[],this.engine=new Ce(e.config.logger()),this.dispatcher=new We(this.engine,e),e.config.logger().debug("PresenceEventEngine","Create presence event engine."),this._unsubscribeEngine=this.engine.subscribe((e=>{"invocationDispatched"===e.type&&this.dispatcher.dispatch(e.invocation)})),this.engine.start(Qe,void 0)}join({channels:e,groups:t}){this.channels=[...this.channels,...(null!=e?e:[]).filter((e=>!this.channels.includes(e)))],this.groups=[...this.groups,...(null!=t?t:[]).filter((e=>!this.groups.includes(e)))],0===this.channels.length&&0===this.groups.length||this.engine.transition(qe(this.channels.slice(0),this.groups.slice(0)))}leave({channels:e,groups:t}){this.dependencies.presenceState&&(null==e||e.forEach((e=>delete this.dependencies.presenceState[e])),null==t||t.forEach((e=>delete this.dependencies.presenceState[e]))),this.engine.transition(Ge(null!=e?e:[],null!=t?t:[]))}leaveAll(e=!1){this.engine.transition(Ke(e))}reconnect(){this.engine.transition(De())}disconnect(e=!1){this.engine.transition(xe(e))}dispose(){this.disconnect(!0),this._unsubscribeEngine(),this.dispatcher.dispose()}}const Ze=Ne("HANDSHAKE",((e,t)=>({channels:e,groups:t}))),et=Ne("RECEIVE_MESSAGES",((e,t,s)=>({channels:e,groups:t,cursor:s}))),tt=Ee("EMIT_MESSAGES",((e,t)=>({cursor:e,events:t}))),st=Ee("EMIT_STATUS",(e=>e)),nt=je("SUBSCRIPTION_CHANGED",((e,t,s=!1)=>({channels:e,groups:t,isOffline:s}))),rt=je("SUBSCRIPTION_RESTORED",((e,t,s,n)=>({channels:e,groups:t,cursor:{timetoken:s,region:null!=n?n:0}}))),it=je("HANDSHAKE_SUCCESS",(e=>e)),at=je("HANDSHAKE_FAILURE",(e=>e)),ot=je("RECEIVE_SUCCESS",((e,t)=>({cursor:e,events:t}))),ct=je("RECEIVE_FAILURE",(e=>e)),ut=je("DISCONNECT",((e=!1)=>({isOffline:e}))),lt=je("RECONNECT",((e,t)=>({cursor:{timetoken:null!=e?e:"",region:null!=t?t:0}}))),ht=je("UNSUBSCRIBE_ALL",(()=>({}))),dt=new ke("UNSUBSCRIBED");dt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups}))),dt.on(rt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region}})));const pt=new ke("HANDSHAKE_STOPPED");pt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):pt.with({channels:t.channels,groups:t.groups,cursor:e.cursor}))),pt.on(lt.type,((e,{payload:t})=>bt.with(Object.assign(Object.assign({},e),{cursor:t.cursor||e.cursor})))),pt.on(rt.type,((e,{payload:t})=>{var s;return 0===t.channels.length&&0===t.groups.length?dt.with(void 0):pt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||(null===(s=e.cursor)||void 0===s?void 0:s.region)||0}})})),pt.on(ht.type,(e=>dt.with()));const gt=new ke("HANDSHAKE_FAILED");gt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:e.cursor}))),gt.on(lt.type,((e,{payload:t})=>bt.with(Object.assign(Object.assign({},e),{cursor:t.cursor||e.cursor})))),gt.on(rt.type,((e,{payload:t})=>{var s,n;return 0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region?t.cursor.region:null!==(n=null===(s=null==e?void 0:e.cursor)||void 0===s?void 0:s.region)&&void 0!==n?n:0}})})),gt.on(ht.type,(e=>dt.with()));const bt=new ke("HANDSHAKING");bt.onEnter((e=>Ze(e.channels,e.groups))),bt.onExit((()=>Ze.cancel)),bt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:e.cursor}))),bt.on(it.type,((e,{payload:t})=>{var s,n,r,i,a;return ft.with({channels:e.channels,groups:e.groups,cursor:{timetoken:(null===(s=e.cursor)||void 0===s?void 0:s.timetoken)?null===(n=e.cursor)||void 0===n?void 0:n.timetoken:t.timetoken,region:t.region},referenceTimetoken:L(t.timetoken,null===(r=e.cursor)||void 0===r?void 0:r.timetoken)},[st({category:h.PNConnectedCategory,affectedChannels:e.channels.slice(0),affectedChannelGroups:e.groups.slice(0),currentTimetoken:(null===(i=e.cursor)||void 0===i?void 0:i.timetoken)?null===(a=e.cursor)||void 0===a?void 0:a.timetoken:t.timetoken})])})),bt.on(at.type,((e,t)=>{var s;return gt.with(Object.assign(Object.assign({},e),{reason:t.payload}),[st({category:h.PNConnectionErrorCategory,error:null===(s=t.payload.status)||void 0===s?void 0:s.category})])})),bt.on(ut.type,((e,t)=>{var s;if(t.payload.isOffline){const t=I.create(new Error("Network connection error")).toPubNubError(M.PNSubscribeOperation);return gt.with(Object.assign(Object.assign({},e),{reason:t}),[st({category:h.PNConnectionErrorCategory,error:null===(s=t.status)||void 0===s?void 0:s.category})])}return pt.with(Object.assign({},e))})),bt.on(rt.type,((e,{payload:t})=>{var s;return 0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||(null===(s=null==e?void 0:e.cursor)||void 0===s?void 0:s.region)||0}})})),bt.on(ht.type,(e=>dt.with()));const yt=new ke("RECEIVE_STOPPED");yt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):yt.with({channels:t.channels,groups:t.groups,cursor:e.cursor}))),yt.on(rt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):yt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||e.cursor.region}}))),yt.on(lt.type,((e,{payload:t})=>{var s;return bt.with({channels:e.channels,groups:e.groups,cursor:{timetoken:t.cursor.timetoken?null===(s=t.cursor)||void 0===s?void 0:s.timetoken:e.cursor.timetoken,region:t.cursor.region||e.cursor.region}})})),yt.on(ht.type,(()=>dt.with(void 0)));const mt=new ke("RECEIVE_FAILED");mt.on(lt.type,((e,{payload:t})=>{var s;return bt.with({channels:e.channels,groups:e.groups,cursor:{timetoken:t.cursor.timetoken?null===(s=t.cursor)||void 0===s?void 0:s.timetoken:e.cursor.timetoken,region:t.cursor.region||e.cursor.region}})})),mt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:e.cursor}))),mt.on(rt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||e.cursor.region}}))),mt.on(ht.type,(e=>dt.with(void 0)));const ft=new ke("RECEIVING");ft.onEnter((e=>et(e.channels,e.groups,e.cursor))),ft.onExit((()=>et.cancel)),ft.on(ot.type,((e,{payload:t})=>ft.with({channels:e.channels,groups:e.groups,cursor:t.cursor,referenceTimetoken:L(t.cursor.timetoken)},[tt(e.cursor,t.events)]))),ft.on(nt.type,((e,{payload:t})=>{var s;if(0===t.channels.length&&0===t.groups.length){let e;return t.isOffline&&(e=null===(s=I.create(new Error("Network connection error")).toPubNubError(M.PNSubscribeOperation).status)||void 0===s?void 0:s.category),dt.with(void 0,[st(Object.assign({category:t.isOffline?h.PNDisconnectedUnexpectedlyCategory:h.PNDisconnectedCategory},e?{error:e}:{}))])}return ft.with({channels:t.channels,groups:t.groups,cursor:e.cursor,referenceTimetoken:e.referenceTimetoken},[st({category:h.PNSubscriptionChangedCategory,affectedChannels:t.channels.slice(0),affectedChannelGroups:t.groups.slice(0),currentTimetoken:e.cursor.timetoken})])})),ft.on(rt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0,[st({category:h.PNDisconnectedCategory})]):ft.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||e.cursor.region},referenceTimetoken:L(e.cursor.timetoken,`${t.cursor.timetoken}`,e.referenceTimetoken)},[st({category:h.PNSubscriptionChangedCategory,affectedChannels:t.channels.slice(0),affectedChannelGroups:t.groups.slice(0),currentTimetoken:t.cursor.timetoken})]))),ft.on(ct.type,((e,{payload:t})=>{var s;return mt.with(Object.assign(Object.assign({},e),{reason:t}),[st({category:h.PNDisconnectedUnexpectedlyCategory,error:null===(s=t.status)||void 0===s?void 0:s.category})])})),ft.on(ut.type,((e,t)=>{var s;if(t.payload.isOffline){const t=I.create(new Error("Network connection error")).toPubNubError(M.PNSubscribeOperation);return mt.with(Object.assign(Object.assign({},e),{reason:t}),[st({category:h.PNDisconnectedUnexpectedlyCategory,error:null===(s=t.status)||void 0===s?void 0:s.category})])}return yt.with(Object.assign({},e),[st({category:h.PNDisconnectedCategory})])})),ft.on(ht.type,(e=>dt.with(void 0,[st({category:h.PNDisconnectedCategory})])));class vt extends Pe{constructor(e,t){super(t,t.config.logger()),this.on(Ze.type,Ae(((t,s,n)=>i(this,[t,s,n],void 0,(function*(t,s,{handshake:n,presenceState:r,config:i}){s.throwIfAborted();try{const a=yield n(Object.assign({abortSignal:s,channels:t.channels,channelGroups:t.groups,filterExpression:i.filterExpression},i.maintainPresenceState&&{state:r}));return e.transition(it(a))}catch(t){if(t instanceof d){if(t.status&&t.status.category==h.PNCancelledCategory)return;return e.transition(at(t))}}}))))),this.on(et.type,Ae(((t,s,n)=>i(this,[t,s,n],void 0,(function*(t,s,{receiveMessages:n,config:r}){s.throwIfAborted();try{const i=yield n({abortSignal:s,channels:t.channels,channelGroups:t.groups,timetoken:t.cursor.timetoken,region:t.cursor.region,filterExpression:r.filterExpression});e.transition(ot(i.cursor,i.messages))}catch(t){if(t instanceof d){if(t.status&&t.status.category==h.PNCancelledCategory)return;if(!s.aborted)return e.transition(ct(t))}}}))))),this.on(tt.type,Ae(((e,t,s)=>i(this,[e,t,s],void 0,(function*({cursor:e,events:t},s,{emitMessages:n}){t.length>0&&n(e,t)}))))),this.on(st.type,Ae(((e,t,s)=>i(this,[e,t,s],void 0,(function*(e,t,{emitStatus:s}){return s(e)})))))}}class St{get _engine(){return this.engine}constructor(e){this.channels=[],this.groups=[],this.dependencies=e,this.engine=new Ce(e.config.logger()),this.dispatcher=new vt(this.engine,e),e.config.logger().debug("EventEngine","Create subscribe event engine."),this._unsubscribeEngine=this.engine.subscribe((e=>{"invocationDispatched"===e.type&&this.dispatcher.dispatch(e.invocation)})),this.engine.start(dt,void 0)}get subscriptionTimetoken(){const e=this.engine.currentState;if(!e)return;let t,s="0";if(e.label===ft.label){const e=this.engine.currentContext;s=e.cursor.timetoken,t=e.referenceTimetoken}return K(s,null!=t?t:"0")}subscribe({channels:e,channelGroups:t,timetoken:s,withPresence:n}){this.channels=[...this.channels,...null!=e?e:[]],this.groups=[...this.groups,...null!=t?t:[]],n&&(this.channels.map((e=>this.channels.push(`${e}-pnpres`))),this.groups.map((e=>this.groups.push(`${e}-pnpres`)))),s?this.engine.transition(rt(Array.from(new Set([...this.channels,...null!=e?e:[]])),Array.from(new Set([...this.groups,...null!=t?t:[]])),s)):this.engine.transition(nt(Array.from(new Set([...this.channels,...null!=e?e:[]])),Array.from(new Set([...this.groups,...null!=t?t:[]])))),this.dependencies.join&&this.dependencies.join({channels:Array.from(new Set(this.channels.filter((e=>!e.endsWith("-pnpres"))))),groups:Array.from(new Set(this.groups.filter((e=>!e.endsWith("-pnpres")))))})}unsubscribe({channels:e=[],channelGroups:t=[]}){const s=x(this.channels,[...e,...e.map((e=>`${e}-pnpres`))]),n=x(this.groups,[...t,...t.map((e=>`${e}-pnpres`))]);if(new Set(this.channels).size!==new Set(s).size||new Set(this.groups).size!==new Set(n).size){const r=q(this.channels,e),i=q(this.groups,t);this.dependencies.presenceState&&(null==r||r.forEach((e=>delete this.dependencies.presenceState[e])),null==i||i.forEach((e=>delete this.dependencies.presenceState[e]))),this.channels=s,this.groups=n,this.engine.transition(nt(Array.from(new Set(this.channels.slice(0))),Array.from(new Set(this.groups.slice(0))))),this.dependencies.leave&&this.dependencies.leave({channels:r.slice(0),groups:i.slice(0)})}}unsubscribeAll(e=!1){const t=this.getSubscribedChannelGroups(),s=this.getSubscribedChannels();this.channels=[],this.groups=[],this.dependencies.presenceState&&Object.keys(this.dependencies.presenceState).forEach((e=>{delete this.dependencies.presenceState[e]})),this.engine.transition(nt(this.channels.slice(0),this.groups.slice(0),e)),this.dependencies.leaveAll&&this.dependencies.leaveAll({channels:s,groups:t,isOffline:e})}reconnect({timetoken:e,region:t}){const s=this.getSubscribedChannels(),n=this.getSubscribedChannels();this.engine.transition(lt(e,t)),this.dependencies.presenceReconnect&&this.dependencies.presenceReconnect({channels:n,groups:s})}disconnect(e=!1){const t=this.getSubscribedChannels(),s=this.getSubscribedChannels();this.engine.transition(ut(e)),this.dependencies.presenceDisconnect&&this.dependencies.presenceDisconnect({channels:s,groups:t,isOffline:e})}getSubscribedChannels(){return Array.from(new Set(this.channels.slice(0)))}getSubscribedChannelGroups(){return Array.from(new Set(this.groups.slice(0)))}dispose(){this.disconnect(!0),this._unsubscribeEngine(),this.dispatcher.dispose()}}class wt extends le{constructor(e){var t;const s=null!==(t=e.sendByPost)&&void 0!==t&&t;super({method:s?ae.POST:ae.GET,compressible:s}),this.parameters=e,this.parameters.sendByPost=s}operation(){return M.PNPublishOperation}validate(){const{message:e,channel:t,keySet:{publishKey:s}}=this.parameters;return t?e?s?void 0:"Missing 'publishKey'":"Missing 'message'":"Missing 'channel'"}parse(e){return i(this,void 0,void 0,(function*(){return{timetoken:this.deserializeResponse(e)[2]}}))}get path(){const{message:e,channel:t,keySet:s}=this.parameters,n=this.prepareMessagePayload(e);return`/publish/${s.publishKey}/${s.subscribeKey}/0/${$(t)}/0${this.parameters.sendByPost?"":`/${$(n)}`}`}get queryParameters(){const{customMessageType:e,meta:t,replicate:s,storeInHistory:n,ttl:r}=this.parameters,i={};return e&&(i.custom_message_type=e),void 0!==n&&(i.store=n?"1":"0"),void 0!==r&&(i.ttl=r),void 0===s||s||(i.norep="true"),t&&"object"==typeof t&&(i.meta=JSON.stringify(t)),i}get headers(){var e;return this.parameters.sendByPost?Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"}):super.headers}get body(){return this.prepareMessagePayload(this.parameters.message)}prepareMessagePayload(e){const{crypto:t}=this.parameters;if(!t)return JSON.stringify(e)||"";const s=t.encrypt(JSON.stringify(e));return JSON.stringify("string"==typeof s?s:u(s))}}class Ot extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNSignalOperation}validate(){const{message:e,channel:t,keySet:{publishKey:s}}=this.parameters;return t?e?s?void 0:"Missing 'publishKey'":"Missing 'message'":"Missing 'channel'"}parse(e){return i(this,void 0,void 0,(function*(){return{timetoken:this.deserializeResponse(e)[2]}}))}get path(){const{keySet:{publishKey:e,subscribeKey:t},channel:s,message:n}=this.parameters,r=JSON.stringify(n);return`/signal/${e}/${t}/0/${$(s)}/0/${$(r)}`}get queryParameters(){const{customMessageType:e}=this.parameters,t={};return e&&(t.custom_message_type=e),t}}class kt extends de{operation(){return M.PNReceiveMessagesOperation}validate(){const e=super.validate();return e||(this.parameters.timetoken?this.parameters.region?void 0:"region can not be empty":"timetoken can not be empty")}get path(){const{keySet:{subscribeKey:e},channels:t=[]}=this.parameters;return`/v2/subscribe/${e}/${D(t.sort(),",")}/0`}get queryParameters(){const{channelGroups:e,filterExpression:t,timetoken:s,region:n}=this.parameters,r={ee:""};return e&&e.length>0&&(r["channel-group"]=e.sort().join(",")),t&&t.length>0&&(r["filter-expr"]=t),"string"==typeof s?s&&"0"!==s&&s.length>0&&(r.tt=s):s&&s>0&&(r.tt=s),n&&(r.tr=n),r}}class Ct extends de{operation(){return M.PNHandshakeOperation}get path(){const{keySet:{subscribeKey:e},channels:t=[]}=this.parameters;return`/v2/subscribe/${e}/${D(t.sort(),",")}/0`}get queryParameters(){const{channelGroups:e,filterExpression:t,state:s}=this.parameters,n={ee:""};return e&&e.length>0&&(n["channel-group"]=e.sort().join(",")),t&&t.length>0&&(n["filter-expr"]=t),s&&Object.keys(s).length>0&&(n.state=JSON.stringify(s)),n}}var Pt;!function(e){e[e.Channel=0]="Channel",e[e.ChannelGroup=1]="ChannelGroup"}(Pt||(Pt={}));class jt{constructor({channels:e,channelGroups:t}){this.isEmpty=!0,this._channelGroups=new Set((null!=t?t:[]).filter((e=>e.length>0))),this._channels=new Set((null!=e?e:[]).filter((e=>e.length>0))),this.isEmpty=0===this._channels.size&&0===this._channelGroups.size}get length(){return this.isEmpty?0:this._channels.size+this._channelGroups.size}get channels(){return this.isEmpty?[]:Array.from(this._channels)}get channelGroups(){return this.isEmpty?[]:Array.from(this._channelGroups)}contains(e){return!this.isEmpty&&(this._channels.has(e)||this._channelGroups.has(e))}with(e){return new jt({channels:[...this._channels,...e._channels],channelGroups:[...this._channelGroups,...e._channelGroups]})}without(e){return new jt({channels:[...this._channels].filter((t=>!e._channels.has(t))),channelGroups:[...this._channelGroups].filter((t=>!e._channelGroups.has(t)))})}add(e){return e._channelGroups.size>0&&(this._channelGroups=new Set([...this._channelGroups,...e._channelGroups])),e._channels.size>0&&(this._channels=new Set([...this._channels,...e._channels])),this.isEmpty=0===this._channels.size&&0===this._channelGroups.size,this}remove(e){return e._channelGroups.size>0&&(this._channelGroups=new Set([...this._channelGroups].filter((t=>!e._channelGroups.has(t))))),e._channels.size>0&&(this._channels=new Set([...this._channels].filter((t=>!e._channels.has(t))))),this}removeAll(){return this._channels.clear(),this._channelGroups.clear(),this.isEmpty=!0,this}toString(){return`SubscriptionInput { channels: [${this.channels.join(", ")}], channelGroups: [${this.channelGroups.join(", ")}], is empty: ${this.isEmpty?"true":"false"}} }`}}class Et{constructor(e,t,s,n){this._isSubscribed=!1,this.clones={},this.parents=[],this._id=te.createUUID(),this.referenceTimetoken=n,this.subscriptionInput=t,this.options=s,this.client=e}get id(){return this._id}get isLastClone(){return 1===Object.keys(this.clones).length}get isSubscribed(){return!!this._isSubscribed||this.parents.length>0&&this.parents.some((e=>e.isSubscribed))}set isSubscribed(e){this.isSubscribed!==e&&(this._isSubscribed=e)}addParentState(e){this.parents.includes(e)||this.parents.push(e)}removeParentState(e){const t=this.parents.indexOf(e);-1!==t&&this.parents.splice(t,1)}storeClone(e,t){this.clones[e]||(this.clones[e]=t)}}class Nt{constructor(e,t="Subscription"){this.subscriptionType=t,this.id=te.createUUID(),this.eventDispatcher=new ge,this._state=e}get state(){return this._state}get channels(){return this.state.subscriptionInput.channels.slice(0)}get channelGroups(){return this.state.subscriptionInput.channelGroups.slice(0)}set onMessage(e){this.eventDispatcher.onMessage=e}set onPresence(e){this.eventDispatcher.onPresence=e}set onSignal(e){this.eventDispatcher.onSignal=e}set onObjects(e){this.eventDispatcher.onObjects=e}set onMessageAction(e){this.eventDispatcher.onMessageAction=e}set onFile(e){this.eventDispatcher.onFile=e}addListener(e){this.eventDispatcher.addListener(e)}removeListener(e){this.eventDispatcher.removeListener(e)}removeAllListeners(){this.eventDispatcher.removeAllListeners()}handleEvent(e,t){var s;if((!this.state.cursor||e>this.state.cursor)&&(this.state.cursor=e),this.state.referenceTimetoken&&t.data.timetoken({messageType:"text",message:`Event timetoken (${t.data.timetoken}) is older than reference timetoken (${this.state.referenceTimetoken}) for ${this.id} subscription object. Ignoring event.`})));if((null===(s=this.state.options)||void 0===s?void 0:s.filter)&&!this.state.options.filter(t))return void this.state.client.logger.trace(this.subscriptionType,`Event filtered out by filter function for ${this.id} subscription object. Ignoring event.`);const n=Object.values(this.state.clones);n.length>0&&this.state.client.logger.trace(this.subscriptionType,`Notify ${this.id} subscription object clones (count: ${n.length}) about received event.`),n.forEach((e=>e.eventDispatcher.handleEvent(t)))}dispose(){const e=Object.keys(this.state.clones);e.length>1?(this.state.client.logger.debug(this.subscriptionType,`Remove subscription object clone on dispose: ${this.id}`),delete this.state.clones[this.id]):1===e.length&&this.state.clones[this.id]&&(this.state.client.logger.debug(this.subscriptionType,`Unsubscribe subscription object on dispose: ${this.id}`),this.unsubscribe())}invalidate(e=!1){this.state._isSubscribed=!1,e&&(delete this.state.clones[this.id],0===Object.keys(this.state.clones).length&&(this.state.client.logger.trace(this.subscriptionType,"Last clone removed. Reset shared subscription state."),this.state.subscriptionInput.removeAll(),this.state.parents=[]))}subscribe(e){this.state.isSubscribed?this.state.client.logger.trace(this.subscriptionType,"Already subscribed. Ignoring subscribe request."):(this.state.client.logger.debug(this.subscriptionType,(()=>e?{messageType:"object",message:e,details:"Subscribe with parameters:"}:{messageType:"text",message:"Subscribe"})),this.state.isSubscribed=!0,this.updateSubscription({subscribing:!0,timetoken:null==e?void 0:e.timetoken}))}unsubscribe(){if(!this.state._isSubscribed||this.state.isSubscribed){if(!this.state._isSubscribed&&this.state.parents.length>0&&this.state.isSubscribed)return void this.state.client.logger.warn(this.subscriptionType,(()=>({messageType:"object",details:"Subscription is subscribed as part of a subscription set. Remove from active sets to unsubscribe:",message:this.state.parents.filter((e=>e.isSubscribed))})));if(!this.state._isSubscribed)return void this.state.client.logger.trace(this.subscriptionType,"Not subscribed. Ignoring unsubscribe request.")}this.state.client.logger.debug(this.subscriptionType,"Unsubscribe"),this.state.isSubscribed=!1,delete this.state.cursor,this.updateSubscription({subscribing:!1})}updateSubscription(e){var t,s;(null==e?void 0:e.timetoken)&&((null===(t=this.state.cursor)||void 0===t?void 0:t.timetoken)&&"0"!==(null===(s=this.state.cursor)||void 0===s?void 0:s.timetoken)?"0"!==e.timetoken&&e.timetoken>this.state.cursor.timetoken&&(this.state.cursor.timetoken=e.timetoken):this.state.cursor={timetoken:e.timetoken});const n=e.subscriptions&&e.subscriptions.length>0?e.subscriptions:void 0;e.subscribing?this.register(Object.assign(Object.assign({},e.timetoken?{cursor:this.state.cursor}:{}),n?{subscriptions:n}:{})):this.unregister(n)}}class Tt extends Et{constructor(e){const t=new jt({});e.subscriptions.forEach((e=>t.add(e.state.subscriptionInput))),super(e.client,t,e.options,e.client.subscriptionTimetoken),this.subscriptions=e.subscriptions}addSubscription(e){this.subscriptions.includes(e)||(e.state.addParentState(this),this.subscriptions.push(e),this.subscriptionInput.add(e.state.subscriptionInput))}removeSubscription(e,t){const s=this.subscriptions.indexOf(e);-1!==s&&(this.subscriptions.splice(s,1),t||e.state.removeParentState(this),this.subscriptionInput.remove(e.state.subscriptionInput))}removeAllSubscriptions(){this.subscriptions.forEach((e=>e.state.removeParentState(this))),this.subscriptions.splice(0,this.subscriptions.length),this.subscriptionInput.removeAll()}}class _t extends Nt{constructor(e){let t;if("client"in e){let s=[];!e.subscriptions&&e.entities?e.entities.forEach((t=>s.push(t.subscription(e.options)))):e.subscriptions&&(s=e.subscriptions),t=new Tt({client:e.client,subscriptions:s,options:e.options}),s.forEach((e=>e.state.addParentState(t))),t.client.logger.debug("SubscriptionSet",(()=>({messageType:"object",details:"Create subscription set with parameters:",message:Object.assign({subscriptions:t.subscriptions},e.options?e.options:{})})))}else t=e.state,t.client.logger.debug("SubscriptionSet","Create subscription set clone");super(t,"SubscriptionSet"),this.state.storeClone(this.id,this),t.subscriptions.forEach((e=>e.addParentSet(this)))}get state(){return super.state}get subscriptions(){return this.state.subscriptions.slice(0)}handleEvent(e,t){var s;this.state.subscriptionInput.contains(null!==(s=t.data.subscription)&&void 0!==s?s:t.data.channel)&&(this.state._isSubscribed?(super.handleEvent(e,t),this.state.subscriptions.length>0&&this.state.client.logger.trace(this.subscriptionType,`Notify ${this.id} subscription set subscriptions (count: ${this.state.subscriptions.length}) about received event.`),this.state.subscriptions.forEach((s=>s.handleEvent(e,t)))):this.state.client.logger.trace(this.subscriptionType,`Subscription set ${this.id} is not subscribed. Ignoring event.`))}subscriptionInput(e=!1){let t=this.state.subscriptionInput;return this.state.subscriptions.forEach((s=>{e&&s.state.entity.subscriptionsCount>0&&(t=t.without(s.state.subscriptionInput))})),t}cloneEmpty(){return new _t({state:this.state})}dispose(){const e=this.state.isLastClone;this.state.subscriptions.forEach((t=>{t.removeParentSet(this),e&&t.state.removeParentState(this.state)})),super.dispose()}invalidate(e=!1){(e?this.state.subscriptions.slice(0):this.state.subscriptions).forEach((t=>{e&&(t.state.entity.decreaseSubscriptionCount(this.state.id),t.removeParentSet(this)),t.invalidate(e)})),e&&this.state.removeAllSubscriptions(),super.invalidate()}addSubscription(e){this.addSubscriptions([e])}addSubscriptions(e){const t=[],s=[];this.state.client.logger.debug(this.subscriptionType,(()=>{const t=[],s=[];return e.forEach((e=>{this.state.subscriptions.includes(e)?t.push(e):s.push(e)})),{messageType:"object",details:`Add subscriptions to ${this.id} (subscriptions count: ${this.state.subscriptions.length+s.length}):`,message:{addedSubscriptions:s,ignoredSubscriptions:t}}})),e.filter((e=>!this.state.subscriptions.includes(e))).forEach((e=>{e.state.isSubscribed?s.push(e):t.push(e),e.addParentSet(this),this.state.addSubscription(e)})),0===s.length&&0===t.length||!this.state.isSubscribed||(s.forEach((({state:e})=>e.entity.increaseSubscriptionCount(this.state.id))),t.length>0&&this.updateSubscription({subscribing:!0,subscriptions:t}))}removeSubscription(e){this.removeSubscriptions([e])}removeSubscriptions(e){const t=[];this.state.client.logger.debug(this.subscriptionType,(()=>{const t=[],s=[];return e.forEach((e=>{this.state.subscriptions.includes(e)?s.push(e):t.push(e)})),{messageType:"object",details:`Remove subscriptions from ${this.id} (subscriptions count: ${this.state.subscriptions.length}):`,message:{removedSubscriptions:s,ignoredSubscriptions:t}}})),e.filter((e=>this.state.subscriptions.includes(e))).forEach((e=>{e.state.isSubscribed&&t.push(e),e.removeParentSet(this),this.state.removeSubscription(e,e.parentSetsCount>1)})),0!==t.length&&this.state.isSubscribed&&this.updateSubscription({subscribing:!1,subscriptions:t})}addSubscriptionSet(e){this.addSubscriptions(e.subscriptions)}removeSubscriptionSet(e){this.removeSubscriptions(e.subscriptions)}register(e){var t;const s=null!==(t=e.subscriptions)&&void 0!==t?t:this.state.subscriptions;s.forEach((({state:e})=>e.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,e.cursor,s)}unregister(e){const t=null!=e?e:this.state.subscriptions;t.forEach((({state:e})=>e.entity.decreaseSubscriptionCount(this.state.id))),this.state.client.logger.trace(this.subscriptionType,(()=>e?{messageType:"object",message:{subscription:this,subscriptions:e},details:"Unregister subscriptions of subscription set from real-time events:"}:{messageType:"text",message:`Unregister subscription from real-time events: ${this}`})),this.state.client.unregisterEventHandleCapable(this,t)}toString(){const e=this.state;return`${this.subscriptionType} { id: ${this.id}, stateId: ${e.id}, clonesCount: ${Object.keys(this.state.clones).length}, isSubscribed: ${e.isSubscribed}, subscriptions: [${e.subscriptions.map((e=>e.toString())).join(", ")}] }`}}class It extends Et{constructor(e){var t,s;const n=e.entity.subscriptionNames(null!==(s=null===(t=e.options)||void 0===t?void 0:t.receivePresenceEvents)&&void 0!==s&&s),r=new jt({[e.entity.subscriptionType==Pt.Channel?"channels":"channelGroups"]:n});super(e.client,r,e.options,e.client.subscriptionTimetoken),this.entity=e.entity}}class Mt extends Nt{constructor(e){"client"in e?e.client.logger.debug("Subscription",(()=>({messageType:"object",details:"Create subscription with parameters:",message:Object.assign({entity:e.entity},e.options?e.options:{})}))):e.state.client.logger.debug("Subscription","Create subscription clone"),super("state"in e?e.state:new It(e)),this.parents=[],this.handledUpdates=[],this.state.storeClone(this.id,this)}get state(){return super.state}get parentSetsCount(){return this.parents.length}handleEvent(e,t){var s,n;if(this.state.isSubscribed&&this.state.subscriptionInput.contains(null!==(s=t.data.subscription)&&void 0!==s?s:t.data.channel)){if(this.parentSetsCount>0){const e=B(t.data);if(this.handledUpdates.includes(e))return void this.state.client.logger.trace(this.subscriptionType,`Event (${e}) already handled by ${this.id}. Ignoring.`);this.handledUpdates.push(e),this.handledUpdates.length>10&&this.handledUpdates.shift()}this.state.subscriptionInput.contains(null!==(n=t.data.subscription)&&void 0!==n?n:t.data.channel)&&super.handleEvent(e,t)}}subscriptionInput(e=!1){return e&&this.state.entity.subscriptionsCount>0?new jt({}):this.state.subscriptionInput}cloneEmpty(){return new Mt({state:this.state})}dispose(){this.parentSetsCount>0?this.state.client.logger.debug(this.subscriptionType,(()=>({messageType:"text",message:`'${this.state.entity.subscriptionNames()}' subscription still in use. Ignore dispose request.`}))):(this.handledUpdates.splice(0,this.handledUpdates.length),super.dispose())}invalidate(e=!1){e&&this.state.entity.decreaseSubscriptionCount(this.state.id),this.handledUpdates.splice(0,this.handledUpdates.length),super.invalidate(e)}addParentSet(e){this.parents.includes(e)||(this.parents.push(e),this.state.client.logger.trace(this.subscriptionType,`Add parent subscription set for ${this.id}: ${e.id}. Parent subscription set count: ${this.parentSetsCount}`))}removeParentSet(e){const t=this.parents.indexOf(e);-1!==t&&(this.parents.splice(t,1),this.state.client.logger.trace(this.subscriptionType,`Remove parent subscription set from ${this.id}: ${e.id}. Parent subscription set count: ${this.parentSetsCount}`)),0===this.parentSetsCount&&this.handledUpdates.splice(0,this.handledUpdates.length)}addSubscription(e){this.state.client.logger.debug(this.subscriptionType,(()=>({messageType:"text",message:`Create set with subscription: ${e}`})));const t=new _t({client:this.state.client,subscriptions:[this,e],options:this.state.options});return this.state.isSubscribed||e.state.isSubscribed?(this.state.client.logger.trace(this.subscriptionType,"Subscribe resulting set because the receiver is already subscribed."),t.subscribe(),t):t}register(e){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,e.cursor)}unregister(e){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)}toString(){const e=this.state;return`${this.subscriptionType} { id: ${this.id}, stateId: ${e.id}, entity: ${e.entity.subscriptionNames(!1).pop()}, clonesCount: ${Object.keys(e.clones).length}, isSubscribed: ${e.isSubscribed}, parentSetsCount: ${this.parentSetsCount}, cursor: ${e.cursor?e.cursor.timetoken:"not set"}, referenceTimetoken: ${e.referenceTimetoken?e.referenceTimetoken:"not set"} }`}}class At extends le{constructor(e){var t,s,n,r;super(),this.parameters=e,null!==(t=(n=this.parameters).channels)&&void 0!==t||(n.channels=[]),null!==(s=(r=this.parameters).channelGroups)&&void 0!==s||(r.channelGroups=[])}operation(){return M.PNGetStateOperation}validate(){const{keySet:{subscribeKey:e},channels:t,channelGroups:s}=this.parameters;if(!e)return"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e),{channels:s=[],channelGroups:n=[]}=this.parameters,r={channels:{}};return 1===s.length&&0===n.length?r.channels[s[0]]=t.payload:r.channels=t.payload,r}))}get path(){const{keySet:{subscribeKey:e},uuid:t,channels:s}=this.parameters;return`/v2/presence/sub-key/${e}/channel/${D(null!=s?s:[],",")}/uuid/${t}`}get queryParameters(){const{channelGroups:e}=this.parameters;return e&&0!==e.length?{"channel-group":e.join(",")}:{}}}class Ut extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNSetStateOperation}validate(){const{keySet:{subscribeKey:e},state:t,channels:s=[],channelGroups:n=[]}=this.parameters;return e?t?0===(null==s?void 0:s.length)&&0===(null==n?void 0:n.length)?"Please provide a list of channels and/or channel-groups":void 0:"Missing State":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){return{state:this.deserializeResponse(e).payload}}))}get path(){const{keySet:{subscribeKey:e},uuid:t,channels:s}=this.parameters;return`/v2/presence/sub-key/${e}/channel/${D(null!=s?s:[],",")}/uuid/${$(t)}/data`}get queryParameters(){const{channelGroups:e,state:t}=this.parameters,s={state:JSON.stringify(t)};return e&&0!==e.length&&(s["channel-group"]=e.join(",")),s}}class Rt extends le{constructor(e){super({cancellable:!0}),this.parameters=e}operation(){return M.PNHeartbeatOperation}validate(){const{keySet:{subscribeKey:e},channels:t=[],channelGroups:s=[]}=this.parameters;return e?0===t.length&&0===s.length?"Please provide a list of channels and/or channel-groups":void 0:"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channels:t}=this.parameters;return`/v2/presence/sub-key/${e}/channel/${D(null!=t?t:[],",")}/heartbeat`}get queryParameters(){const{channelGroups:e,state:t,heartbeat:s}=this.parameters,n={heartbeat:`${s}`};return e&&0!==e.length&&(n["channel-group"]=e.join(",")),t&&(n.state=JSON.stringify(t)),n}}class Ft extends le{constructor(e){super(),this.parameters=e,this.parameters.channelGroups&&(this.parameters.channelGroups=Array.from(new Set(this.parameters.channelGroups))),this.parameters.channels&&(this.parameters.channels=Array.from(new Set(this.parameters.channels)))}operation(){return M.PNUnsubscribeOperation}validate(){const{keySet:{subscribeKey:e},channels:t=[],channelGroups:s=[]}=this.parameters;return e?0===t.length&&0===s.length?"At least one `channel` or `channel group` should be provided.":void 0:"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){var e;const{keySet:{subscribeKey:t},channels:s}=this.parameters;return`/v2/presence/sub-key/${t}/channel/${D(null!==(e=null==s?void 0:s.sort())&&void 0!==e?e:[],",")}/leave`}get queryParameters(){const{channelGroups:e}=this.parameters;return e&&0!==e.length?{"channel-group":e.sort().join(",")}:{}}}class $t extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNWhereNowOperation}validate(){if(!this.parameters.keySet.subscribeKey)return"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e);return t.payload?{channels:t.payload.channels}:{channels:[]}}))}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/presence/sub-key/${e}/uuid/${$(t)}`}}class Dt extends le{constructor(e){var t,s,n,r,i,a;super(),this.parameters=e,null!==(t=(r=this.parameters).queryParameters)&&void 0!==t||(r.queryParameters={}),null!==(s=(i=this.parameters).includeUUIDs)&&void 0!==s||(i.includeUUIDs=true),null!==(n=(a=this.parameters).includeState)&&void 0!==n||(a.includeState=false)}operation(){const{channels:e=[],channelGroups:t=[]}=this.parameters;return 0===e.length&&0===t.length?M.PNGlobalHereNowOperation:M.PNHereNowOperation}validate(){if(!this.parameters.keySet.subscribeKey)return"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){var t,s;const n=this.deserializeResponse(e),r="occupancy"in n?1:n.payload.total_channels,i="occupancy"in n?n.occupancy:n.payload.total_occupancy,a={};let o={};if("occupancy"in n){const e=this.parameters.channels[0];o[e]={uuids:null!==(t=n.uuids)&&void 0!==t?t:[],occupancy:i}}else o=null!==(s=n.payload.channels)&&void 0!==s?s:{};return Object.keys(o).forEach((e=>{const t=o[e];a[e]={occupants:this.parameters.includeUUIDs?t.uuids.map((e=>"string"==typeof e?{uuid:e,state:null}:e)):[],name:e,occupancy:t.occupancy}})),{totalChannels:r,totalOccupancy:i,channels:a}}))}get path(){const{keySet:{subscribeKey:e},channels:t,channelGroups:s}=this.parameters;let n=`/v2/presence/sub-key/${e}`;return(t&&t.length>0||s&&s.length>0)&&(n+=`/channel/${D(null!=t?t:[],",")}`),n}get queryParameters(){const{channelGroups:e,includeUUIDs:t,includeState:s,queryParameters:n}=this.parameters;return Object.assign(Object.assign(Object.assign(Object.assign({},t?{}:{disable_uuids:"1"}),null!=s&&s?{state:"1"}:{}),e&&e.length>0?{"channel-group":e.join(",")}:{}),n)}}class xt extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e}operation(){return M.PNDeleteMessagesOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channel?void 0:"Missing channel":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v3/history/sub-key/${e}/channel/${$(t)}`}get queryParameters(){const{start:e,end:t}=this.parameters;return Object.assign(Object.assign({},e?{start:e}:{}),t?{end:t}:{})}}class qt extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNMessageCounts}validate(){const{keySet:{subscribeKey:e},channels:t,timetoken:s,channelTimetokens:n}=this.parameters;return e?t?s&&n?"`timetoken` and `channelTimetokens` are incompatible together":s||n?n&&n.length>1&&n.length!==t.length?"Length of `channelTimetokens` and `channels` do not match":void 0:"`timetoken` or `channelTimetokens` need to be set":"Missing channels":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){return{channels:this.deserializeResponse(e).channels}}))}get path(){return`/v3/history/sub-key/${this.parameters.keySet.subscribeKey}/message-counts/${D(this.parameters.channels)}`}get queryParameters(){let{channelTimetokens:e}=this.parameters;return this.parameters.timetoken&&(e=[this.parameters.timetoken]),Object.assign(Object.assign({},1===e.length?{timetoken:e[0]}:{}),e.length>1?{channelsTimetoken:e.join(",")}:{})}}class Gt extends le{constructor(e){var t,s,n;super(),this.parameters=e,e.count?e.count=Math.min(e.count,100):e.count=100,null!==(t=e.stringifiedTimeToken)&&void 0!==t||(e.stringifiedTimeToken=false),null!==(s=e.includeMeta)&&void 0!==s||(e.includeMeta=false),null!==(n=e.logVerbosity)&&void 0!==n||(e.logVerbosity=false)}operation(){return M.PNHistoryOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channel?void 0:"Missing channel":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e),s=t[0],n=t[1],r=t[2];return Array.isArray(s)?{messages:s.map((e=>{const t=this.processPayload(e.message),s={entry:t.payload,timetoken:e.timetoken};return t.error&&(s.error=t.error),e.meta&&(s.meta=e.meta),s})),startTimeToken:n,endTimeToken:r}:{messages:[],startTimeToken:n,endTimeToken:r}}))}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/history/sub-key/${e}/channel/${$(t)}`}get queryParameters(){const{start:e,end:t,reverse:s,count:n,stringifiedTimeToken:r,includeMeta:i}=this.parameters;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:n,include_token:"true"},e?{start:e}:{}),t?{end:t}:{}),r?{string_message_token:"true"}:{}),null!=s?{reverse:s.toString()}:{}),i?{include_meta:"true"}:{})}processPayload(e){const{crypto:t,logVerbosity:s}=this.parameters;if(!t||"string"!=typeof e)return{payload:e};let n,r;try{const s=t.decrypt(e);n=s instanceof ArrayBuffer?JSON.parse(Gt.decoder.decode(s)):s}catch(t){s&&console.log("decryption error",t.message),n=e,r=`Error while decrypting message content: ${t.message}`}return{payload:n,error:r}}}var Kt;!function(e){e[e.Message=-1]="Message",e[e.Files=4]="Files"}(Kt||(Kt={}));class Lt extends le{constructor(e){var t,s,n,r,i;super(),this.parameters=e;const a=null!==(t=e.includeMessageActions)&&void 0!==t&&t,o=e.channels.length>1||a?25:100;e.count?e.count=Math.min(e.count,o):e.count=o,e.includeUuid?e.includeUUID=e.includeUuid:null!==(s=e.includeUUID)&&void 0!==s||(e.includeUUID=true),null!==(n=e.stringifiedTimeToken)&&void 0!==n||(e.stringifiedTimeToken=false),null!==(r=e.includeMessageType)&&void 0!==r||(e.includeMessageType=true),null!==(i=e.logVerbosity)&&void 0!==i||(e.logVerbosity=false)}operation(){return M.PNFetchMessagesOperation}validate(){const{keySet:{subscribeKey:e},channels:t,includeMessageActions:s}=this.parameters;return e?t?void 0!==s&&s&&t.length>1?"History can return actions data for a single channel only. Either pass a single channel or disable the includeMessageActions flag.":void 0:"Missing channels":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){var t;const s=this.deserializeResponse(e),n=null!==(t=s.channels)&&void 0!==t?t:{},r={};return Object.keys(n).forEach((e=>{r[e]=n[e].map((t=>{null===t.message_type&&(t.message_type=Kt.Message);const s=this.processPayload(e,t),n=Object.assign(Object.assign({channel:e,timetoken:t.timetoken,message:s.payload,messageType:t.message_type},t.custom_message_type?{customMessageType:t.custom_message_type}:{}),{uuid:t.uuid});if(t.actions){const e=n;e.actions=t.actions,e.data=t.actions}return t.meta&&(n.meta=t.meta),s.error&&(n.error=s.error),n}))})),s.more?{channels:r,more:s.more}:{channels:r}}))}get path(){const{keySet:{subscribeKey:e},channels:t,includeMessageActions:s}=this.parameters;return`/v3/${s?"history-with-actions":"history"}/sub-key/${e}/channel/${D(t)}`}get queryParameters(){const{start:e,end:t,count:s,includeCustomMessageType:n,includeMessageType:r,includeMeta:i,includeUUID:a,stringifiedTimeToken:o}=this.parameters;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({max:s},e?{start:e}:{}),t?{end:t}:{}),o?{string_message_token:"true"}:{}),void 0!==i&&i?{include_meta:"true"}:{}),a?{include_uuid:"true"}:{}),null!=n?{include_custom_message_type:n?"true":"false"}:{}),r?{include_message_type:"true"}:{})}processPayload(e,t){const{crypto:s,logVerbosity:n}=this.parameters;if(!s||"string"!=typeof t.message)return{payload:t.message};let r,i;try{const e=s.decrypt(t.message);r=e instanceof ArrayBuffer?JSON.parse(Lt.decoder.decode(e)):e}catch(e){n&&console.log("decryption error",e.message),r=t.message,i=`Error while decrypting message content: ${e.message}`}if(!i&&r&&t.message_type==Kt.Files&&"object"==typeof r&&this.isFileMessage(r)){const t=r;return{payload:{message:t.message,file:Object.assign(Object.assign({},t.file),{url:this.parameters.getFileUrl({channel:e,id:t.file.id,name:t.file.name})})},error:i}}return{payload:r,error:i}}isFileMessage(e){return void 0!==e.file}}class Ht extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNGetMessageActionsOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channel?void 0:"Missing message channel":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e);let s=null,n=null;return t.data.length>0&&(s=t.data[0].actionTimetoken,n=t.data[t.data.length-1].actionTimetoken),{data:t.data,more:t.more,start:s,end:n}}))}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v1/message-actions/${e}/channel/${$(t)}`}get queryParameters(){const{limit:e,start:t,end:s}=this.parameters;return Object.assign(Object.assign(Object.assign({},t?{start:t}:{}),s?{end:s}:{}),e?{limit:e}:{})}}class Bt extends le{constructor(e){super({method:ae.POST}),this.parameters=e}operation(){return M.PNAddMessageActionOperation}validate(){const{keySet:{subscribeKey:e},action:t,channel:s,messageTimetoken:n}=this.parameters;return e?s?n?t?t.value?t.type?t.type.length>15?"Action.type value exceed maximum length of 15":void 0:"Missing Action.type":"Missing Action.value":"Missing Action":"Missing message timetoken":"Missing message channel":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((({data:e})=>({data:e})))}))}get path(){const{keySet:{subscribeKey:e},channel:t,messageTimetoken:s}=this.parameters;return`/v1/message-actions/${e}/channel/${$(t)}/message/${s}`}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"})}get body(){return JSON.stringify(this.parameters.action)}}class Wt extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e}operation(){return M.PNRemoveMessageActionOperation}validate(){const{keySet:{subscribeKey:e},channel:t,messageTimetoken:s,actionTimetoken:n}=this.parameters;return e?t?s?n?void 0:"Missing action timetoken":"Missing message timetoken":"Missing message action channel":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((({data:e})=>({data:e})))}))}get path(){const{keySet:{subscribeKey:e},channel:t,actionTimetoken:s,messageTimetoken:n}=this.parameters;return`/v1/message-actions/${e}/channel/${$(t)}/message/${n}/action/${s}`}}class zt extends le{constructor(e){var t,s;super(),this.parameters=e,null!==(t=(s=this.parameters).storeInHistory)&&void 0!==t||(s.storeInHistory=true)}operation(){return M.PNPublishFileMessageOperation}validate(){const{channel:e,fileId:t,fileName:s}=this.parameters;return e?t?s?void 0:"file name can't be empty":"file id can't be empty":"channel can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){return{timetoken:this.deserializeResponse(e)[2]}}))}get path(){const{message:e,channel:t,keySet:{publishKey:s,subscribeKey:n},fileId:r,fileName:i}=this.parameters,a=Object.assign({file:{name:i,id:r}},e?{message:e}:{});return`/v1/files/publish-file/${s}/${n}/0/${$(t)}/0/${$(this.prepareMessagePayload(a))}`}get queryParameters(){const{customMessageType:e,storeInHistory:t,ttl:s,meta:n}=this.parameters;return Object.assign(Object.assign(Object.assign({store:t?"1":"0"},e?{custom_message_type:e}:{}),s?{ttl:s}:{}),n&&"object"==typeof n?{meta:JSON.stringify(n)}:{})}prepareMessagePayload(e){const{crypto:t}=this.parameters;if(!t)return JSON.stringify(e)||"";const s=t.encrypt(JSON.stringify(e));return JSON.stringify("string"==typeof s?s:u(s))}}class Vt extends le{constructor(e){super({method:ae.LOCAL}),this.parameters=e}operation(){return M.PNGetFileUrlOperation}validate(){const{channel:e,id:t,name:s}=this.parameters;return e?t?s?void 0:"file name can't be empty":"file id can't be empty":"channel can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){return e.url}))}get path(){const{channel:e,id:t,name:s,keySet:{subscribeKey:n}}=this.parameters;return`/v1/files/${n}/channels/${$(e)}/files/${t}/${s}`}}class Jt extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e}operation(){return M.PNDeleteFileOperation}validate(){const{channel:e,id:t,name:s}=this.parameters;return e?t?s?void 0:"file name can't be empty":"file id can't be empty":"channel can't be empty"}get path(){const{keySet:{subscribeKey:e},id:t,channel:s,name:n}=this.parameters;return`/v1/files/${e}/channels/${$(s)}/files/${t}/${n}`}}class Xt extends le{constructor(e){var t,s;super(),this.parameters=e,null!==(t=(s=this.parameters).limit)&&void 0!==t||(s.limit=100)}operation(){return M.PNListFilesOperation}validate(){if(!this.parameters.channel)return"channel can't be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v1/files/${e}/channels/${$(t)}/files`}get queryParameters(){const{limit:e,next:t}=this.parameters;return Object.assign({limit:e},t?{next:t}:{})}}class Qt extends le{constructor(e){super({method:ae.POST}),this.parameters=e}operation(){return M.PNGenerateUploadUrlOperation}validate(){return this.parameters.channel?this.parameters.name?void 0:"'name' can't be empty":"channel can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e);return{id:t.data.id,name:t.data.name,url:t.file_upload_request.url,formFields:t.file_upload_request.form_fields}}))}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v1/files/${e}/channels/${$(t)}/generate-upload-url`}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"})}get body(){return JSON.stringify({name:this.parameters.name})}}class Yt extends le{constructor(e){super({method:ae.POST}),this.parameters=e;const t=e.file.mimeType;t&&(e.formFields=e.formFields.map((e=>"Content-Type"===e.name?{name:e.name,value:t}:e)))}operation(){return M.PNPublishFileOperation}validate(){const{fileId:e,fileName:t,file:s,uploadUrl:n}=this.parameters;return e?t?s?n?void 0:"Validation failed: file upload 'url' can't be empty":"Validation failed: 'file' can't be empty":"Validation failed: file 'name' can't be empty":"Validation failed: file 'id' can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){return{status:e.status,message:e.body?Yt.decoder.decode(e.body):"OK"}}))}request(){return Object.assign(Object.assign({},super.request()),{origin:new URL(this.parameters.uploadUrl).origin,timeout:300})}get path(){const{pathname:e,search:t}=new URL(this.parameters.uploadUrl);return`${e}${t}`}get body(){return this.parameters.file}get formData(){return this.parameters.formFields}}class Zt{constructor(e){var t;if(this.parameters=e,this.file=null===(t=this.parameters.PubNubFile)||void 0===t?void 0:t.create(e.file),!this.file)throw new Error("File upload error: unable to create File object.")}process(){return i(this,void 0,void 0,(function*(){let e,t;return this.generateFileUploadUrl().then((s=>(e=s.name,t=s.id,this.uploadFile(s)))).then((e=>{if(204!==e.status)throw new d("Upload to bucket was unsuccessful",{error:!0,statusCode:e.status,category:h.PNUnknownCategory,operation:M.PNPublishFileOperation,errorData:{message:e.message}})})).then((()=>this.publishFileMessage(t,e))).catch((e=>{if(e instanceof d)throw e;const t=e instanceof I?e:I.create(e);throw new d("File upload error.",t.toStatus(M.PNPublishFileOperation))}))}))}generateFileUploadUrl(){return i(this,void 0,void 0,(function*(){const e=new Qt(Object.assign(Object.assign({},this.parameters),{name:this.file.name,keySet:this.parameters.keySet}));return this.parameters.sendRequest(e)}))}uploadFile(e){return i(this,void 0,void 0,(function*(){const{cipherKey:t,PubNubFile:s,crypto:n,cryptography:r}=this.parameters,{id:i,name:a,url:o,formFields:c}=e;return this.parameters.PubNubFile.supportsEncryptFile&&(!t&&n?this.file=yield n.encryptFile(this.file,s):t&&r&&(this.file=yield r.encryptFile(t,this.file,s))),this.parameters.sendRequest(new Yt({fileId:i,fileName:a,file:this.file,uploadUrl:o,formFields:c}))}))}publishFileMessage(e,t){return i(this,void 0,void 0,(function*(){var s,n,r,i;let a,o={timetoken:"0"},c=this.parameters.fileUploadPublishRetryLimit,u=!1;do{try{o=yield this.parameters.publishFile(Object.assign(Object.assign({},this.parameters),{fileId:e,fileName:t})),u=!0}catch(e){e instanceof d&&(a=e),c-=1}}while(!u&&c>0);if(u)return{status:200,timetoken:o.timetoken,id:e,name:t};throw new d("Publish failed. You may want to execute that operation manually using pubnub.publishFile",{error:!0,category:null!==(n=null===(s=a.status)||void 0===s?void 0:s.category)&&void 0!==n?n:h.PNUnknownCategory,statusCode:null!==(i=null===(r=a.status)||void 0===r?void 0:r.statusCode)&&void 0!==i?i:0,channel:this.parameters.channel,id:e,name:t})}))}}class es{constructor(e,t){this.subscriptionStateIds=[],this.client=t,this._nameOrId=e}get entityType(){return"Channel"}get subscriptionType(){return Pt.Channel}subscriptionNames(e){return[this._nameOrId,...e&&!this._nameOrId.endsWith("-pnpres")?[`${this._nameOrId}-pnpres`]:[]]}subscription(e){return new Mt({client:this.client,entity:this,options:e})}get subscriptionsCount(){return this.subscriptionStateIds.length}increaseSubscriptionCount(e){this.subscriptionStateIds.includes(e)||this.subscriptionStateIds.push(e)}decreaseSubscriptionCount(e){{const t=this.subscriptionStateIds.indexOf(e);t>=0&&this.subscriptionStateIds.splice(t,1)}}toString(){return`${this.entityType} { nameOrId: ${this._nameOrId}, subscriptionsCount: ${this.subscriptionsCount} }`}}class ts extends es{get entityType(){return"ChannelMetadata"}get id(){return this._nameOrId}subscriptionNames(e){return[this.id]}}class ss extends es{get entityType(){return"ChannelGroups"}get name(){return this._nameOrId}get subscriptionType(){return Pt.ChannelGroup}}class ns extends es{get entityType(){return"UserMetadata"}get id(){return this._nameOrId}subscriptionNames(e){return[this.id]}}class rs extends es{get entityType(){return"Channel"}get name(){return this._nameOrId}}class is extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNRemoveChannelsFromGroupOperation}validate(){const{keySet:{subscribeKey:e},channels:t,channelGroup:s}=this.parameters;return e?s?t?void 0:"Missing channels":"Missing Channel Group":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channelGroup:t}=this.parameters;return`/v1/channel-registration/sub-key/${e}/channel-group/${$(t)}`}get queryParameters(){return{remove:this.parameters.channels.join(",")}}}class as extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNAddChannelsToGroupOperation}validate(){const{keySet:{subscribeKey:e},channels:t,channelGroup:s}=this.parameters;return e?s?t?void 0:"Missing channels":"Missing Channel Group":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channelGroup:t}=this.parameters;return`/v1/channel-registration/sub-key/${e}/channel-group/${$(t)}`}get queryParameters(){return{add:this.parameters.channels.join(",")}}}class os extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNChannelsForGroupOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channelGroup?void 0:"Missing Channel Group":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){return{channels:this.deserializeResponse(e).payload.channels}}))}get path(){const{keySet:{subscribeKey:e},channelGroup:t}=this.parameters;return`/v1/channel-registration/sub-key/${e}/channel-group/${$(t)}`}}class cs extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNRemoveGroupOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channelGroup?void 0:"Missing Channel Group":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channelGroup:t}=this.parameters;return`/v1/channel-registration/sub-key/${e}/channel-group/${$(t)}/remove`}}class us extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNChannelGroupsOperation}validate(){if(!this.parameters.keySet.subscribeKey)return"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){return{groups:this.deserializeResponse(e).payload.groups}}))}get path(){return`/v1/channel-registration/sub-key/${this.parameters.keySet.subscribeKey}/channel-group`}}class ls{constructor(e,t,s){this.sendRequest=s,this.logger=e,this.keySet=t}listChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"List channel group channels with parameters:"})));const s=new os(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=e=>{e&&this.logger.info("PubNub",`List channel group channels success. Received ${e.channels.length} channels.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}listGroups(e){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub","List all channel groups.");const t=new us({keySet:this.keySet}),s=e=>{e&&this.logger.info("PubNub",`List all channel groups success. Received ${e.groups.length} groups.`)};return e?this.sendRequest(t,((t,n)=>{s(n),e(t,n)})):this.sendRequest(t).then((e=>(s(e),e)))}))}addChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Add channels to the channel group with parameters:"})));const s=new as(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.info("PubNub","Add channels to the channel group success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}removeChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove channels from the channel group with parameters:"})));const s=new is(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.info("PubNub","Remove channels from the channel group success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}deleteGroup(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove a channel group with parameters:"})));const s=new cs(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.info("PubNub",`Remove a channel group success. Removed '${e.channelGroup}' channel group.'`)};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}}class hs extends le{constructor(e){var t,s;super(),this.parameters=e,"apns2"===this.parameters.pushGateway&&(null!==(t=(s=this.parameters).environment)&&void 0!==t||(s.environment="development")),this.parameters.count&&this.parameters.count>1e3&&(this.parameters.count=1e3)}operation(){throw Error("Should be implemented in subclass.")}validate(){const{keySet:{subscribeKey:e},action:t,device:s,pushGateway:n}=this.parameters;return e?s?"add"!==t&&"remove"!==t||"channels"in this.parameters&&0!==this.parameters.channels.length?n?"apns2"!==this.parameters.pushGateway||this.parameters.topic?void 0:"Missing APNS2 topic":"Missing GW Type (pushGateway: gcm or apns2)":"Missing Channels":"Missing Device ID (device)":"Missing Subscribe Key"}get path(){const{keySet:{subscribeKey:e},action:t,device:s,pushGateway:n}=this.parameters;let r="apns2"===n?`/v2/push/sub-key/${e}/devices-apns2/${s}`:`/v1/push/sub-key/${e}/devices/${s}`;return"remove-device"===t&&(r=`${r}/remove`),r}get queryParameters(){const{start:e,count:t}=this.parameters;let s=Object.assign(Object.assign({type:this.parameters.pushGateway},e?{start:e}:{}),t&&t>0?{count:t}:{});if("channels"in this.parameters&&(s[this.parameters.action]=this.parameters.channels.join(",")),"apns2"===this.parameters.pushGateway){const{environment:e,topic:t}=this.parameters;s=Object.assign(Object.assign({},s),{environment:e,topic:t})}return s}}class ds extends hs{constructor(e){super(Object.assign(Object.assign({},e),{action:"remove"}))}operation(){return M.PNRemovePushNotificationEnabledChannelsOperation}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}}class ps extends hs{constructor(e){super(Object.assign(Object.assign({},e),{action:"list"}))}operation(){return M.PNPushNotificationEnabledChannelsOperation}parse(e){return i(this,void 0,void 0,(function*(){return{channels:this.deserializeResponse(e)}}))}}class gs extends hs{constructor(e){super(Object.assign(Object.assign({},e),{action:"add"}))}operation(){return M.PNAddPushNotificationEnabledChannelsOperation}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}}class bs extends hs{constructor(e){super(Object.assign(Object.assign({},e),{action:"remove-device"}))}operation(){return M.PNRemoveAllPushNotificationsOperation}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}}class ys{constructor(e,t,s){this.sendRequest=s,this.logger=e,this.keySet=t}listChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"List push-enabled channels with parameters:"})));const s=new ps(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=e=>{e&&this.logger.debug("PubNub",`List push-enabled channels success. Received ${e.channels.length} channels.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}addChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Add push-enabled channels with parameters:"})));const s=new gs(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.debug("PubNub","Add push-enabled channels success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}removeChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove push-enabled channels with parameters:"})));const s=new ds(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.debug("PubNub","Remove push-enabled channels success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}deleteDevice(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove push notifications for device with parameters:"})));const s=new bs(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.debug("PubNub","Remove push notifications for device success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}}class ms extends le{constructor(e){var t,s,n,r,i,a;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(i=e.include).customFields)&&void 0!==s||(i.customFields=false),null!==(n=(a=e.include).totalCount)&&void 0!==n||(a.totalCount=false),null!==(r=e.limit)&&void 0!==r||(e.limit=100)}operation(){return M.PNGetAllChannelMetadataOperation}get path(){return`/v2/objects/${this.parameters.keySet.subscribeKey}/channels`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";return i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e)),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({include:["status","type",...e.customFields?["custom"]:[]].join(","),count:`${e.totalCount}`},s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}}class fs extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e}operation(){return M.PNRemoveChannelMetadataOperation}validate(){if(!this.parameters.channel)return"Channel cannot be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${$(t)}`}}class vs extends le{constructor(e){var t,s,n,r,i,a,o,c,u,l,h,d,p,g,b,y,m,f;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(h=e.include).customFields)&&void 0!==s||(h.customFields=false),null!==(n=(d=e.include).totalCount)&&void 0!==n||(d.totalCount=false),null!==(r=(p=e.include).statusField)&&void 0!==r||(p.statusField=false),null!==(i=(g=e.include).typeField)&&void 0!==i||(g.typeField=false),null!==(a=(b=e.include).channelFields)&&void 0!==a||(b.channelFields=false),null!==(o=(y=e.include).customChannelFields)&&void 0!==o||(y.customChannelFields=false),null!==(c=(m=e.include).channelStatusField)&&void 0!==c||(m.channelStatusField=false),null!==(u=(f=e.include).channelTypeField)&&void 0!==u||(f.channelTypeField=false),null!==(l=e.limit)&&void 0!==l||(e.limit=100),this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNGetMembershipsOperation}validate(){if(!this.parameters.uuid)return"'uuid' cannot be empty"}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${$(t)}/channels`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e));const a=[];return e.statusField&&a.push("status"),e.typeField&&a.push("type"),e.customFields&&a.push("custom"),e.channelFields&&a.push("channel"),e.channelStatusField&&a.push("channel.status"),e.channelTypeField&&a.push("channel.type"),e.customChannelFields&&a.push("channel.custom"),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:`${e.totalCount}`},a.length>0?{include:a.join(",")}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}}class Ss extends le{constructor(e){var t,s,n,r,i,a,o,c,u,l,h,d,p,g,b,y,m,f;super({method:ae.PATCH}),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(h=e.include).customFields)&&void 0!==s||(h.customFields=false),null!==(n=(d=e.include).totalCount)&&void 0!==n||(d.totalCount=false),null!==(r=(p=e.include).statusField)&&void 0!==r||(p.statusField=false),null!==(i=(g=e.include).typeField)&&void 0!==i||(g.typeField=false),null!==(a=(b=e.include).channelFields)&&void 0!==a||(b.channelFields=false),null!==(o=(y=e.include).customChannelFields)&&void 0!==o||(y.customChannelFields=false),null!==(c=(m=e.include).channelStatusField)&&void 0!==c||(m.channelStatusField=false),null!==(u=(f=e.include).channelTypeField)&&void 0!==u||(f.channelTypeField=false),null!==(l=e.limit)&&void 0!==l||(e.limit=100),this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNSetMembershipsOperation}validate(){const{uuid:e,channels:t}=this.parameters;return e?t&&0!==t.length?void 0:"Channels cannot be empty":"'uuid' cannot be empty"}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${$(t)}/channels`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e));const a=["channel.status","channel.type","status"];return e.statusField&&a.push("status"),e.typeField&&a.push("type"),e.customFields&&a.push("custom"),e.channelFields&&a.push("channel"),e.channelStatusField&&a.push("channel.status"),e.channelTypeField&&a.push("channel.type"),e.customChannelFields&&a.push("channel.custom"),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:`${e.totalCount}`},a.length>0?{include:a.join(",")}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"})}get body(){const{channels:e,type:t}=this.parameters;return JSON.stringify({[`${t}`]:e.map((e=>"string"==typeof e?{channel:{id:e}}:{channel:{id:e.id},status:e.status,type:e.type,custom:e.custom}))})}}class ws extends le{constructor(e){var t,s,n,r;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(r=e.include).customFields)&&void 0!==s||(r.customFields=false),null!==(n=e.limit)&&void 0!==n||(e.limit=100)}operation(){return M.PNGetAllUUIDMetadataOperation}get path(){return`/v2/objects/${this.parameters.keySet.subscribeKey}/uuids`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";return i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e)),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({include:["status","type",...e.customFields?["custom"]:[]].join(",")},void 0!==e.totalCount?{count:`${e.totalCount}`}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}}class Os extends le{constructor(e){var t,s,n;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(n=e.include).customFields)&&void 0!==s||(n.customFields=true)}operation(){return M.PNGetChannelMetadataOperation}validate(){if(!this.parameters.channel)return"Channel cannot be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${$(t)}`}get queryParameters(){return{include:["status","type",...this.parameters.include.customFields?["custom"]:[]].join(",")}}}class ks extends le{constructor(e){var t,s,n;super({method:ae.PATCH}),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(n=e.include).customFields)&&void 0!==s||(n.customFields=true)}operation(){return M.PNSetChannelMetadataOperation}validate(){return this.parameters.channel?this.parameters.data?void 0:"Data cannot be empty":"Channel cannot be empty"}get headers(){var e;let t=null!==(e=super.headers)&&void 0!==e?e:{};return this.parameters.ifMatchesEtag&&(t=Object.assign(Object.assign({},t),{"If-Match":this.parameters.ifMatchesEtag})),Object.assign(Object.assign({},t),{"Content-Type":"application/json"})}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${$(t)}`}get queryParameters(){return{include:["status","type",...this.parameters.include.customFields?["custom"]:[]].join(",")}}get body(){return JSON.stringify(this.parameters.data)}}class Cs extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e,this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNRemoveUUIDMetadataOperation}validate(){if(!this.parameters.uuid)return"'uuid' cannot be empty"}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${$(t)}`}}class Ps extends le{constructor(e){var t,s,n,r,i,a,o,c,u,l,h,d,p,g,b,y,m,f;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(h=e.include).customFields)&&void 0!==s||(h.customFields=false),null!==(n=(d=e.include).totalCount)&&void 0!==n||(d.totalCount=false),null!==(r=(p=e.include).statusField)&&void 0!==r||(p.statusField=false),null!==(i=(g=e.include).typeField)&&void 0!==i||(g.typeField=false),null!==(a=(b=e.include).UUIDFields)&&void 0!==a||(b.UUIDFields=false),null!==(o=(y=e.include).customUUIDFields)&&void 0!==o||(y.customUUIDFields=false),null!==(c=(m=e.include).UUIDStatusField)&&void 0!==c||(m.UUIDStatusField=false),null!==(u=(f=e.include).UUIDTypeField)&&void 0!==u||(f.UUIDTypeField=false),null!==(l=e.limit)&&void 0!==l||(e.limit=100)}operation(){return M.PNSetMembersOperation}validate(){if(!this.parameters.channel)return"Channel cannot be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${$(t)}/uuids`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e));const a=[];return e.statusField&&a.push("status"),e.typeField&&a.push("type"),e.customFields&&a.push("custom"),e.UUIDFields&&a.push("uuid"),e.UUIDStatusField&&a.push("uuid.status"),e.UUIDTypeField&&a.push("uuid.type"),e.customUUIDFields&&a.push("uuid.custom"),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:`${e.totalCount}`},a.length>0?{include:a.join(",")}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}}class js extends le{constructor(e){var t,s,n,r,i,a,o,c,u,l,h,d,p,g,b,y,m,f;super({method:ae.PATCH}),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(h=e.include).customFields)&&void 0!==s||(h.customFields=false),null!==(n=(d=e.include).totalCount)&&void 0!==n||(d.totalCount=false),null!==(r=(p=e.include).statusField)&&void 0!==r||(p.statusField=false),null!==(i=(g=e.include).typeField)&&void 0!==i||(g.typeField=false),null!==(a=(b=e.include).UUIDFields)&&void 0!==a||(b.UUIDFields=false),null!==(o=(y=e.include).customUUIDFields)&&void 0!==o||(y.customUUIDFields=false),null!==(c=(m=e.include).UUIDStatusField)&&void 0!==c||(m.UUIDStatusField=false),null!==(u=(f=e.include).UUIDTypeField)&&void 0!==u||(f.UUIDTypeField=false),null!==(l=e.limit)&&void 0!==l||(e.limit=100)}operation(){return M.PNSetMembersOperation}validate(){const{channel:e,uuids:t}=this.parameters;return e?t&&0!==t.length?void 0:"UUIDs cannot be empty":"Channel cannot be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${$(t)}/uuids`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e));const a=["uuid.status","uuid.type","type"];return e.statusField&&a.push("status"),e.typeField&&a.push("type"),e.customFields&&a.push("custom"),e.UUIDFields&&a.push("uuid"),e.UUIDStatusField&&a.push("uuid.status"),e.UUIDTypeField&&a.push("uuid.type"),e.customUUIDFields&&a.push("uuid.custom"),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:`${e.totalCount}`},a.length>0?{include:a.join(",")}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"})}get body(){const{uuids:e,type:t}=this.parameters;return JSON.stringify({[`${t}`]:e.map((e=>"string"==typeof e?{uuid:{id:e}}:{uuid:{id:e.id},status:e.status,type:e.type,custom:e.custom}))})}}class Es extends le{constructor(e){var t,s,n;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(n=e.include).customFields)&&void 0!==s||(n.customFields=true),this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNGetUUIDMetadataOperation}validate(){if(!this.parameters.uuid)return"'uuid' cannot be empty"}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${$(t)}`}get queryParameters(){const{include:e}=this.parameters;return{include:["status","type",...e.customFields?["custom"]:[]].join(",")}}}class Ns extends le{constructor(e){var t,s,n;super({method:ae.PATCH}),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(n=e.include).customFields)&&void 0!==s||(n.customFields=true),this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNSetUUIDMetadataOperation}validate(){return this.parameters.uuid?this.parameters.data?void 0:"Data cannot be empty":"'uuid' cannot be empty"}get headers(){var e;let t=null!==(e=super.headers)&&void 0!==e?e:{};return this.parameters.ifMatchesEtag&&(t=Object.assign(Object.assign({},t),{"If-Match":this.parameters.ifMatchesEtag})),Object.assign(Object.assign({},t),{"Content-Type":"application/json"})}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${$(t)}`}get queryParameters(){return{include:["status","type",...this.parameters.include.customFields?["custom"]:[]].join(",")}}get body(){return JSON.stringify(this.parameters.data)}}class Ts{constructor(e,t){this.keySet=e.keySet,this.configuration=e,this.sendRequest=t}get logger(){return this.configuration.logger()}getAllUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{},details:"Get all UUID metadata objects with parameters:"}))),this._getAllUUIDMetadata(e,t)}))}_getAllUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0);const n=new ws(Object.assign(Object.assign({},s),{keySet:this.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Get all UUID metadata success. Received ${e.totalCount} UUID metadata objects.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}getUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{uuid:this.configuration.userId},details:`Get ${e&&"function"!=typeof e?"":" current"} UUID metadata object with parameters:`}))),this._getUUIDMetadata(e,t)}))}_getUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){var s;const n=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0),n.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),n.uuid=n.userId),null!==(s=n.uuid)&&void 0!==s||(n.uuid=this.configuration.userId);const r=new Es(Object.assign(Object.assign({},n),{keySet:this.keySet})),i=e=>{e&&this.logger.debug("PubNub",`Get UUID metadata object success. Received '${n.uuid}' UUID metadata object.`)};return t?this.sendRequest(r,((e,s)=>{i(s),t(e,s)})):this.sendRequest(r).then((e=>(i(e),e)))}))}setUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set UUID metadata object with parameters:"}))),this._setUUIDMetadata(e,t)}))}_setUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){var s;e.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),e.uuid=e.userId),null!==(s=e.uuid)&&void 0!==s||(e.uuid=this.configuration.userId);const n=new Ns(Object.assign(Object.assign({},e),{keySet:this.keySet})),r=t=>{t&&this.logger.debug("PubNub",`Set UUID metadata object success. Updated '${e.uuid}' UUID metadata object.'`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}removeUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{uuid:this.configuration.userId},details:`Remove${e&&"function"!=typeof e?"":" current"} UUID metadata object with parameters:`}))),this._removeUUIDMetadata(e,t)}))}_removeUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){var s;const n=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0),n.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),n.uuid=n.userId),null!==(s=n.uuid)&&void 0!==s||(n.uuid=this.configuration.userId);const r=new Cs(Object.assign(Object.assign({},n),{keySet:this.keySet})),i=e=>{e&&this.logger.debug("PubNub",`Remove UUID metadata object success. Removed '${n.uuid}' UUID metadata object.`)};return t?this.sendRequest(r,((e,s)=>{i(s),t(e,s)})):this.sendRequest(r).then((e=>(i(e),e)))}))}getAllChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{},details:"Get all Channel metadata objects with parameters:"}))),this._getAllChannelMetadata(e,t)}))}_getAllChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0);const n=new ms(Object.assign(Object.assign({},s),{keySet:this.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Get all Channel metadata objects success. Received ${e.totalCount} Channel metadata objects.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}getChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get Channel metadata object with parameters:"}))),this._getChannelMetadata(e,t)}))}_getChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=new Os(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Get Channel metadata object success. Received '${e.channel}' Channel metadata object.'`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}setChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set Channel metadata object with parameters:"}))),this._setChannelMetadata(e,t)}))}_setChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=new ks(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Set Channel metadata object success. Updated '${e.channel}' Channel metadata object.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}removeChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove Channel metadata object with parameters:"}))),this._removeChannelMetadata(e,t)}))}_removeChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=new fs(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Remove Channel metadata object success. Removed '${e.channel}' Channel metadata object.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}getChannelMembers(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get channel members with parameters:"})));const s=new Ps(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Get channel members success. Received ${e.totalCount} channel members.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}setChannelMembers(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set channel members with parameters:"})));const s=new js(Object.assign(Object.assign({},e),{type:"set",keySet:this.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Set channel members success. There are ${e.totalCount} channel members now.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}removeChannelMembers(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove channel members with parameters:"})));const s=new js(Object.assign(Object.assign({},e),{type:"delete",keySet:this.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Remove channel members success. There are ${e.totalCount} channel members now.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}getMemberships(e,t){return i(this,void 0,void 0,(function*(){var s;const n=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0),n.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),n.uuid=n.userId),null!==(s=n.uuid)&&void 0!==s||(n.uuid=this.configuration.userId),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},n),details:"Get memberships with parameters:"})));const r=new vs(Object.assign(Object.assign({},n),{keySet:this.keySet})),i=e=>{e&&this.logger.debug("PubNub",`Get memberships success. Received ${e.totalCount} memberships.`)};return t?this.sendRequest(r,((e,s)=>{i(s),t(e,s)})):this.sendRequest(r).then((e=>(i(e),e)))}))}setMemberships(e,t){return i(this,void 0,void 0,(function*(){var s;e.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),e.uuid=e.userId),null!==(s=e.uuid)&&void 0!==s||(e.uuid=this.configuration.userId),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set memberships with parameters:"})));const n=new Ss(Object.assign(Object.assign({},e),{type:"set",keySet:this.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Set memberships success. There are ${e.totalCount} memberships now.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}removeMemberships(e,t){return i(this,void 0,void 0,(function*(){var s;e.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),e.uuid=e.userId),null!==(s=e.uuid)&&void 0!==s||(e.uuid=this.configuration.userId),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove memberships with parameters:"})));const n=new Ss(Object.assign(Object.assign({},e),{type:"delete",keySet:this.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Remove memberships success. There are ${e.totalCount} memberships now.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}fetchMemberships(e,t){return i(this,void 0,void 0,(function*(){var s,n;if(this.logger.warn("PubNub","'fetchMemberships' is deprecated. Use 'pubnub.objects.getChannelMembers' or 'pubnub.objects.getMemberships' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fetch memberships with parameters:"}))),"spaceId"in e){const n=e,r={channel:null!==(s=n.spaceId)&&void 0!==s?s:n.channel,filter:n.filter,limit:n.limit,page:n.page,include:Object.assign({},n.include),sort:n.sort?Object.fromEntries(Object.entries(n.sort).map((([e,t])=>[e.replace("user","uuid"),t]))):void 0},i=e=>({status:e.status,data:e.data.map((e=>({user:e.uuid,custom:e.custom,updated:e.updated,eTag:e.eTag}))),totalCount:e.totalCount,next:e.next,prev:e.prev});return t?this.getChannelMembers(r,((e,s)=>{t(e,s?i(s):s)})):this.getChannelMembers(r).then(i)}const r=e,i={uuid:null!==(n=r.userId)&&void 0!==n?n:r.uuid,filter:r.filter,limit:r.limit,page:r.page,include:Object.assign({},r.include),sort:r.sort?Object.fromEntries(Object.entries(r.sort).map((([e,t])=>[e.replace("space","channel"),t]))):void 0},a=e=>({status:e.status,data:e.data.map((e=>({space:e.channel,custom:e.custom,updated:e.updated,eTag:e.eTag}))),totalCount:e.totalCount,next:e.next,prev:e.prev});return t?this.getMemberships(i,((e,s)=>{t(e,s?a(s):s)})):this.getMemberships(i).then(a)}))}addMemberships(e,t){return i(this,void 0,void 0,(function*(){var s,n,r,i,a,o;if(this.logger.warn("PubNub","'addMemberships' is deprecated. Use 'pubnub.objects.setChannelMembers' or 'pubnub.objects.setMemberships' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Add memberships with parameters:"}))),"spaceId"in e){const i=e,a={channel:null!==(s=i.spaceId)&&void 0!==s?s:i.channel,uuids:null!==(r=null===(n=i.users)||void 0===n?void 0:n.map((e=>"string"==typeof e?e:{id:e.userId,custom:e.custom})))&&void 0!==r?r:i.uuids,limit:0};return t?this.setChannelMembers(a,t):this.setChannelMembers(a)}const c=e,u={uuid:null!==(i=c.userId)&&void 0!==i?i:c.uuid,channels:null!==(o=null===(a=c.spaces)||void 0===a?void 0:a.map((e=>"string"==typeof e?e:{id:e.spaceId,custom:e.custom})))&&void 0!==o?o:c.channels,limit:0};return t?this.setMemberships(u,t):this.setMemberships(u)}))}}class _s extends le{constructor(){super()}operation(){return M.PNTimeOperation}parse(e){return i(this,void 0,void 0,(function*(){return{timetoken:this.deserializeResponse(e)[0]}}))}get path(){return"/time/0"}}class Is extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNDownloadFileOperation}validate(){const{channel:e,id:t,name:s}=this.parameters;return e?t?s?void 0:"file name can't be empty":"file id can't be empty":"channel can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){const{cipherKey:t,crypto:s,cryptography:n,name:r,PubNubFile:i}=this.parameters,a=e.headers["content-type"];let o,c=e.body;return i.supportsEncryptFile&&(t||s)&&(t&&n?c=yield n.decrypt(t,c):!t&&s&&(o=yield s.decryptFile(i.create({data:c,name:r,mimeType:a}),i))),o||i.create({data:c,name:r,mimeType:a})}))}get path(){const{keySet:{subscribeKey:e},channel:t,id:s,name:n}=this.parameters;return`/v1/files/${e}/channels/${$(t)}/files/${s}/${n}`}}class Ms{static notificationPayload(e,t){return new we(e,t)}static generateUUID(){return te.createUUID()}constructor(e){if(this.eventHandleCapable={},this.entities={},this._configuration=e.configuration,this.cryptography=e.cryptography,this.tokenManager=e.tokenManager,this.transport=e.transport,this.crypto=e.crypto,this.logger.debug("PubNub",(()=>({messageType:"object",message:e.configuration,details:"Create with configuration:",ignoredKeys:(e,t)=>"function"==typeof t[e]||e.startsWith("_")}))),this._objects=new Ts(this._configuration,this.sendRequest.bind(this)),this._channelGroups=new ls(this._configuration.logger(),this._configuration.keySet,this.sendRequest.bind(this)),this._push=new ys(this._configuration.logger(),this._configuration.keySet,this.sendRequest.bind(this)),this.eventDispatcher=new ge,this._configuration.enableEventEngine){this.logger.debug("PubNub","Using new subscription loop management.");let e=this._configuration.getHeartbeatInterval();this.presenceState={},e&&(this.presenceEventEngine=new Ye({heartbeat:(e,t)=>(this.logger.trace("PresenceEventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Heartbeat with parameters:"}))),this.heartbeat(e,t)),leave:e=>{this.logger.trace("PresenceEventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave with parameters:"}))),this.makeUnsubscribe(e,(()=>{}))},heartbeatDelay:()=>new Promise(((t,s)=>{e=this._configuration.getHeartbeatInterval(),e?setTimeout(t,1e3*e):s(new d("Heartbeat interval has been reset."))})),emitStatus:e=>this.emitStatus(e),config:this._configuration,presenceState:this.presenceState})),this.eventEngine=new St({handshake:e=>(this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Handshake with parameters:",ignoredKeys:["abortSignal","crypto","timeout","keySet","getFileUrl"]}))),this.subscribeHandshake(e)),receiveMessages:e=>(this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Receive messages with parameters:",ignoredKeys:["abortSignal","crypto","timeout","keySet","getFileUrl"]}))),this.subscribeReceiveMessages(e)),delay:e=>new Promise((t=>setTimeout(t,e))),join:e=>{var t,s;this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Join with parameters:"}))),e&&0===(null!==(t=e.channels)&&void 0!==t?t:[]).length&&0===(null!==(s=e.groups)&&void 0!==s?s:[]).length?this.logger.trace("EventEngine","Ignoring 'join' announcement request."):this.join(e)},leave:e=>{var t,s;this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave with parameters:"}))),e&&0===(null!==(t=e.channels)&&void 0!==t?t:[]).length&&0===(null!==(s=e.groups)&&void 0!==s?s:[]).length?this.logger.trace("EventEngine","Ignoring 'leave' announcement request."):this.leave(e)},leaveAll:e=>{this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave all with parameters:"}))),this.leaveAll(e)},presenceReconnect:e=>{this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Reconnect with parameters:"}))),this.presenceReconnect(e)},presenceDisconnect:e=>{this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Disconnect with parameters:"}))),this.presenceDisconnect(e)},presenceState:this.presenceState,config:this._configuration,emitMessages:(e,t)=>{try{this.logger.debug("EventEngine",(()=>({messageType:"object",message:t.map((e=>{const t=e.type===he.Message||e.type===he.Signal?B(e.data.message):void 0;return t?{type:e.type,data:Object.assign(Object.assign({},e.data),{pn_mfp:t})}:e})),details:"Received events:"}))),t.forEach((t=>this.emitEvent(e,t)))}catch(e){const t={error:!0,category:h.PNUnknownCategory,errorData:e,statusCode:0};this.emitStatus(t)}},emitStatus:e=>this.emitStatus(e)})}else this.logger.debug("PubNub","Using legacy subscription loop management."),this.subscriptionManager=new me(this._configuration,((e,t)=>{try{this.emitEvent(e,t)}catch(e){const t={error:!0,category:h.PNUnknownCategory,errorData:e,statusCode:0};this.emitStatus(t)}}),this.emitStatus.bind(this),((e,t)=>{this.logger.trace("SubscriptionManager",(()=>({messageType:"object",message:Object.assign({},e),details:"Subscribe with parameters:",ignoredKeys:["crypto","timeout","keySet","getFileUrl"]}))),this.makeSubscribe(e,t)}),((e,t)=>(this.logger.trace("SubscriptionManager",(()=>({messageType:"object",message:Object.assign({},e),details:"Heartbeat with parameters:",ignoredKeys:["crypto","timeout","keySet","getFileUrl"]}))),this.heartbeat(e,t))),((e,t)=>{this.logger.trace("SubscriptionManager",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave with parameters:"}))),this.makeUnsubscribe(e,t)}),this.time.bind(this))}get configuration(){return this._configuration}get _config(){return this.configuration}get authKey(){var e;return null!==(e=this._configuration.authKey)&&void 0!==e?e:void 0}getAuthKey(){return this.authKey}setAuthKey(e){this.logger.debug("PubNub",`Set auth key: ${e}`),this._configuration.setAuthKey(e),this.onAuthenticationChange&&this.onAuthenticationChange(e)}get userId(){return this._configuration.userId}set userId(e){if(!e||"string"!=typeof e||0===e.trim().length){const e=new Error("Missing or invalid userId parameter. Provide a valid string userId");throw this.logger.error("PubNub",(()=>({messageType:"error",message:e}))),e}this.logger.debug("PubNub",`Set user ID: ${e}`),this._configuration.userId=e,this.onUserIdChange&&this.onUserIdChange(this._configuration.userId)}getUserId(){return this._configuration.userId}setUserId(e){this.userId=e}get filterExpression(){var e;return null!==(e=this._configuration.getFilterExpression())&&void 0!==e?e:void 0}getFilterExpression(){return this.filterExpression}set filterExpression(e){this.logger.debug("PubNub",`Set filter expression: ${e}`),this._configuration.setFilterExpression(e)}setFilterExpression(e){this.logger.debug("PubNub",`Set filter expression: ${e}`),this.filterExpression=e}get cipherKey(){return this._configuration.getCipherKey()}set cipherKey(e){this._configuration.setCipherKey(e)}setCipherKey(e){this.logger.debug("PubNub",`Set cipher key: ${e}`),this.cipherKey=e}set heartbeatInterval(e){var t;this.logger.debug("PubNub",`Set heartbeat interval: ${e}`),this._configuration.setHeartbeatInterval(e),this.onHeartbeatIntervalChange&&this.onHeartbeatIntervalChange(null!==(t=this._configuration.getHeartbeatInterval())&&void 0!==t?t:0)}setHeartbeatInterval(e){this.heartbeatInterval=e}get logger(){return this._configuration.logger()}getVersion(){return this._configuration.getVersion()}_addPnsdkSuffix(e,t){this.logger.debug("PubNub",`Add '${e}' 'pnsdk' suffix: ${t}`),this._configuration._addPnsdkSuffix(e,t)}getUUID(){return this.userId}setUUID(e){this.logger.warn("PubNub","'setUserId` is deprecated, please use 'setUserId' or 'userId' setter instead."),this.logger.debug("PubNub",`Set UUID: ${e}`),this.userId=e}get customEncrypt(){return this._configuration.getCustomEncrypt()}get customDecrypt(){return this._configuration.getCustomDecrypt()}channel(e){let t=this.entities[`${e}_ch`];return t||(t=this.entities[`${e}_ch`]=new rs(e,this)),t}channelGroup(e){let t=this.entities[`${e}_chg`];return t||(t=this.entities[`${e}_chg`]=new ss(e,this)),t}channelMetadata(e){let t=this.entities[`${e}_chm`];return t||(t=this.entities[`${e}_chm`]=new ts(e,this)),t}userMetadata(e){let t=this.entities[`${e}_um`];return t||(t=this.entities[`${e}_um`]=new ns(e,this)),t}subscriptionSet(e){var t,s;{const n=[];return null===(t=e.channels)||void 0===t||t.forEach((e=>n.push(this.channel(e)))),null===(s=e.channelGroups)||void 0===s||s.forEach((e=>n.push(this.channelGroup(e)))),new _t({client:this,entities:n,options:e.subscriptionOptions})}}sendRequest(e,t){return i(this,void 0,void 0,(function*(){const s=e.validate();if(s){const e=(n=s,p(Object.assign({message:n},{}),h.PNValidationErrorCategory));if(this.logger.error("PubNub",(()=>({messageType:"error",message:e}))),t)return t(e,null);throw new d("Validation failed, check status for details",e)}var n;const r=e.request(),i=e.operation();r.formData&&r.formData.length>0||i===M.PNDownloadFileOperation?r.timeout=this._configuration.getFileTimeout():i===M.PNSubscribeOperation||i===M.PNReceiveMessagesOperation?r.timeout=this._configuration.getSubscribeTimeout():r.timeout=this._configuration.getTransactionTimeout();const a={error:!1,operation:i,category:h.PNAcknowledgmentCategory,statusCode:0},[o,c]=this.transport.makeSendable(r);return e.cancellationController=c||null,o.then((t=>{if(a.statusCode=t.status,200!==t.status&&204!==t.status){const e=Ms.decoder.decode(t.body),s=t.headers["content-type"];if(s||-1!==s.indexOf("javascript")||-1!==s.indexOf("json")){const t=JSON.parse(e);"object"==typeof t&&"error"in t&&t.error&&"object"==typeof t.error&&(a.errorData=t.error)}else a.responseText=e}return e.parse(t)})).then((e=>t?t(a,e):e)).catch((e=>{const s=e instanceof I?e:I.create(e);if(t)return s.category!==h.PNCancelledCategory&&this.logger.error("PubNub",(()=>({messageType:"error",message:s.toPubNubError(i,"REST API request processing error, check status for details")}))),t(s.toStatus(i),null);const n=s.toPubNubError(i,"REST API request processing error, check status for details");throw s.category!==h.PNCancelledCategory&&this.logger.error("PubNub",(()=>({messageType:"error",message:n}))),n}))}))}destroy(e=!1){this.logger.info("PubNub","Destroying PubNub client."),this._globalSubscriptionSet&&(this._globalSubscriptionSet.invalidate(!0),this._globalSubscriptionSet=void 0),Object.values(this.eventHandleCapable).forEach((e=>e.invalidate(!0))),this.eventHandleCapable={},this.subscriptionManager?(this.subscriptionManager.unsubscribeAll(e),this.subscriptionManager.disconnect()):this.eventEngine&&this.eventEngine.unsubscribeAll(e),this.presenceEventEngine&&this.presenceEventEngine.leaveAll(e)}stop(){this.logger.warn("PubNub","'stop' is deprecated, please use 'destroy' instead."),this.destroy()}publish(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Publish with parameters:"})));const s=!1===e.replicate&&!1===e.storeInHistory,n=new wt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule()})),r=e=>{e&&this.logger.debug("PubNub",`${s?"Fire":"Publish"} success with timetoken: ${e.timetoken}`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}}))}signal(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Signal with parameters:"})));const s=new Ot(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Publish success with timetoken: ${e.timetoken}`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}fire(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fire with parameters:"}))),null!=t||(t=()=>{}),this.publish(Object.assign(Object.assign({},e),{replicate:!1,storeInHistory:!1}),t)}))}get globalSubscriptionSet(){return this._globalSubscriptionSet||(this._globalSubscriptionSet=this.subscriptionSet({})),this._globalSubscriptionSet}get subscriptionTimetoken(){return this.subscriptionManager?this.subscriptionManager.subscriptionTimetoken:this.eventEngine?this.eventEngine.subscriptionTimetoken:void 0}getSubscribedChannels(){return this.subscriptionManager?this.subscriptionManager.subscribedChannels:this.eventEngine?this.eventEngine.getSubscribedChannels():[]}getSubscribedChannelGroups(){return this.subscriptionManager?this.subscriptionManager.subscribedChannelGroups:this.eventEngine?this.eventEngine.getSubscribedChannelGroups():[]}registerEventHandleCapable(e,t,s){{let n;this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign(Object.assign({subscription:e},t?{cursor:t}:[]),s?{subscriptions:s}:{}),details:"Register event handle capable:"}))),this.eventHandleCapable[e.state.id]||(this.eventHandleCapable[e.state.id]=e),s&&0!==s.length?(n=new jt({}),s.forEach((e=>n.add(e.subscriptionInput(!1))))):n=e.subscriptionInput(!1);const r={};r.channels=n.channels,r.channelGroups=n.channelGroups,t&&(r.timetoken=t.timetoken),this.subscriptionManager?this.subscriptionManager.subscribe(r):this.eventEngine&&this.eventEngine.subscribe(r)}}unregisterEventHandleCapable(e,t){{if(!this.eventHandleCapable[e.state.id])return;const s=[];this.logger.trace("PubNub",(()=>({messageType:"object",message:{subscription:e,subscriptions:t},details:"Unregister event handle capable:"})));let n,r=!t||0===t.length;if(!r&&e instanceof _t&&e.subscriptions.length===(null==t?void 0:t.length)&&(r=e.subscriptions.every((e=>t.includes(e)))),r&&delete this.eventHandleCapable[e.state.id],t&&0!==t.length?(n=new jt({}),t.forEach((e=>{const t=e.subscriptionInput(!0);t.isEmpty?s.push(e):n.add(t)}))):(n=e.subscriptionInput(!0),n.isEmpty&&s.push(e)),s.length>0&&this.logger.trace("PubNub",(()=>{const e=[];return s[0]instanceof _t?s[0].subscriptions.forEach((t=>e.push(t.state.entity))):s.forEach((t=>e.push(t.state.entity))),{messageType:"object",message:{entities:e},details:"Can't unregister event handle capable because entities still in use:"}})),n.isEmpty)return;{const e=[],t=[];if(Object.values(this.eventHandleCapable).forEach((s=>{const r=s.subscriptionInput(!1),i=r.channelGroups,a=r.channels;e.push(...n.channelGroups.filter((e=>i.includes(e)))),t.push(...n.channels.filter((e=>a.includes(e))))})),(t.length>0||e.length>0)&&(this.logger.trace("PubNub",(()=>{const s=[],r=n=>{const r=n.subscriptionNames(!0),i=n.subscriptionType===Pt.Channel?t:e;r.some((e=>i.includes(e)))&&s.push(n)};Object.values(this.eventHandleCapable).forEach((e=>{e instanceof _t?e.subscriptions.forEach((e=>{r(e.state.entity)})):e instanceof Mt&&r(e.state.entity)}));let i="Some entities still in use:";return t.length+e.length===n.length&&(i="Can't unregister event handle capable because entities still in use:"),{messageType:"object",message:{entities:s},details:i}})),n.remove(new jt({channels:t,channelGroups:e})),n.isEmpty))return}const i={};i.channels=n.channels,i.channelGroups=n.channelGroups,this.subscriptionManager?this.subscriptionManager.unsubscribe(i):this.eventEngine&&this.eventEngine.unsubscribe(i)}}subscribe(e){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Subscribe with parameters:"})));const t=this.subscriptionSet(Object.assign(Object.assign({},e),{subscriptionOptions:{receivePresenceEvents:e.withPresence}}));this.globalSubscriptionSet.addSubscriptionSet(t),t.dispose();const s="number"==typeof e.timetoken?`${e.timetoken}`:e.timetoken;this.globalSubscriptionSet.subscribe({timetoken:s})}}makeSubscribe(e,t){{const s=new pe(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule(),getFileUrl:this.getFileUrl.bind(this)}));if(this.sendRequest(s,((e,n)=>{var r;this.subscriptionManager&&(null===(r=this.subscriptionManager.abort)||void 0===r?void 0:r.identifier)===s.requestIdentifier&&(this.subscriptionManager.abort=null),t(e,n)})),this.subscriptionManager){const e=()=>s.abort("Cancel long-poll subscribe request");e.identifier=s.requestIdentifier,this.subscriptionManager.abort=e}}}unsubscribe(e){{if(this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Unsubscribe with parameters:"}))),!this._globalSubscriptionSet)return void this.logger.debug("PubNub","There are no active subscriptions. Ignore.");const t=this.globalSubscriptionSet.subscriptions.filter((t=>{var s,n;const r=t.subscriptionInput(!1);if(r.isEmpty)return!1;for(const t of null!==(s=e.channels)&&void 0!==s?s:[])if(r.contains(t))return!0;for(const t of null!==(n=e.channelGroups)&&void 0!==n?n:[])if(r.contains(t))return!0}));t.length>0&&this.globalSubscriptionSet.removeSubscriptions(t)}}makeUnsubscribe(e,t){{let{channels:s,channelGroups:n}=e;if(this._configuration.getKeepPresenceChannelsInPresenceRequests()||(n&&(n=n.filter((e=>!e.endsWith("-pnpres")))),s&&(s=s.filter((e=>!e.endsWith("-pnpres"))))),0===(null!=n?n:[]).length&&0===(null!=s?s:[]).length)return t({error:!1,operation:M.PNUnsubscribeOperation,category:h.PNAcknowledgmentCategory,statusCode:200});this.sendRequest(new Ft({channels:s,channelGroups:n,keySet:this._configuration.keySet}),t)}}unsubscribeAll(){this.logger.debug("PubNub","Unsubscribe all channels and groups"),this._globalSubscriptionSet&&this._globalSubscriptionSet.invalidate(!1),Object.values(this.eventHandleCapable).forEach((e=>e.invalidate(!1))),this.eventHandleCapable={},this.subscriptionManager?this.subscriptionManager.unsubscribeAll():this.eventEngine&&this.eventEngine.unsubscribeAll()}disconnect(e=!1){this.logger.debug("PubNub",`Disconnect (while offline? ${e?"yes":"no"})`),this.subscriptionManager?this.subscriptionManager.disconnect():this.eventEngine&&this.eventEngine.disconnect(e)}reconnect(e){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Reconnect with parameters:"}))),this.subscriptionManager?this.subscriptionManager.reconnect():this.eventEngine&&this.eventEngine.reconnect(null!=e?e:{})}subscribeHandshake(e){return i(this,void 0,void 0,(function*(){{const t=new Ct(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule(),getFileUrl:this.getFileUrl.bind(this)})),s=e.abortSignal.subscribe((e=>{t.abort("Cancel subscribe handshake request")}));return this.sendRequest(t).then((e=>(s(),e.cursor)))}}))}subscribeReceiveMessages(e){return i(this,void 0,void 0,(function*(){{const t=new kt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule(),getFileUrl:this.getFileUrl.bind(this)})),s=e.abortSignal.subscribe((e=>{t.abort("Cancel long-poll subscribe request")}));return this.sendRequest(t).then((e=>(s(),e)))}}))}getMessageActions(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get message actions with parameters:"})));const s=new Ht(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Get message actions success. Received ${e.data.length} message actions.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}addMessageAction(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Add message action with parameters:"})));const s=new Bt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Message action add success. Message action added with timetoken: ${e.data.actionTimetoken}`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}removeMessageAction(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove message action with parameters:"})));const s=new Wt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Message action remove success. Removed message action with ${e.actionTimetoken} timetoken.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}fetchMessages(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fetch messages with parameters:"})));const s=new Lt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule(),getFileUrl:this.getFileUrl.bind(this)})),n=e=>{if(!e)return;const t=Object.values(e.channels).reduce(((e,t)=>e+t.length),0);this.logger.debug("PubNub",`Fetch messages success. Received ${t} messages.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}deleteMessages(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Delete messages with parameters:"})));const s=new xt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub","Delete messages success.")};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}messageCounts(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get messages count with parameters:"})));const s=new qt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=t=>{if(!t)return;const s=Object.values(t.channels).reduce(((e,t)=>e+t),0);this.logger.debug("PubNub",`Get messages count success. There are ${s} messages since provided reference timetoken${e.channelTimetokens?e.channelTimetokens.join(","):""}.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}history(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fetch history with parameters:"})));const s=new Gt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule()})),n=e=>{e&&this.logger.debug("PubNub",`Fetch history success. Received ${e.messages.length} messages.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}hereNow(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Here now with parameters:"})));const s=new Dt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Here now success. There are ${e.totalOccupancy} participants in ${e.totalChannels} channels.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}whereNow(e,t){return i(this,void 0,void 0,(function*(){var s;{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Where now with parameters:"})));const n=new $t({uuid:null!==(s=e.uuid)&&void 0!==s?s:this._configuration.userId,keySet:this._configuration.keySet}),r=e=>{e&&this.logger.debug("PubNub",`Where now success. Currently present in ${e.channels.length} channels.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}}))}getState(e,t){return i(this,void 0,void 0,(function*(){var s;{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get presence state with parameters:"})));const n=new At(Object.assign(Object.assign({},e),{uuid:null!==(s=e.uuid)&&void 0!==s?s:this._configuration.userId,keySet:this._configuration.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Get presence state success. Received presence state for ${Object.keys(e.channels).length} channels.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}}))}setState(e,t){return i(this,void 0,void 0,(function*(){var s,n;{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set presence state with parameters:"})));const{keySet:r,userId:i}=this._configuration,a=this._configuration.getPresenceTimeout();let o;if(this._configuration.enableEventEngine&&this.presenceState){const t=this.presenceState;null===(s=e.channels)||void 0===s||s.forEach((s=>t[s]=e.state)),"channelGroups"in e&&(null===(n=e.channelGroups)||void 0===n||n.forEach((s=>t[s]=e.state)))}o="withHeartbeat"in e&&e.withHeartbeat?new Rt(Object.assign(Object.assign({},e),{keySet:r,heartbeat:a})):new Ut(Object.assign(Object.assign({},e),{keySet:r,uuid:i}));const c=e=>{e&&this.logger.debug("PubNub","Set presence state success."+(o instanceof Rt?" Presence state has been set using heartbeat endpoint.":""))};return this.subscriptionManager&&this.subscriptionManager.setState(e),t?this.sendRequest(o,((e,s)=>{c(s),t(e,s)})):this.sendRequest(o).then((e=>(c(e),e)))}}))}presence(e){var t;this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Change presence with parameters:"}))),null===(t=this.subscriptionManager)||void 0===t||t.changePresence(e)}heartbeat(e,t){return i(this,void 0,void 0,(function*(){var s;{this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Heartbeat with parameters:"})));let{channels:n,channelGroups:r}=e;if(this._configuration.getKeepPresenceChannelsInPresenceRequests()||(r&&(r=r.filter((e=>!e.endsWith("-pnpres")))),n&&(n=n.filter((e=>!e.endsWith("-pnpres"))))),0===(null!=r?r:[]).length&&0===(null!=n?n:[]).length){const e={error:!1,operation:M.PNHeartbeatOperation,category:h.PNAcknowledgmentCategory,statusCode:200};return this.logger.trace("PubNub","There are no active subscriptions. Ignore."),t?t(e,{}):Promise.resolve(e)}const i=new Rt(Object.assign(Object.assign({},e),{channels:n,channelGroups:r,keySet:this._configuration.keySet})),a=e=>{e&&this.logger.trace("PubNub","Heartbeat success.")},o=null===(s=e.abortSignal)||void 0===s?void 0:s.subscribe((e=>{i.abort("Cancel long-poll subscribe request")}));return t?this.sendRequest(i,((e,s)=>{a(s),o&&o(),t(e,s)})):this.sendRequest(i).then((e=>(a(e),o&&o(),e)))}}))}join(e){var t,s;this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Join with parameters:"}))),e&&0===(null!==(t=e.channels)&&void 0!==t?t:[]).length&&0===(null!==(s=e.groups)&&void 0!==s?s:[]).length?this.logger.trace("PubNub","Ignoring 'join' announcement request."):this.presenceEventEngine?this.presenceEventEngine.join(e):this.heartbeat(Object.assign(Object.assign({channels:e.channels,channelGroups:e.groups},this._configuration.maintainPresenceState&&this.presenceState&&Object.keys(this.presenceState).length>0&&{state:this.presenceState}),{heartbeat:this._configuration.getPresenceTimeout()}),(()=>{}))}presenceReconnect(e){this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Presence reconnect with parameters:"}))),this.presenceEventEngine?this.presenceEventEngine.reconnect():this.heartbeat(Object.assign(Object.assign({channels:e.channels,channelGroups:e.groups},this._configuration.maintainPresenceState&&{state:this.presenceState}),{heartbeat:this._configuration.getPresenceTimeout()}),(()=>{}))}leave(e){var t,s,n;this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave with parameters:"}))),e&&0===(null!==(t=e.channels)&&void 0!==t?t:[]).length&&0===(null!==(s=e.groups)&&void 0!==s?s:[]).length?this.logger.trace("PubNub","Ignoring 'leave' announcement request."):this.presenceEventEngine?null===(n=this.presenceEventEngine)||void 0===n||n.leave(e):this.makeUnsubscribe({channels:e.channels,channelGroups:e.groups},(()=>{}))}leaveAll(e={}){this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave all with parameters:"}))),this.presenceEventEngine?this.presenceEventEngine.leaveAll(!!e.isOffline):e.isOffline||this.makeUnsubscribe({channels:e.channels,channelGroups:e.groups},(()=>{}))}presenceDisconnect(e){this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Presence disconnect parameters:"}))),this.presenceEventEngine?this.presenceEventEngine.disconnect(!!e.isOffline):e.isOffline||this.makeUnsubscribe({channels:e.channels,channelGroups:e.groups},(()=>{}))}grantToken(e,t){return i(this,void 0,void 0,(function*(){throw new Error("Grant Token error: PAM module disabled")}))}revokeToken(e,t){return i(this,void 0,void 0,(function*(){throw new Error("Revoke Token error: PAM module disabled")}))}get token(){return this.tokenManager&&this.tokenManager.getToken()}getToken(){return this.token}set token(e){this.tokenManager&&this.tokenManager.setToken(e),this.onAuthenticationChange&&this.onAuthenticationChange(e)}setToken(e){this.token=e}parseToken(e){return this.tokenManager&&this.tokenManager.parseToken(e)}grant(e,t){return i(this,void 0,void 0,(function*(){throw new Error("Grant error: PAM module disabled")}))}audit(e,t){return i(this,void 0,void 0,(function*(){throw new Error("Grant Permissions error: PAM module disabled")}))}get objects(){return this._objects}fetchUsers(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'fetchUsers' is deprecated. Use 'pubnub.objects.getAllUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{},details:"Fetch all User objects with parameters:"}))),this.objects._getAllUUIDMetadata(e,t)}))}fetchUser(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'fetchUser' is deprecated. Use 'pubnub.objects.getUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{uuid:this.userId},details:`Fetch${e&&"function"!=typeof e?"":" current"} User object with parameters:`}))),this.objects._getUUIDMetadata(e,t)}))}createUser(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'createUser' is deprecated. Use 'pubnub.objects.setUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Create User object with parameters:"}))),this.objects._setUUIDMetadata(e,t)}))}updateUser(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'updateUser' is deprecated. Use 'pubnub.objects.setUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Update User object with parameters:"}))),this.objects._setUUIDMetadata(e,t)}))}removeUser(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'removeUser' is deprecated. Use 'pubnub.objects.removeUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{uuid:this.userId},details:`Remove${e&&"function"!=typeof e?"":" current"} User object with parameters:`}))),this.objects._removeUUIDMetadata(e,t)}))}fetchSpaces(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'fetchSpaces' is deprecated. Use 'pubnub.objects.getAllChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{},details:"Fetch all Space objects with parameters:"}))),this.objects._getAllChannelMetadata(e,t)}))}fetchSpace(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'fetchSpace' is deprecated. Use 'pubnub.objects.getChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fetch Space object with parameters:"}))),this.objects._getChannelMetadata(e,t)}))}createSpace(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'createSpace' is deprecated. Use 'pubnub.objects.setChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Create Space object with parameters:"}))),this.objects._setChannelMetadata(e,t)}))}updateSpace(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'updateSpace' is deprecated. Use 'pubnub.objects.setChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Update Space object with parameters:"}))),this.objects._setChannelMetadata(e,t)}))}removeSpace(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'removeSpace' is deprecated. Use 'pubnub.objects.removeChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove Space object with parameters:"}))),this.objects._removeChannelMetadata(e,t)}))}fetchMemberships(e,t){return i(this,void 0,void 0,(function*(){return this.objects.fetchMemberships(e,t)}))}addMemberships(e,t){return i(this,void 0,void 0,(function*(){return this.objects.addMemberships(e,t)}))}updateMemberships(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'addMemberships' is deprecated. Use 'pubnub.objects.setChannelMembers' or 'pubnub.objects.setMemberships' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Update memberships with parameters:"}))),this.objects.addMemberships(e,t)}))}removeMemberships(e,t){return i(this,void 0,void 0,(function*(){var s,n,r;{if(this.logger.warn("PubNub","'removeMemberships' is deprecated. Use 'pubnub.objects.removeMemberships' or 'pubnub.objects.removeChannelMembers' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove memberships with parameters:"}))),"spaceId"in e){const r=e,i={channel:null!==(s=r.spaceId)&&void 0!==s?s:r.channel,uuids:null!==(n=r.userIds)&&void 0!==n?n:r.uuids,limit:0};return t?this.objects.removeChannelMembers(i,t):this.objects.removeChannelMembers(i)}const i=e,a={uuid:i.userId,channels:null!==(r=i.spaceIds)&&void 0!==r?r:i.channels,limit:0};return t?this.objects.removeMemberships(a,t):this.objects.removeMemberships(a)}}))}get channelGroups(){return this._channelGroups}get push(){return this._push}sendFile(e,t){return i(this,void 0,void 0,(function*(){{if(!this._configuration.PubNubFile)throw new Error("Validation failed: 'PubNubFile' not configured or file upload not supported by the platform.");this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Send file with parameters:"})));const s=new Zt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,PubNubFile:this._configuration.PubNubFile,fileUploadPublishRetryLimit:this._configuration.fileUploadPublishRetryLimit,file:e.file,sendRequest:this.sendRequest.bind(this),publishFile:this.publishFile.bind(this),crypto:this._configuration.getCryptoModule(),cryptography:this.cryptography?this.cryptography:void 0})),n={error:!1,operation:M.PNPublishFileOperation,category:h.PNAcknowledgmentCategory,statusCode:0},r=e=>{e&&this.logger.debug("PubNub",`Send file success. File shared with ${e.id} ID.`)};return s.process().then((e=>(n.statusCode=e.status,r(e),t?t(n,e):e))).catch((e=>{let s;throw e instanceof d?s=e.status:e instanceof I&&(s=e.toStatus(n.operation)),this.logger.error("PubNub",(()=>({messageType:"error",message:new d("File sending error. Check status for details",s)}))),t&&s&&t(s,null),new d("REST API request processing error. Check status for details",s)}))}}))}publishFile(e,t){return i(this,void 0,void 0,(function*(){{if(!this._configuration.PubNubFile)throw new Error("Validation failed: 'PubNubFile' not configured or file upload not supported by the platform.");this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Publish file message with parameters:"})));const s=new zt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule()})),n=e=>{e&&this.logger.debug("PubNub",`Publish file message success. File message published with timetoken: ${e.timetoken}`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}listFiles(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"List files with parameters:"})));const s=new Xt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`List files success. There are ${e.count} uploaded files.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}getFileUrl(e){var t;{const s=this.transport.request(new Vt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})).request()),n=null!==(t=s.queryParameters)&&void 0!==t?t:{},r=Object.keys(n).map((e=>{const t=n[e];return Array.isArray(t)?t.map((t=>`${e}=${$(t)}`)).join("&"):`${e}=${$(t)}`})).join("&");return`${s.origin}${s.path}?${r}`}}downloadFile(e,t){return i(this,void 0,void 0,(function*(){{if(!this._configuration.PubNubFile)throw new Error("Validation failed: 'PubNubFile' not configured or file upload not supported by the platform.");this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Download file with parameters:"})));const s=new Is(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,PubNubFile:this._configuration.PubNubFile,cryptography:this.cryptography?this.cryptography:void 0,crypto:this._configuration.getCryptoModule()})),n=e=>{e&&this.logger.debug("PubNub","Download file success.")};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):yield this.sendRequest(s).then((e=>(n(e),e)))}}))}deleteFile(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Delete file with parameters:"})));const s=new Jt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Delete file success. Deleted file with ${e.id} ID.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}time(e){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub","Get service time.");const t=new _s,s=e=>{e&&this.logger.debug("PubNub",`Get service time success. Current timetoken: ${e.timetoken}`)};return e?this.sendRequest(t,((t,n)=>{s(n),e(t,n)})):this.sendRequest(t).then((e=>(s(e),e)))}))}emitStatus(e){var t;null===(t=this.eventDispatcher)||void 0===t||t.handleStatus(e)}emitEvent(e,t){var s;this._globalSubscriptionSet&&this._globalSubscriptionSet.handleEvent(e,t),null===(s=this.eventDispatcher)||void 0===s||s.handleEvent(t),Object.values(this.eventHandleCapable).forEach((s=>{s!==this._globalSubscriptionSet&&s.handleEvent(e,t)}))}set onStatus(e){this.eventDispatcher&&(this.eventDispatcher.onStatus=e)}set onMessage(e){this.eventDispatcher&&(this.eventDispatcher.onMessage=e)}set onPresence(e){this.eventDispatcher&&(this.eventDispatcher.onPresence=e)}set onSignal(e){this.eventDispatcher&&(this.eventDispatcher.onSignal=e)}set onObjects(e){this.eventDispatcher&&(this.eventDispatcher.onObjects=e)}set onMessageAction(e){this.eventDispatcher&&(this.eventDispatcher.onMessageAction=e)}set onFile(e){this.eventDispatcher&&(this.eventDispatcher.onFile=e)}addListener(e){this.eventDispatcher&&this.eventDispatcher.addListener(e)}removeListener(e){this.eventDispatcher&&this.eventDispatcher.removeListener(e)}removeAllListeners(){this.eventDispatcher&&this.eventDispatcher.removeAllListeners()}encrypt(e,t){this.logger.warn("PubNub","'encrypt' is deprecated. Use cryptoModule instead.");const s=this._configuration.getCryptoModule();if(!t&&s&&"string"==typeof e){const t=s.encrypt(e);return"string"==typeof t?t:u(t)}if(!this.crypto)throw new Error("Encryption error: cypher key not set");return this.crypto.encrypt(e,t)}decrypt(e,t){this.logger.warn("PubNub","'decrypt' is deprecated. Use cryptoModule instead.");const s=this._configuration.getCryptoModule();if(!t&&s){const t=s.decrypt(e);return t instanceof ArrayBuffer?JSON.parse((new TextDecoder).decode(t)):t}if(!this.crypto)throw new Error("Decryption error: cypher key not set");return this.crypto.decrypt(e,t)}encryptFile(e,t){return i(this,void 0,void 0,(function*(){var s;if("string"!=typeof e&&(t=e),!t)throw new Error("File encryption error. Source file is missing.");if(!this._configuration.PubNubFile)throw new Error("File encryption error. File constructor not configured.");if("string"!=typeof e&&!this._configuration.getCryptoModule())throw new Error("File encryption error. Crypto module not configured.");if("string"==typeof e){if(!this.cryptography)throw new Error("File encryption error. File encryption not available");return this.cryptography.encryptFile(e,t,this._configuration.PubNubFile)}return null===(s=this._configuration.getCryptoModule())||void 0===s?void 0:s.encryptFile(t,this._configuration.PubNubFile)}))}decryptFile(e,t){return i(this,void 0,void 0,(function*(){var s;if("string"!=typeof e&&(t=e),!t)throw new Error("File encryption error. Source file is missing.");if(!this._configuration.PubNubFile)throw new Error("File decryption error. File constructor not configured.");if("string"==typeof e&&!this._configuration.getCryptoModule())throw new Error("File decryption error. Crypto module not configured.");if("string"==typeof e){if(!this.cryptography)throw new Error("File decryption error. File decryption not available");return this.cryptography.decryptFile(e,t,this._configuration.PubNubFile)}return null===(s=this._configuration.getCryptoModule())||void 0===s?void 0:s.decryptFile(t,this._configuration.PubNubFile)}))}}Ms.decoder=new TextDecoder,Ms.OPERATIONS=M,Ms.CATEGORIES=h,Ms.Endpoint=z,Ms.ExponentialRetryPolicy=V.ExponentialRetryPolicy,Ms.LinearRetryPolicy=V.LinearRetryPolicy,Ms.NoneRetryPolicy=V.None,Ms.LogLevel=F;class As{constructor(e,t){this.decode=e,this.base64ToBinary=t}decodeToken(e){let t="";e.length%4==3?t="=":e.length%4==2&&(t="==");const s=e.replace(/-/gi,"+").replace(/_/gi,"/")+t,n=this.decode(this.base64ToBinary(s));return"object"==typeof n?n:void 0}}class Us extends Ms{constructor(e){var t;const s=void 0!==e.subscriptionWorkerUrl,r=R(e),i=Object.assign(Object.assign({},r),{sdkFamily:"Web"});i.PubNubFile=o;const a=se(i,(e=>{if(e.cipherKey){return new N({default:new E(Object.assign(Object.assign({},e),e.logger?{}:{logger:a.logger()})),cryptors:[new k({cipherKey:e.cipherKey})]})}}));let u,l;a.getCryptoModule()&&(a.getCryptoModule().logger=a.logger()),u=new ie(new As((e=>U(n.decode(e))),c)),(a.getCipherKey()||a.secretKey)&&(l=new P({secretKey:a.secretKey,cipherKey:a.getCipherKey(),useRandomIVs:a.getUseRandomIVs(),customEncrypt:a.getCustomEncrypt(),customDecrypt:a.getCustomDecrypt(),logger:a.logger()}));let h,d=()=>{},p=()=>{},g=()=>{};h=new j;let b=new ue(a.logger(),i.transport);if(r.subscriptionWorkerUrl)try{const e=new A({clientIdentifier:a._instanceId,subscriptionKey:a.subscribeKey,userId:a.getUserId(),workerUrl:r.subscriptionWorkerUrl,sdkVersion:a.getVersion(),heartbeatInterval:a.getHeartbeatInterval(),announceSuccessfulHeartbeats:a.announceSuccessfulHeartbeats,announceFailedHeartbeats:a.announceFailedHeartbeats,workerOfflineClientsCheckInterval:i.subscriptionWorkerOfflineClientsCheckInterval,workerUnsubscribeOfflineClients:i.subscriptionWorkerUnsubscribeOfflineClients,workerLogVerbosity:i.subscriptionWorkerLogVerbosity,tokenManager:u,transport:b,logger:a.logger()});d=t=>e.onHeartbeatIntervalChange(t),p=t=>e.onTokenChange(t),g=t=>e.onUserIdChange(t),b=e,r.subscriptionWorkerUnsubscribeOfflineClients&&window.addEventListener("pagehide",(t=>{t.persisted||e.terminate()}),{once:!0})}catch(e){a.logger().error("PubNub",(()=>({messageType:"error",message:e})))}else s&&a.logger().warn("PubNub","SharedWorker not supported in this browser. Fallback to the original transport.");const y=new ce({clientConfiguration:a,tokenManager:u,transport:b});super({configuration:a,transport:y,cryptography:h,tokenManager:u,crypto:l}),this.onHeartbeatIntervalChange=d,this.onAuthenticationChange=p,this.onUserIdChange=g,b instanceof A&&(b.emitStatus=this.emitStatus.bind(this)),(null===(t=e.listenToBrowserNetworkEvents)||void 0===t||t)&&(window.addEventListener("offline",(()=>{this.networkDownDetected()})),window.addEventListener("online",(()=>{this.networkUpDetected()})))}networkDownDetected(){this.logger.debug("PubNub","Network down detected"),this.emitStatus({category:Us.CATEGORIES.PNNetworkDownCategory}),this._configuration.restore?this.disconnect(!0):this.destroy(!0)}networkUpDetected(){this.logger.debug("PubNub","Network up detected"),this.emitStatus({category:Us.CATEGORIES.PNNetworkUpCategory}),this.reconnect()}}return Us.CryptoModule=N,Us})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).PubNub=t()}(this,(function(){"use strict";var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s={exports:{}};!function(t){!function(e,s){var n=Math.pow(2,-24),r=Math.pow(2,32),i=Math.pow(2,53);var a={encode:function(e){var t,n=new ArrayBuffer(256),a=new DataView(n),o=0;function c(e){for(var s=n.byteLength,r=o+e;s>2,u=0;u>6),r.push(128|63&a)):a<55296?(r.push(224|a>>12),r.push(128|a>>6&63),r.push(128|63&a)):(a=(1023&a)<<10,a|=1023&t.charCodeAt(++n),a+=65536,r.push(240|a>>18),r.push(128|a>>12&63),r.push(128|a>>6&63),r.push(128|63&a))}return d(3,r.length),h(r);default:var p;if(Array.isArray(t))for(d(4,p=t.length),n=0;n>5!==e)throw"Invalid indefinite length element";return s}function y(e,t){for(var s=0;s>10),e.push(56320|1023&n))}}"function"!=typeof t&&(t=function(e){return e}),"function"!=typeof i&&(i=function(){return s});var m=function e(){var r,d,m=l(),f=m>>5,v=31&m;if(7===f)switch(v){case 25:return function(){var e=new ArrayBuffer(4),t=new DataView(e),s=h(),r=32768&s,i=31744&s,a=1023&s;if(31744===i)i=261120;else if(0!==i)i+=114688;else if(0!==a)return a*n;return t.setUint32(0,r<<16|i<<13|a<<13),t.getFloat32(0)}();case 26:return c(a.getFloat32(o),4);case 27:return c(a.getFloat64(o),8)}if((d=g(v))<0&&(f<2||6=0;)w+=d,S.push(u(d));var O=new Uint8Array(w),k=0;for(r=0;r=0;)y(C,d);else y(C,d);return String.fromCharCode.apply(null,C);case 4:var P;if(d<0)for(P=[];!p();)P.push(e());else for(P=new Array(d),r=0;re.toString())).join(", ")}]}`}}a.encoder=new TextEncoder,a.decoder=new TextDecoder;class o{static create(e){return new o(e)}constructor(e){let t,s,n,r;if(e instanceof File)r=e,n=e.name,s=e.type,t=e.size;else if("data"in e){const i=e.data;s=e.mimeType,n=e.name,r=new File([i],n,{type:s}),t=r.size}if(void 0===r)throw new Error("Couldn't construct a file out of supplied options.");if(void 0===n)throw new Error("Couldn't guess filename out of the options. Please provide one.");t&&(this.contentLength=t),this.mimeType=s,this.data=r,this.name=n}toBuffer(){return i(this,void 0,void 0,(function*(){throw new Error("This feature is only supported in Node.js environments.")}))}toArrayBuffer(){return i(this,void 0,void 0,(function*(){return new Promise(((e,t)=>{const s=new FileReader;s.addEventListener("load",(()=>{if(s.result instanceof ArrayBuffer)return e(s.result)})),s.addEventListener("error",(()=>t(s.error))),s.readAsArrayBuffer(this.data)}))}))}toString(){return i(this,void 0,void 0,(function*(){return new Promise(((e,t)=>{const s=new FileReader;s.addEventListener("load",(()=>{if("string"==typeof s.result)return e(s.result)})),s.addEventListener("error",(()=>{t(s.error)})),s.readAsBinaryString(this.data)}))}))}toStream(){return i(this,void 0,void 0,(function*(){throw new Error("This feature is only supported in Node.js environments.")}))}toFile(){return i(this,void 0,void 0,(function*(){return this.data}))}toFileUri(){return i(this,void 0,void 0,(function*(){throw new Error("This feature is only supported in React Native environments.")}))}toBlob(){return i(this,void 0,void 0,(function*(){return this.data}))}}o.supportsBlob="undefined"!=typeof Blob,o.supportsFile="undefined"!=typeof File,o.supportsBuffer=!1,o.supportsStream=!1,o.supportsString=!0,o.supportsArrayBuffer=!0,o.supportsEncryptFile=!0,o.supportsFileUri=!1;function c(e){const t=e.replace(/==?$/,""),s=Math.floor(t.length/4*3),n=new ArrayBuffer(s),r=new Uint8Array(n);let i=0;function a(){const e=t.charAt(i++),s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(e);if(-1===s)throw new Error(`Illegal character at ${i}: ${t.charAt(i-1)}`);return s}for(let e=0;e>4,c=(15&s)<<4|n>>2,u=(3&n)<<6|i;r[e]=o,64!=n&&(r[e+1]=c),64!=i&&(r[e+2]=u)}return n}function u(e){let t="";const s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(e),r=n.byteLength,i=r%3,a=r-i;let o,c,u,l,h;for(let e=0;e>18,c=(258048&h)>>12,u=(4032&h)>>6,l=63&h,t+=s[o]+s[c]+s[u]+s[l];return 1==i?(h=n[a],o=(252&h)>>2,c=(3&h)<<4,t+=s[o]+s[c]+"=="):2==i&&(h=n[a]<<8|n[a+1],o=(64512&h)>>10,c=(1008&h)>>4,u=(15&h)<<2,t+=s[o]+s[c]+s[u]+"="),t}var l;!function(e){e.PNNetworkIssuesCategory="PNNetworkIssuesCategory",e.PNTimeoutCategory="PNTimeoutCategory",e.PNCancelledCategory="PNCancelledCategory",e.PNBadRequestCategory="PNBadRequestCategory",e.PNAccessDeniedCategory="PNAccessDeniedCategory",e.PNValidationErrorCategory="PNValidationErrorCategory",e.PNAcknowledgmentCategory="PNAcknowledgmentCategory",e.PNMalformedResponseCategory="PNMalformedResponseCategory",e.PNServerErrorCategory="PNServerErrorCategory",e.PNUnknownCategory="PNUnknownCategory",e.PNNetworkUpCategory="PNNetworkUpCategory",e.PNNetworkDownCategory="PNNetworkDownCategory",e.PNReconnectedCategory="PNReconnectedCategory",e.PNConnectedCategory="PNConnectedCategory",e.PNSubscriptionChangedCategory="PNSubscriptionChangedCategory",e.PNRequestMessageCountExceededCategory="PNRequestMessageCountExceededCategory",e.PNDisconnectedCategory="PNDisconnectedCategory",e.PNConnectionErrorCategory="PNConnectionErrorCategory",e.PNDisconnectedUnexpectedlyCategory="PNDisconnectedUnexpectedlyCategory",e.PNSharedWorkerUpdatedCategory="PNSharedWorkerUpdatedCategory"}(l||(l={}));var h=l;class d extends Error{constructor(e,t){super(e),this.status=t,this.name="PubNubError",this.message=e,Object.setPrototypeOf(this,new.target.prototype)}}function p(e,t){var s;return null!==(s=e.statusCode)&&void 0!==s||(e.statusCode=0),Object.assign(Object.assign({},e),{statusCode:e.statusCode,category:t,error:!0})}function g(e,t){return p(Object.assign(Object.assign({message:"Unable to deserialize service response"},void 0!==e?{responseText:e}:{}),void 0!==t?{statusCode:t}:{}),h.PNMalformedResponseCategory)}var b,y,m,f,v,S=S||function(e){var t={},s=t.lib={},n=function(){},r=s.Base={extend:function(e){n.prototype=this;var t=new n;return e&&t.mixIn(e),t.hasOwnProperty("init")||(t.init=function(){t.$super.init.apply(this,arguments)}),t.init.prototype=t,t.$super=this,t},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}},i=s.WordArray=r.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||o).stringify(this)},concat:function(e){var t=this.words,s=e.words,n=this.sigBytes;if(e=e.sigBytes,this.clamp(),n%4)for(var r=0;r>>2]|=(s[r>>>2]>>>24-r%4*8&255)<<24-(n+r)%4*8;else if(65535>>2]=s[r>>>2];else t.push.apply(t,s);return this.sigBytes+=e,this},clamp:function(){var t=this.words,s=this.sigBytes;t[s>>>2]&=4294967295<<32-s%4*8,t.length=e.ceil(s/4)},clone:function(){var e=r.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var s=[],n=0;n>>2]>>>24-n%4*8&255;s.push((r>>>4).toString(16)),s.push((15&r).toString(16))}return s.join("")},parse:function(e){for(var t=e.length,s=[],n=0;n>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new i.init(s,t/2)}},c=a.Latin1={stringify:function(e){var t=e.words;e=e.sigBytes;for(var s=[],n=0;n>>2]>>>24-n%4*8&255));return s.join("")},parse:function(e){for(var t=e.length,s=[],n=0;n>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new i.init(s,t)}},u=a.Utf8={stringify:function(e){try{return decodeURIComponent(escape(c.stringify(e)))}catch(e){throw Error("Malformed UTF-8 data")}},parse:function(e){return c.parse(unescape(encodeURIComponent(e)))}},l=s.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new i.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=u.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var s=this._data,n=s.words,r=s.sigBytes,a=this.blockSize,o=r/(4*a);if(t=(o=t?e.ceil(o):e.max((0|o)-this._minBufferSize,0))*a,r=e.min(4*t,r),t){for(var c=0;cu;){var l;e:{l=c;for(var h=e.sqrt(l),d=2;d<=h;d++)if(!(l%d)){l=!1;break e}l=!0}l&&(8>u&&(i[u]=o(e.pow(c,.5))),a[u]=o(e.pow(c,1/3)),u++),c++}var p=[];r=r.SHA256=n.extend({_doReset:function(){this._hash=new s.init(i.slice(0))},_doProcessBlock:function(e,t){for(var s=this._hash.words,n=s[0],r=s[1],i=s[2],o=s[3],c=s[4],u=s[5],l=s[6],h=s[7],d=0;64>d;d++){if(16>d)p[d]=0|e[t+d];else{var g=p[d-15],b=p[d-2];p[d]=((g<<25|g>>>7)^(g<<14|g>>>18)^g>>>3)+p[d-7]+((b<<15|b>>>17)^(b<<13|b>>>19)^b>>>10)+p[d-16]}g=h+((c<<26|c>>>6)^(c<<21|c>>>11)^(c<<7|c>>>25))+(c&u^~c&l)+a[d]+p[d],b=((n<<30|n>>>2)^(n<<19|n>>>13)^(n<<10|n>>>22))+(n&r^n&i^r&i),h=l,l=u,u=c,c=o+g|0,o=i,i=r,r=n,n=g+b|0}s[0]=s[0]+n|0,s[1]=s[1]+r|0,s[2]=s[2]+i|0,s[3]=s[3]+o|0,s[4]=s[4]+c|0,s[5]=s[5]+u|0,s[6]=s[6]+l|0,s[7]=s[7]+h|0},_doFinalize:function(){var t=this._data,s=t.words,n=8*this._nDataBytes,r=8*t.sigBytes;return s[r>>>5]|=128<<24-r%32,s[14+(r+64>>>9<<4)]=e.floor(n/4294967296),s[15+(r+64>>>9<<4)]=n,t.sigBytes=4*s.length,this._process(),this._hash},clone:function(){var e=n.clone.call(this);return e._hash=this._hash.clone(),e}});t.SHA256=n._createHelper(r),t.HmacSHA256=n._createHmacHelper(r)}(Math),y=(b=S).enc.Utf8,b.algo.HMAC=b.lib.Base.extend({init:function(e,t){e=this._hasher=new e.init,"string"==typeof t&&(t=y.parse(t));var s=e.blockSize,n=4*s;t.sigBytes>n&&(t=e.finalize(t)),t.clamp();for(var r=this._oKey=t.clone(),i=this._iKey=t.clone(),a=r.words,o=i.words,c=0;c>>2]>>>24-r%4*8&255)<<16|(t[r+1>>>2]>>>24-(r+1)%4*8&255)<<8|t[r+2>>>2]>>>24-(r+2)%4*8&255,a=0;4>a&&r+.75*a>>6*(3-a)&63));if(t=n.charAt(64))for(;e.length%4;)e.push(t);return e.join("")},parse:function(e){var t=e.length,s=this._map;(n=s.charAt(64))&&-1!=(n=e.indexOf(n))&&(t=n);for(var n=[],r=0,i=0;i>>6-i%4*2;n[r>>>2]|=(a|o)<<24-r%4*8,r++}return f.create(n,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="},function(e){function t(e,t,s,n,r,i,a){return((e=e+(t&s|~t&n)+r+a)<>>32-i)+t}function s(e,t,s,n,r,i,a){return((e=e+(t&n|s&~n)+r+a)<>>32-i)+t}function n(e,t,s,n,r,i,a){return((e=e+(t^s^n)+r+a)<>>32-i)+t}function r(e,t,s,n,r,i,a){return((e=e+(s^(t|~n))+r+a)<>>32-i)+t}for(var i=S,a=(c=i.lib).WordArray,o=c.Hasher,c=i.algo,u=[],l=0;64>l;l++)u[l]=4294967296*e.abs(e.sin(l+1))|0;c=c.MD5=o.extend({_doReset:function(){this._hash=new a.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(e,i){for(var a=0;16>a;a++){var o=e[c=i+a];e[c]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8)}a=this._hash.words;var c=e[i+0],l=(o=e[i+1],e[i+2]),h=e[i+3],d=e[i+4],p=e[i+5],g=e[i+6],b=e[i+7],y=e[i+8],m=e[i+9],f=e[i+10],v=e[i+11],S=e[i+12],w=e[i+13],O=e[i+14],k=e[i+15],C=t(C=a[0],E=a[1],j=a[2],P=a[3],c,7,u[0]),P=t(P,C,E,j,o,12,u[1]),j=t(j,P,C,E,l,17,u[2]),E=t(E,j,P,C,h,22,u[3]);C=t(C,E,j,P,d,7,u[4]),P=t(P,C,E,j,p,12,u[5]),j=t(j,P,C,E,g,17,u[6]),E=t(E,j,P,C,b,22,u[7]),C=t(C,E,j,P,y,7,u[8]),P=t(P,C,E,j,m,12,u[9]),j=t(j,P,C,E,f,17,u[10]),E=t(E,j,P,C,v,22,u[11]),C=t(C,E,j,P,S,7,u[12]),P=t(P,C,E,j,w,12,u[13]),j=t(j,P,C,E,O,17,u[14]),C=s(C,E=t(E,j,P,C,k,22,u[15]),j,P,o,5,u[16]),P=s(P,C,E,j,g,9,u[17]),j=s(j,P,C,E,v,14,u[18]),E=s(E,j,P,C,c,20,u[19]),C=s(C,E,j,P,p,5,u[20]),P=s(P,C,E,j,f,9,u[21]),j=s(j,P,C,E,k,14,u[22]),E=s(E,j,P,C,d,20,u[23]),C=s(C,E,j,P,m,5,u[24]),P=s(P,C,E,j,O,9,u[25]),j=s(j,P,C,E,h,14,u[26]),E=s(E,j,P,C,y,20,u[27]),C=s(C,E,j,P,w,5,u[28]),P=s(P,C,E,j,l,9,u[29]),j=s(j,P,C,E,b,14,u[30]),C=n(C,E=s(E,j,P,C,S,20,u[31]),j,P,p,4,u[32]),P=n(P,C,E,j,y,11,u[33]),j=n(j,P,C,E,v,16,u[34]),E=n(E,j,P,C,O,23,u[35]),C=n(C,E,j,P,o,4,u[36]),P=n(P,C,E,j,d,11,u[37]),j=n(j,P,C,E,b,16,u[38]),E=n(E,j,P,C,f,23,u[39]),C=n(C,E,j,P,w,4,u[40]),P=n(P,C,E,j,c,11,u[41]),j=n(j,P,C,E,h,16,u[42]),E=n(E,j,P,C,g,23,u[43]),C=n(C,E,j,P,m,4,u[44]),P=n(P,C,E,j,S,11,u[45]),j=n(j,P,C,E,k,16,u[46]),C=r(C,E=n(E,j,P,C,l,23,u[47]),j,P,c,6,u[48]),P=r(P,C,E,j,b,10,u[49]),j=r(j,P,C,E,O,15,u[50]),E=r(E,j,P,C,p,21,u[51]),C=r(C,E,j,P,S,6,u[52]),P=r(P,C,E,j,h,10,u[53]),j=r(j,P,C,E,f,15,u[54]),E=r(E,j,P,C,o,21,u[55]),C=r(C,E,j,P,y,6,u[56]),P=r(P,C,E,j,k,10,u[57]),j=r(j,P,C,E,g,15,u[58]),E=r(E,j,P,C,w,21,u[59]),C=r(C,E,j,P,d,6,u[60]),P=r(P,C,E,j,v,10,u[61]),j=r(j,P,C,E,l,15,u[62]),E=r(E,j,P,C,m,21,u[63]);a[0]=a[0]+C|0,a[1]=a[1]+E|0,a[2]=a[2]+j|0,a[3]=a[3]+P|0},_doFinalize:function(){var t=this._data,s=t.words,n=8*this._nDataBytes,r=8*t.sigBytes;s[r>>>5]|=128<<24-r%32;var i=e.floor(n/4294967296);for(s[15+(r+64>>>9<<4)]=16711935&(i<<8|i>>>24)|4278255360&(i<<24|i>>>8),s[14+(r+64>>>9<<4)]=16711935&(n<<8|n>>>24)|4278255360&(n<<24|n>>>8),t.sigBytes=4*(s.length+1),this._process(),s=(t=this._hash).words,n=0;4>n;n++)r=s[n],s[n]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8);return t},clone:function(){var e=o.clone.call(this);return e._hash=this._hash.clone(),e}}),i.MD5=o._createHelper(c),i.HmacMD5=o._createHmacHelper(c)}(Math),function(){var e,t=S,s=(e=t.lib).Base,n=e.WordArray,r=(e=t.algo).EvpKDF=s.extend({cfg:s.extend({keySize:4,hasher:e.MD5,iterations:1}),init:function(e){this.cfg=this.cfg.extend(e)},compute:function(e,t){for(var s=(o=this.cfg).hasher.create(),r=n.create(),i=r.words,a=o.keySize,o=o.iterations;i.length>>2]}},e.BlockCipher=a.extend({cfg:a.cfg.extend({mode:o,padding:u}),reset:function(){a.reset.call(this);var e=(t=this.cfg).iv,t=t.mode;if(this._xformMode==this._ENC_XFORM_MODE)var s=t.createEncryptor;else s=t.createDecryptor,this._minBufferSize=1;this._mode=s.call(t,this,e&&e.words)},_doProcessBlock:function(e,t){this._mode.processBlock(e,t)},_doFinalize:function(){var e=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){e.pad(this._data,this.blockSize);var t=this._process(!0)}else t=this._process(!0),e.unpad(t);return t},blockSize:4});var l=e.CipherParams=t.extend({init:function(e){this.mixIn(e)},toString:function(e){return(e||this.formatter).stringify(this)}}),h=(o=(d.format={}).OpenSSL={stringify:function(e){var t=e.ciphertext;return((e=e.salt)?s.create([1398893684,1701076831]).concat(e).concat(t):t).toString(r)},parse:function(e){var t=(e=r.parse(e)).words;if(1398893684==t[0]&&1701076831==t[1]){var n=s.create(t.slice(2,4));t.splice(0,4),e.sigBytes-=16}return l.create({ciphertext:e,salt:n})}},e.SerializableCipher=t.extend({cfg:t.extend({format:o}),encrypt:function(e,t,s,n){n=this.cfg.extend(n);var r=e.createEncryptor(s,n);return t=r.finalize(t),r=r.cfg,l.create({ciphertext:t,key:s,iv:r.iv,algorithm:e,mode:r.mode,padding:r.padding,blockSize:e.blockSize,formatter:n.format})},decrypt:function(e,t,s,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),e.createDecryptor(s,n).finalize(t.ciphertext)},_parse:function(e,t){return"string"==typeof e?t.parse(e,this):e}})),d=(d.kdf={}).OpenSSL={execute:function(e,t,n,r){return r||(r=s.random(8)),e=i.create({keySize:t+n}).compute(e,r),n=s.create(e.words.slice(t),4*n),e.sigBytes=4*t,l.create({key:e,iv:n,salt:r})}},p=e.PasswordBasedCipher=h.extend({cfg:h.cfg.extend({kdf:d}),encrypt:function(e,t,s,n){return s=(n=this.cfg.extend(n)).kdf.execute(s,e.keySize,e.ivSize),n.iv=s.iv,(e=h.encrypt.call(this,e,t,s.key,n)).mixIn(s),e},decrypt:function(e,t,s,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),s=n.kdf.execute(s,e.keySize,e.ivSize,t.salt),n.iv=s.iv,h.decrypt.call(this,e,t,s.key,n)}})}(),function(){for(var e=S,t=e.lib.BlockCipher,s=e.algo,n=[],r=[],i=[],a=[],o=[],c=[],u=[],l=[],h=[],d=[],p=[],g=0;256>g;g++)p[g]=128>g?g<<1:g<<1^283;var b=0,y=0;for(g=0;256>g;g++){var m=(m=y^y<<1^y<<2^y<<3^y<<4)>>>8^255&m^99;n[b]=m,r[m]=b;var f=p[b],v=p[f],w=p[v],O=257*p[m]^16843008*m;i[b]=O<<24|O>>>8,a[b]=O<<16|O>>>16,o[b]=O<<8|O>>>24,c[b]=O,O=16843009*w^65537*v^257*f^16843008*b,u[m]=O<<24|O>>>8,l[m]=O<<16|O>>>16,h[m]=O<<8|O>>>24,d[m]=O,b?(b=f^p[p[p[w^f]]],y^=p[p[y]]):b=y=1}var k=[0,1,2,4,8,16,32,64,128,27,54];s=s.AES=t.extend({_doReset:function(){for(var e=(s=this._key).words,t=s.sigBytes/4,s=4*((this._nRounds=t+6)+1),r=this._keySchedule=[],i=0;i>>24]<<24|n[a>>>16&255]<<16|n[a>>>8&255]<<8|n[255&a]):(a=n[(a=a<<8|a>>>24)>>>24]<<24|n[a>>>16&255]<<16|n[a>>>8&255]<<8|n[255&a],a^=k[i/t|0]<<24),r[i]=r[i-t]^a}for(e=this._invKeySchedule=[],t=0;tt||4>=i?a:u[n[a>>>24]]^l[n[a>>>16&255]]^h[n[a>>>8&255]]^d[n[255&a]]},encryptBlock:function(e,t){this._doCryptBlock(e,t,this._keySchedule,i,a,o,c,n)},decryptBlock:function(e,t){var s=e[t+1];e[t+1]=e[t+3],e[t+3]=s,this._doCryptBlock(e,t,this._invKeySchedule,u,l,h,d,r),s=e[t+1],e[t+1]=e[t+3],e[t+3]=s},_doCryptBlock:function(e,t,s,n,r,i,a,o){for(var c=this._nRounds,u=e[t]^s[0],l=e[t+1]^s[1],h=e[t+2]^s[2],d=e[t+3]^s[3],p=4,g=1;g>>24]^r[l>>>16&255]^i[h>>>8&255]^a[255&d]^s[p++],y=n[l>>>24]^r[h>>>16&255]^i[d>>>8&255]^a[255&u]^s[p++],m=n[h>>>24]^r[d>>>16&255]^i[u>>>8&255]^a[255&l]^s[p++];d=n[d>>>24]^r[u>>>16&255]^i[l>>>8&255]^a[255&h]^s[p++],u=b,l=y,h=m}b=(o[u>>>24]<<24|o[l>>>16&255]<<16|o[h>>>8&255]<<8|o[255&d])^s[p++],y=(o[l>>>24]<<24|o[h>>>16&255]<<16|o[d>>>8&255]<<8|o[255&u])^s[p++],m=(o[h>>>24]<<24|o[d>>>16&255]<<16|o[u>>>8&255]<<8|o[255&l])^s[p++],d=(o[d>>>24]<<24|o[u>>>16&255]<<16|o[l>>>8&255]<<8|o[255&h])^s[p++],e[t]=b,e[t+1]=y,e[t+2]=m,e[t+3]=d},keySize:8});e.AES=t._createHelper(s)}(),S.mode.ECB=((v=S.lib.BlockCipherMode.extend()).Encryptor=v.extend({processBlock:function(e,t){this._cipher.encryptBlock(e,t)}}),v.Decryptor=v.extend({processBlock:function(e,t){this._cipher.decryptBlock(e,t)}}),v);var w,O=t(S);class k{constructor({cipherKey:e}){this.cipherKey=e,this.CryptoJS=O,this.encryptedKey=this.CryptoJS.SHA256(e)}encrypt(e){if(0===("string"==typeof e?e:k.decoder.decode(e)).length)throw new Error("encryption error. empty content");const t=this.getIv();return{metadata:t,data:c(this.CryptoJS.AES.encrypt(e,this.encryptedKey,{iv:this.bufferToWordArray(t),mode:this.CryptoJS.mode.CBC}).ciphertext.toString(this.CryptoJS.enc.Base64))}}encryptFileData(e){return i(this,void 0,void 0,(function*(){const t=yield this.getKey(),s=this.getIv();return{data:yield crypto.subtle.encrypt({name:this.algo,iv:s},t,e),metadata:s}}))}decrypt(e){if("string"==typeof e.data)throw new Error("Decryption error: data for decryption should be ArrayBuffed.");const t=this.bufferToWordArray(new Uint8ClampedArray(e.metadata)),s=this.bufferToWordArray(new Uint8ClampedArray(e.data));return k.encoder.encode(this.CryptoJS.AES.decrypt({ciphertext:s},this.encryptedKey,{iv:t,mode:this.CryptoJS.mode.CBC}).toString(this.CryptoJS.enc.Utf8)).buffer}decryptFileData(e){return i(this,void 0,void 0,(function*(){if("string"==typeof e.data)throw new Error("Decryption error: data for decryption should be ArrayBuffed.");const t=yield this.getKey();return crypto.subtle.decrypt({name:this.algo,iv:e.metadata},t,e.data)}))}get identifier(){return"ACRH"}get algo(){return"AES-CBC"}getIv(){return crypto.getRandomValues(new Uint8Array(k.BLOCK_SIZE))}getKey(){return i(this,void 0,void 0,(function*(){const e=k.encoder.encode(this.cipherKey),t=yield crypto.subtle.digest("SHA-256",e.buffer);return crypto.subtle.importKey("raw",t,this.algo,!0,["encrypt","decrypt"])}))}bufferToWordArray(e){const t=[];let s;for(s=0;s({messageType:"object",message:this.configuration,details:"Create with configuration:",ignoredKeys:(e,t)=>"function"==typeof t[e]||"logger"===e})))}get logger(){return this._logger}HMACSHA256(e){return O.HmacSHA256(e,this.configuration.secretKey).toString(O.enc.Base64)}SHA256(e){return O.SHA256(e).toString(O.enc.Hex)}encrypt(e,t,s){return this.configuration.customEncrypt?(this.logger&&this.logger.warn("Crypto","'customEncrypt' is deprecated. Consult docs for better alternative."),this.configuration.customEncrypt(e)):this.pnEncrypt(e,t,s)}decrypt(e,t,s){return this.configuration.customDecrypt?(this.logger&&this.logger.warn("Crypto","'customDecrypt' is deprecated. Consult docs for better alternative."),this.configuration.customDecrypt(e)):this.pnDecrypt(e,t,s)}pnEncrypt(e,t,s){const n=null!=t?t:this.configuration.cipherKey;if(!n)return e;this.logger&&this.logger.debug("Crypto",(()=>({messageType:"object",message:Object.assign({data:e,cipherKey:n},null!=s?s:{}),details:"Encrypt with parameters:"}))),s=this.parseOptions(s);const r=this.getMode(s),i=this.getPaddedKey(n,s);if(this.configuration.useRandomIVs){const t=this.getRandomIV(),s=O.AES.encrypt(e,i,{iv:t,mode:r}).ciphertext;return t.clone().concat(s.clone()).toString(O.enc.Base64)}const a=this.getIV(s);return O.AES.encrypt(e,i,{iv:a,mode:r}).ciphertext.toString(O.enc.Base64)||e}pnDecrypt(e,t,s){const n=null!=t?t:this.configuration.cipherKey;if(!n)return e;this.logger&&this.logger.debug("Crypto",(()=>({messageType:"object",message:Object.assign({data:e,cipherKey:n},null!=s?s:{}),details:"Decrypt with parameters:"}))),s=this.parseOptions(s);const r=this.getMode(s),i=this.getPaddedKey(n,s);if(this.configuration.useRandomIVs){const t=new Uint8ClampedArray(c(e)),s=C(t.slice(0,16)),n=C(t.slice(16));try{const e=O.AES.decrypt({ciphertext:n},i,{iv:s,mode:r}).toString(O.enc.Utf8);return JSON.parse(e)}catch(e){return this.logger&&this.logger.error("Crypto",(()=>({messageType:"error",message:e}))),null}}else{const t=this.getIV(s);try{const s=O.enc.Base64.parse(e),n=O.AES.decrypt({ciphertext:s},i,{iv:t,mode:r}).toString(O.enc.Utf8);return JSON.parse(n)}catch(e){return this.logger&&this.logger.error("Crypto",(()=>({messageType:"error",message:e}))),null}}}parseOptions(e){var t,s,n,r;if(!e)return this.defaultOptions;const i={encryptKey:null!==(t=e.encryptKey)&&void 0!==t?t:this.defaultOptions.encryptKey,keyEncoding:null!==(s=e.keyEncoding)&&void 0!==s?s:this.defaultOptions.keyEncoding,keyLength:null!==(n=e.keyLength)&&void 0!==n?n:this.defaultOptions.keyLength,mode:null!==(r=e.mode)&&void 0!==r?r:this.defaultOptions.mode};return-1===this.allowedKeyEncodings.indexOf(i.keyEncoding.toLowerCase())&&(i.keyEncoding=this.defaultOptions.keyEncoding),-1===this.allowedKeyLengths.indexOf(i.keyLength)&&(i.keyLength=this.defaultOptions.keyLength),-1===this.allowedModes.indexOf(i.mode.toLowerCase())&&(i.mode=this.defaultOptions.mode),i}decodeKey(e,t){return"base64"===t.keyEncoding?O.enc.Base64.parse(e):"hex"===t.keyEncoding?O.enc.Hex.parse(e):e}getPaddedKey(e,t){return e=this.decodeKey(e,t),t.encryptKey?O.enc.Utf8.parse(this.SHA256(e).slice(0,32)):e}getMode(e){return"ecb"===e.mode?O.mode.ECB:O.mode.CBC}getIV(e){return"cbc"===e.mode?O.enc.Utf8.parse(this.iv):null}getRandomIV(){return O.lib.WordArray.random(16)}}class j{encrypt(e,t){return i(this,void 0,void 0,(function*(){if(!(t instanceof ArrayBuffer)&&"string"!=typeof t)throw new Error("Cannot encrypt this file. In browsers file encryption supports only string or ArrayBuffer");const s=yield this.getKey(e);return t instanceof ArrayBuffer?this.encryptArrayBuffer(s,t):this.encryptString(s,t)}))}encryptArrayBuffer(e,t){return i(this,void 0,void 0,(function*(){const s=crypto.getRandomValues(new Uint8Array(16));return this.concatArrayBuffer(s.buffer,yield crypto.subtle.encrypt({name:"AES-CBC",iv:s},e,t))}))}encryptString(e,t){return i(this,void 0,void 0,(function*(){const s=crypto.getRandomValues(new Uint8Array(16)),n=j.encoder.encode(t).buffer,r=yield crypto.subtle.encrypt({name:"AES-CBC",iv:s},e,n),i=this.concatArrayBuffer(s.buffer,r);return j.decoder.decode(i)}))}encryptFile(e,t,s){return i(this,void 0,void 0,(function*(){var n,r;if((null!==(n=t.contentLength)&&void 0!==n?n:0)<=0)throw new Error("encryption error. empty content");const i=yield this.getKey(e),a=yield t.toArrayBuffer(),o=yield this.encryptArrayBuffer(i,a);return s.create({name:t.name,mimeType:null!==(r=t.mimeType)&&void 0!==r?r:"application/octet-stream",data:o})}))}decrypt(e,t){return i(this,void 0,void 0,(function*(){if(!(t instanceof ArrayBuffer)&&"string"!=typeof t)throw new Error("Cannot decrypt this file. In browsers file decryption supports only string or ArrayBuffer");const s=yield this.getKey(e);return t instanceof ArrayBuffer?this.decryptArrayBuffer(s,t):this.decryptString(s,t)}))}decryptArrayBuffer(e,t){return i(this,void 0,void 0,(function*(){const s=t.slice(0,16);if(t.slice(j.IV_LENGTH).byteLength<=0)throw new Error("decryption error: empty content");return yield crypto.subtle.decrypt({name:"AES-CBC",iv:s},e,t.slice(j.IV_LENGTH))}))}decryptString(e,t){return i(this,void 0,void 0,(function*(){const s=j.encoder.encode(t).buffer,n=s.slice(0,16),r=s.slice(16),i=yield crypto.subtle.decrypt({name:"AES-CBC",iv:n},e,r);return j.decoder.decode(i)}))}decryptFile(e,t,s){return i(this,void 0,void 0,(function*(){const n=yield this.getKey(e),r=yield t.toArrayBuffer(),i=yield this.decryptArrayBuffer(n,r);return s.create({name:t.name,mimeType:t.mimeType,data:i})}))}getKey(e){return i(this,void 0,void 0,(function*(){const t=yield crypto.subtle.digest("SHA-256",j.encoder.encode(e)),s=Array.from(new Uint8Array(t)).map((e=>e.toString(16).padStart(2,"0"))).join(""),n=j.encoder.encode(s.slice(0,32)).buffer;return crypto.subtle.importKey("raw",n,"AES-CBC",!0,["encrypt","decrypt"])}))}concatArrayBuffer(e,t){const s=new Uint8Array(e.byteLength+t.byteLength);return s.set(new Uint8Array(e),0),s.set(new Uint8Array(t),e.byteLength),s.buffer}}j.IV_LENGTH=16,j.encoder=new TextEncoder,j.decoder=new TextDecoder;class E{constructor(e){this.config=e,this.cryptor=new P(Object.assign({},e)),this.fileCryptor=new j}set logger(e){this.cryptor.logger=e}encrypt(e){const t="string"==typeof e?e:E.decoder.decode(e);return{data:this.cryptor.encrypt(t),metadata:null}}encryptFile(e,t){return i(this,void 0,void 0,(function*(){var s;if(!this.config.cipherKey)throw new d("File encryption error: cipher key not set.");return this.fileCryptor.encryptFile(null===(s=this.config)||void 0===s?void 0:s.cipherKey,e,t)}))}decrypt(e){const t="string"==typeof e.data?e.data:u(e.data);return this.cryptor.decrypt(t)}decryptFile(e,t){return i(this,void 0,void 0,(function*(){if(!this.config.cipherKey)throw new d("File encryption error: cipher key not set.");return this.fileCryptor.decryptFile(this.config.cipherKey,e,t)}))}get identifier(){return""}toString(){return`AesCbcCryptor { ${Object.entries(this.config).reduce(((e,[t,s])=>("logger"===t||e.push(`${t}: ${"function"==typeof s?"":s}`),e)),[]).join(", ")} }`}}E.encoder=new TextEncoder,E.decoder=new TextDecoder;class N extends a{set logger(e){if(this.defaultCryptor.identifier===N.LEGACY_IDENTIFIER)this.defaultCryptor.logger=e;else{const t=this.cryptors.find((e=>e.identifier===N.LEGACY_IDENTIFIER));t&&(t.logger=e)}}static legacyCryptoModule(e){var t;if(!e.cipherKey)throw new d("Crypto module error: cipher key not set.");return new N({default:new E(Object.assign(Object.assign({},e),{useRandomIVs:null===(t=e.useRandomIVs)||void 0===t||t})),cryptors:[new k({cipherKey:e.cipherKey})]})}static aesCbcCryptoModule(e){var t;if(!e.cipherKey)throw new d("Crypto module error: cipher key not set.");return new N({default:new k({cipherKey:e.cipherKey}),cryptors:[new E(Object.assign(Object.assign({},e),{useRandomIVs:null===(t=e.useRandomIVs)||void 0===t||t}))]})}static withDefaultCryptor(e){return new this({default:e})}encrypt(e){const t=e instanceof ArrayBuffer&&this.defaultCryptor.identifier===N.LEGACY_IDENTIFIER?this.defaultCryptor.encrypt(N.decoder.decode(e)):this.defaultCryptor.encrypt(e);if(!t.metadata)return t.data;if("string"==typeof t.data)throw new Error("Encryption error: encrypted data should be ArrayBuffed.");const s=this.getHeaderData(t);return this.concatArrayBuffer(s,t.data)}encryptFile(e,t){return i(this,void 0,void 0,(function*(){if(this.defaultCryptor.identifier===T.LEGACY_IDENTIFIER)return this.defaultCryptor.encryptFile(e,t);const s=yield this.getFileData(e),n=yield this.defaultCryptor.encryptFileData(s);if("string"==typeof n.data)throw new Error("Encryption error: encrypted data should be ArrayBuffed.");return t.create({name:e.name,mimeType:"application/octet-stream",data:this.concatArrayBuffer(this.getHeaderData(n),n.data)})}))}decrypt(e){const t="string"==typeof e?c(e):e,s=T.tryParse(t),n=this.getCryptor(s),r=s.length>0?t.slice(s.length-s.metadataLength,s.length):null;if(t.slice(s.length).byteLength<=0)throw new Error("Decryption error: empty content");return n.decrypt({data:t.slice(s.length),metadata:r})}decryptFile(e,t){return i(this,void 0,void 0,(function*(){const s=yield e.data.arrayBuffer(),n=T.tryParse(s),r=this.getCryptor(n);if((null==r?void 0:r.identifier)===T.LEGACY_IDENTIFIER)return r.decryptFile(e,t);const i=(yield this.getFileData(s)).slice(n.length-n.metadataLength,n.length);return t.create({name:e.name,data:yield this.defaultCryptor.decryptFileData({data:s.slice(n.length),metadata:i})})}))}getCryptorFromId(e){const t=this.getAllCryptors().find((t=>e===t.identifier));if(t)return t;throw Error("Unknown cryptor error")}getCryptor(e){if("string"==typeof e){const t=this.getAllCryptors().find((t=>t.identifier===e));if(t)return t;throw new Error("Unknown cryptor error")}if(e instanceof _)return this.getCryptorFromId(e.identifier)}getHeaderData(e){if(!e.metadata)return;const t=T.from(this.defaultCryptor.identifier,e.metadata),s=new Uint8Array(t.length);let n=0;return s.set(t.data,n),n+=t.length-e.metadata.byteLength,s.set(new Uint8Array(e.metadata),n),s.buffer}concatArrayBuffer(e,t){const s=new Uint8Array(e.byteLength+t.byteLength);return s.set(new Uint8Array(e),0),s.set(new Uint8Array(t),e.byteLength),s.buffer}getFileData(e){return i(this,void 0,void 0,(function*(){if(e instanceof ArrayBuffer)return e;if(e instanceof o)return e.toArrayBuffer();throw new Error("Cannot decrypt/encrypt file. In browsers file encrypt/decrypt supported for string, ArrayBuffer or Blob")}))}}N.LEGACY_IDENTIFIER="";class T{static from(e,t){if(e!==T.LEGACY_IDENTIFIER)return new _(e,t.byteLength)}static tryParse(e){const t=new Uint8Array(e);let s,n,r=null;if(t.byteLength>=4&&(s=t.slice(0,4),this.decoder.decode(s)!==T.SENTINEL))return N.LEGACY_IDENTIFIER;if(!(t.byteLength>=5))throw new Error("Decryption error: invalid header version");if(r=t[4],r>T.MAX_VERSION)throw new Error("Decryption error: Unknown cryptor error");let i=5+T.IDENTIFIER_LENGTH;if(!(t.byteLength>=i))throw new Error("Decryption error: invalid crypto identifier");n=t.slice(5,i);let a=null;if(!(t.byteLength>=i+1))throw new Error("Decryption error: invalid metadata length");return a=t[i],i+=1,255===a&&t.byteLength>=i+2&&(a=new Uint16Array(t.slice(i,i+2)).reduce(((e,t)=>(e<<8)+t),0)),new _(this.decoder.decode(n),a)}}T.SENTINEL="PNED",T.LEGACY_IDENTIFIER="",T.IDENTIFIER_LENGTH=4,T.VERSION=1,T.MAX_VERSION=1,T.decoder=new TextDecoder;class _{constructor(e,t){this._identifier=e,this._metadataLength=t}get identifier(){return this._identifier}set identifier(e){this._identifier=e}get metadataLength(){return this._metadataLength}set metadataLength(e){this._metadataLength=e}get version(){return T.VERSION}get length(){return T.SENTINEL.length+1+T.IDENTIFIER_LENGTH+(this.metadataLength<255?1:3)+this.metadataLength}get data(){let e=0;const t=new Uint8Array(this.length),s=new TextEncoder;t.set(s.encode(T.SENTINEL)),e+=T.SENTINEL.length,t[e]=this.version,e++,this.identifier&&t.set(s.encode(this.identifier),e);const n=this.metadataLength;return e+=T.IDENTIFIER_LENGTH,n<255?t[e]=n:t.set([255,n>>8,255&n],e),t}}_.IDENTIFIER_LENGTH=4,_.SENTINEL="PNED";class I extends Error{static create(e,t){return I.isErrorObject(e)?I.createFromError(e):I.createFromServiceResponse(e,t)}static createFromError(e){let t=h.PNUnknownCategory,s="Unknown error",n="Error";if(!e)return new I(s,t,0);if(e instanceof I)return e;if(I.isErrorObject(e)&&(s=e.message,n=e.name),"AbortError"===n||-1!==s.indexOf("Aborted"))t=h.PNCancelledCategory,s="Request cancelled";else if(-1!==s.toLowerCase().indexOf("timeout"))t=h.PNTimeoutCategory,s="Request timeout";else if(-1!==s.toLowerCase().indexOf("network"))t=h.PNNetworkIssuesCategory,s="Network issues";else if("TypeError"===n)t=-1!==s.indexOf("Load failed")||-1!=s.indexOf("Failed to fetch")?h.PNNetworkIssuesCategory:h.PNBadRequestCategory;else if("FetchError"===n){const n=e.code;["ECONNREFUSED","ENETUNREACH","ENOTFOUND","ECONNRESET","EAI_AGAIN"].includes(n)&&(t=h.PNNetworkIssuesCategory),"ECONNREFUSED"===n?s="Connection refused":"ENETUNREACH"===n?s="Network not reachable":"ENOTFOUND"===n?s="Server not found":"ECONNRESET"===n?s="Connection reset by peer":"EAI_AGAIN"===n?s="Name resolution error":"ETIMEDOUT"===n?(t=h.PNTimeoutCategory,s="Request timeout"):s=`Unknown system error: ${e}`}else"Request timeout"===s&&(t=h.PNTimeoutCategory);return new I(s,t,0,e)}static createFromServiceResponse(e,t){let s,n=h.PNUnknownCategory,r="Unknown error",{status:i}=e;if(null!=t||(t=e.body),402===i?r="Not available for used key set. Contact support@pubnub.com":400===i?(n=h.PNBadRequestCategory,r="Bad request"):403===i?(n=h.PNAccessDeniedCategory,r="Access denied"):i>=500&&(n=h.PNServerErrorCategory,r="Internal server error"),"object"==typeof e&&0===Object.keys(e).length&&(n=h.PNMalformedResponseCategory,r="Malformed response (network issues)",i=400),t&&t.byteLength>0){const n=(new TextDecoder).decode(t);if(-1!==e.headers["content-type"].indexOf("text/javascript")||-1!==e.headers["content-type"].indexOf("application/json"))try{const e=JSON.parse(n);"object"==typeof e&&(Array.isArray(e)?"number"==typeof e[0]&&0===e[0]&&e.length>1&&"string"==typeof e[1]&&(s=e[1]):("error"in e&&(1===e.error||!0===e.error)&&"status"in e&&"number"==typeof e.status&&"message"in e&&"service"in e?(s=e,i=e.status):s=e,"error"in e&&e.error instanceof Error&&(s=e.error)))}catch(e){s=n}else if(-1!==e.headers["content-type"].indexOf("xml")){const e=/(.*)<\/Message>/gi.exec(n);r=e?`Upload to bucket failed: ${e[1]}`:"Upload to bucket failed."}else s=n}return new I(r,n,i,s)}constructor(e,t,s,n){super(e),this.category=t,this.statusCode=s,this.errorData=n,this.name="PubNubAPIError"}toStatus(e){return{error:!0,category:this.category,operation:e,statusCode:this.statusCode,errorData:this.errorData,toJSON:function(){let e;const t=this.errorData;if(t)try{if("object"==typeof t){const s=Object.assign(Object.assign(Object.assign(Object.assign({},"name"in t?{name:t.name}:{}),"message"in t?{message:t.message}:{}),"stack"in t?{stack:t.stack}:{}),t);e=JSON.parse(JSON.stringify(s,I.circularReplacer()))}else e=t}catch(t){e={error:"Could not serialize the error object"}}const s=r(this,["toJSON"]);return JSON.stringify(Object.assign(Object.assign({},s),{errorData:e}))}}}toPubNubError(e,t){return new d(null!=t?t:this.message,this.toStatus(e))}static circularReplacer(){const e=new WeakSet;return function(t,s){if("object"==typeof s&&null!==s){if(e.has(s))return"[Circular]";e.add(s)}return s}}static isErrorObject(e){return!(!e||"object"!=typeof e)&&(e instanceof Error||("name"in e&&"message"in e&&"string"==typeof e.name&&"string"==typeof e.message||"[object Error]"===Object.prototype.toString.call(e)))}}!function(e){e.PNPublishOperation="PNPublishOperation",e.PNSignalOperation="PNSignalOperation",e.PNSubscribeOperation="PNSubscribeOperation",e.PNUnsubscribeOperation="PNUnsubscribeOperation",e.PNWhereNowOperation="PNWhereNowOperation",e.PNHereNowOperation="PNHereNowOperation",e.PNGlobalHereNowOperation="PNGlobalHereNowOperation",e.PNSetStateOperation="PNSetStateOperation",e.PNGetStateOperation="PNGetStateOperation",e.PNHeartbeatOperation="PNHeartbeatOperation",e.PNAddMessageActionOperation="PNAddActionOperation",e.PNRemoveMessageActionOperation="PNRemoveMessageActionOperation",e.PNGetMessageActionsOperation="PNGetMessageActionsOperation",e.PNTimeOperation="PNTimeOperation",e.PNHistoryOperation="PNHistoryOperation",e.PNDeleteMessagesOperation="PNDeleteMessagesOperation",e.PNFetchMessagesOperation="PNFetchMessagesOperation",e.PNMessageCounts="PNMessageCountsOperation",e.PNGetAllUUIDMetadataOperation="PNGetAllUUIDMetadataOperation",e.PNGetUUIDMetadataOperation="PNGetUUIDMetadataOperation",e.PNSetUUIDMetadataOperation="PNSetUUIDMetadataOperation",e.PNRemoveUUIDMetadataOperation="PNRemoveUUIDMetadataOperation",e.PNGetAllChannelMetadataOperation="PNGetAllChannelMetadataOperation",e.PNGetChannelMetadataOperation="PNGetChannelMetadataOperation",e.PNSetChannelMetadataOperation="PNSetChannelMetadataOperation",e.PNRemoveChannelMetadataOperation="PNRemoveChannelMetadataOperation",e.PNGetMembersOperation="PNGetMembersOperation",e.PNSetMembersOperation="PNSetMembersOperation",e.PNGetMembershipsOperation="PNGetMembershipsOperation",e.PNSetMembershipsOperation="PNSetMembershipsOperation",e.PNListFilesOperation="PNListFilesOperation",e.PNGenerateUploadUrlOperation="PNGenerateUploadUrlOperation",e.PNPublishFileOperation="PNPublishFileOperation",e.PNPublishFileMessageOperation="PNPublishFileMessageOperation",e.PNGetFileUrlOperation="PNGetFileUrlOperation",e.PNDownloadFileOperation="PNDownloadFileOperation",e.PNDeleteFileOperation="PNDeleteFileOperation",e.PNAddPushNotificationEnabledChannelsOperation="PNAddPushNotificationEnabledChannelsOperation",e.PNRemovePushNotificationEnabledChannelsOperation="PNRemovePushNotificationEnabledChannelsOperation",e.PNPushNotificationEnabledChannelsOperation="PNPushNotificationEnabledChannelsOperation",e.PNRemoveAllPushNotificationsOperation="PNRemoveAllPushNotificationsOperation",e.PNChannelGroupsOperation="PNChannelGroupsOperation",e.PNRemoveGroupOperation="PNRemoveGroupOperation",e.PNChannelsForGroupOperation="PNChannelsForGroupOperation",e.PNAddChannelsToGroupOperation="PNAddChannelsToGroupOperation",e.PNRemoveChannelsFromGroupOperation="PNRemoveChannelsFromGroupOperation",e.PNAccessManagerGrant="PNAccessManagerGrant",e.PNAccessManagerGrantToken="PNAccessManagerGrantToken",e.PNAccessManagerAudit="PNAccessManagerAudit",e.PNAccessManagerRevokeToken="PNAccessManagerRevokeToken",e.PNHandshakeOperation="PNHandshakeOperation",e.PNReceiveMessagesOperation="PNReceiveMessagesOperation"}(w||(w={}));var M=w;class A{constructor(e){this.configuration=e,this.subscriptionWorkerReady=!1,this.accessTokensMap={},this.workerEventsQueue=[],this.callbacks=new Map,this.setupSubscriptionWorker()}set emitStatus(e){this._emitStatus=e}onUserIdChange(e){this.configuration.userId=e,this.scheduleEventPost({type:"client-update",heartbeatInterval:this.configuration.heartbeatInterval,clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,userId:this.configuration.userId,workerLogLevel:this.configuration.workerLogLevel})}onHeartbeatIntervalChange(e){this.configuration.heartbeatInterval=e,this.scheduleEventPost({type:"client-update",heartbeatInterval:this.configuration.heartbeatInterval,clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,userId:this.configuration.userId,workerLogLevel:this.configuration.workerLogLevel})}onTokenChange(e){const t={type:"client-update",heartbeatInterval:this.configuration.heartbeatInterval,clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,userId:this.configuration.userId,workerLogLevel:this.configuration.workerLogLevel};this.parsedAccessToken(e).then((s=>{t.preProcessedToken=s,t.accessToken=e})).then((()=>this.scheduleEventPost(t)))}disconnect(){this.scheduleEventPost({type:"client-disconnect",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,workerLogLevel:this.configuration.workerLogLevel})}terminate(){this.scheduleEventPost({type:"client-unregister",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,workerLogLevel:this.configuration.workerLogLevel})}makeSendable(e){if(!e.path.startsWith("/v2/subscribe")&&!e.path.endsWith("/heartbeat")&&!e.path.endsWith("/leave"))return this.configuration.transport.makeSendable(e);let t;this.configuration.logger.debug("SubscriptionWorkerMiddleware","Process request with SharedWorker transport.");const s={type:"send-request",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,request:e,workerLogLevel:this.configuration.workerLogLevel};return e.cancellable&&(t={abort:()=>{const t={type:"cancel-request",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,identifier:e.identifier,workerLogLevel:this.configuration.workerLogLevel};this.scheduleEventPost(t)}}),[new Promise(((t,n)=>{this.callbacks.set(e.identifier,{resolve:t,reject:n}),this.parsedAccessTokenForRequest(e).then((e=>s.preProcessedToken=e)).then((()=>this.scheduleEventPost(s)))})),t]}request(e){return e}scheduleEventPost(e,t=!1){const s=this.sharedSubscriptionWorker;s?s.port.postMessage(e):t?this.workerEventsQueue.splice(0,0,e):this.workerEventsQueue.push(e)}flushScheduledEvents(){const e=this.sharedSubscriptionWorker;if(!e||0===this.workerEventsQueue.length)return;const t=[];for(let e=0;e!t.includes(e))),this.workerEventsQueue.forEach((t=>e.port.postMessage(t))),this.workerEventsQueue=[]}get sharedSubscriptionWorker(){return this.subscriptionWorkerReady?this.subscriptionWorker:null}setupSubscriptionWorker(){if("undefined"!=typeof SharedWorker){try{this.subscriptionWorker=new SharedWorker(this.configuration.workerUrl,`/pubnub-${this.configuration.sdkVersion}`)}catch(e){throw this.configuration.logger.error("SubscriptionWorkerMiddleware",(()=>({messageType:"error",message:e}))),e}this.subscriptionWorker.port.start(),this.scheduleEventPost({type:"client-register",clientIdentifier:this.configuration.clientIdentifier,subscriptionKey:this.configuration.subscriptionKey,userId:this.configuration.userId,heartbeatInterval:this.configuration.heartbeatInterval,workerOfflineClientsCheckInterval:this.configuration.workerOfflineClientsCheckInterval,workerUnsubscribeOfflineClients:this.configuration.workerUnsubscribeOfflineClients,workerLogLevel:this.configuration.workerLogLevel},!0),this.subscriptionWorker.port.onmessage=e=>this.handleWorkerEvent(e),this.shouldAnnounceNewerSharedWorkerVersionAvailability()&&localStorage.setItem("PNSubscriptionSharedWorkerVersion",this.configuration.sdkVersion),window.addEventListener("storage",(e=>{"PNSubscriptionSharedWorkerVersion"===e.key&&e.newValue&&this._emitStatus&&this.isNewerSharedWorkerVersion(e.newValue)&&this._emitStatus({error:!1,category:h.PNSharedWorkerUpdatedCategory})}))}}handleWorkerEvent(e){const{data:t}=e;if("shared-worker-ping"===t.type||"shared-worker-connected"===t.type||"shared-worker-console-log"===t.type||"shared-worker-console-dir"===t.type||t.clientIdentifier===this.configuration.clientIdentifier)if("shared-worker-connected"===t.type)this.configuration.logger.trace("SharedWorker","Ready for events processing."),this.subscriptionWorkerReady=!0,this.flushScheduledEvents();else if("shared-worker-console-log"===t.type)this.configuration.logger.debug("SharedWorker",(()=>"string"==typeof t.message||"number"==typeof t.message||"boolean"==typeof t.message?{messageType:"text",message:t.message}:t.message));else if("shared-worker-console-dir"===t.type)this.configuration.logger.debug("SharedWorker",(()=>({messageType:"object",message:t.data,details:t.message?t.message:void 0})));else if("shared-worker-ping"===t.type){const{subscriptionKey:e,clientIdentifier:t}=this.configuration;this.scheduleEventPost({type:"client-pong",subscriptionKey:e,clientIdentifier:t,workerLogLevel:this.configuration.workerLogLevel})}else if("request-process-success"===t.type||"request-process-error"===t.type)if(this.callbacks.has(t.identifier)){const{resolve:e,reject:s}=this.callbacks.get(t.identifier);this.callbacks.delete(t.identifier),"request-process-success"===t.type?e({status:t.response.status,url:t.url,headers:t.response.headers,body:t.response.body}):s(this.errorFromRequestSendingError(t))}else this._emitStatus&&t.url.indexOf("/v2/presence")>=0&&t.url.indexOf("/heartbeat")>=0&&("request-process-success"===t.type&&this.configuration.announceSuccessfulHeartbeats?this._emitStatus({statusCode:t.response.status,error:!1,operation:M.PNHeartbeatOperation,category:h.PNAcknowledgmentCategory}):"request-process-error"===t.type&&this.configuration.announceFailedHeartbeats&&this._emitStatus(this.errorFromRequestSendingError(t).toStatus(M.PNHeartbeatOperation)))}parsedAccessTokenForRequest(e){return i(this,void 0,void 0,(function*(){var t;return this.parsedAccessToken(e.queryParameters?null!==(t=e.queryParameters.auth)&&void 0!==t?t:"":void 0)}))}parsedAccessToken(e){return i(this,void 0,void 0,(function*(){if(e)return this.accessTokensMap[e]?this.accessTokensMap[e]:this.stringifyAccessToken(e).then((([t,s])=>{if(t&&s)return(this.accessTokensMap={[e]:{token:s,expiration:t.timestamp*t.ttl*60}})[e]}))}))}stringifyAccessToken(e){return i(this,void 0,void 0,(function*(){if(!this.configuration.tokenManager)return[void 0,void 0];const t=this.configuration.tokenManager.parseToken(e);if(!t)return[void 0,void 0];const s=e=>e?Object.entries(e).sort((([e],[t])=>e.localeCompare(t))).map((([e,t])=>Object.entries(t||{}).sort((([e],[t])=>e.localeCompare(t))).map((([t,s])=>{return`${e}:${t}=${s?(n=s,Object.entries(n).filter((([e,t])=>t)).map((([e])=>e[0])).sort().join("")):""}`;var n})).join(","))).join(";"):"";let n=[s(t.resources),s(t.patterns),t.authorized_uuid].filter(Boolean).join("|");if("undefined"!=typeof crypto&&crypto.subtle){const e=yield crypto.subtle.digest("SHA-256",(new TextEncoder).encode(n));n=String.fromCharCode(...Array.from(new Uint8Array(e)))}return[t,"undefined"!=typeof btoa?btoa(n):n]}))}errorFromRequestSendingError(e){let t=h.PNUnknownCategory,s="Unknown error";if(e.error)"NETWORK_ISSUE"===e.error.type?t=h.PNNetworkIssuesCategory:"TIMEOUT"===e.error.type?t=h.PNTimeoutCategory:"ABORTED"===e.error.type&&(t=h.PNCancelledCategory),s=`${e.error.message} (${e.identifier})`;else if(e.response){const{url:t,response:s}=e;return I.create({url:t,headers:s.headers,body:s.body,status:s.status},s.body)}return new I(s,t,0,new Error(s))}shouldAnnounceNewerSharedWorkerVersionAvailability(){const e=localStorage.getItem("PNSubscriptionSharedWorkerVersion");return!e||!this.isNewerSharedWorkerVersion(e)}isNewerSharedWorkerVersion(e){const[t,s,n]=this.configuration.sdkVersion.split(".").map(Number),[r,i,a]=e.split(".").map(Number);return r>t||i>s||a>n}}function U(e,t=0){const s=e=>"object"==typeof e&&null!==e&&e.constructor===Object,n=e=>"number"==typeof e&&isFinite(e);if(!s(e))return e;const r={};return Object.keys(e).forEach((i=>{const a=(e=>"string"==typeof e||e instanceof String)(i);let o=i;const c=e[i];if(t<2)if(a&&i.indexOf(",")>=0){o=i.split(",").map(Number).reduce(((e,t)=>e+String.fromCharCode(t)),"")}else(n(i)||a&&!isNaN(Number(i)))&&(o=String.fromCharCode(n(i)?i:parseInt(i,10)));r[o]=s(c)?U(c,t+1):c})),r}const D=e=>{var t,s,n,r,i,a;return e.subscriptionWorkerUrl&&"undefined"==typeof SharedWorker&&(e.subscriptionWorkerUrl=null),Object.assign(Object.assign({},(e=>{var t,s,n,r,i,a,o,c,u,l,h,p,g,b,y;const m=Object.assign({},e);if(null!==(t=m.ssl)&&void 0!==t||(m.ssl=!0),null!==(s=m.transactionalRequestTimeout)&&void 0!==s||(m.transactionalRequestTimeout=15),null!==(n=m.subscribeRequestTimeout)&&void 0!==n||(m.subscribeRequestTimeout=310),null!==(r=m.fileRequestTimeout)&&void 0!==r||(m.fileRequestTimeout=300),null!==(i=m.restore)&&void 0!==i||(m.restore=!1),null!==(a=m.useInstanceId)&&void 0!==a||(m.useInstanceId=!1),null!==(o=m.suppressLeaveEvents)&&void 0!==o||(m.suppressLeaveEvents=!1),null!==(c=m.requestMessageCountThreshold)&&void 0!==c||(m.requestMessageCountThreshold=100),null!==(u=m.autoNetworkDetection)&&void 0!==u||(m.autoNetworkDetection=!1),null!==(l=m.enableEventEngine)&&void 0!==l||(m.enableEventEngine=!1),null!==(h=m.maintainPresenceState)&&void 0!==h||(m.maintainPresenceState=!0),null!==(p=m.useSmartHeartbeat)&&void 0!==p||(m.useSmartHeartbeat=!1),null!==(g=m.keepAlive)&&void 0!==g||(m.keepAlive=!1),m.userId&&m.uuid)throw new d("PubNub client configuration error: use only 'userId'");if(null!==(b=m.userId)&&void 0!==b||(m.userId=m.uuid),!m.userId)throw new d("PubNub client configuration error: 'userId' not set");if(0===(null===(y=m.userId)||void 0===y?void 0:y.trim().length))throw new d("PubNub client configuration error: 'userId' is empty");m.origin||(m.origin=Array.from({length:20},((e,t)=>`ps${t+1}.pndsn.com`)));const f={subscribeKey:m.subscribeKey,publishKey:m.publishKey,secretKey:m.secretKey};void 0!==m.presenceTimeout&&(m.presenceTimeout>320?(m.presenceTimeout=320,console.warn("WARNING: Presence timeout is larger than the maximum. Using maximum value: ",320)):m.presenceTimeout<=0&&(console.warn("WARNING: Presence timeout should be larger than zero."),delete m.presenceTimeout)),void 0!==m.presenceTimeout?m.heartbeatInterval=m.presenceTimeout/2-1:m.presenceTimeout=300;let v=!1,S=!0,w=5,O=!1,k=100,C=!0;return void 0!==m.dedupeOnSubscribe&&"boolean"==typeof m.dedupeOnSubscribe&&(O=m.dedupeOnSubscribe),void 0!==m.maximumCacheSize&&"number"==typeof m.maximumCacheSize&&(k=m.maximumCacheSize),void 0!==m.useRequestId&&"boolean"==typeof m.useRequestId&&(C=m.useRequestId),void 0!==m.announceSuccessfulHeartbeats&&"boolean"==typeof m.announceSuccessfulHeartbeats&&(v=m.announceSuccessfulHeartbeats),void 0!==m.announceFailedHeartbeats&&"boolean"==typeof m.announceFailedHeartbeats&&(S=m.announceFailedHeartbeats),void 0!==m.fileUploadPublishRetryLimit&&"number"==typeof m.fileUploadPublishRetryLimit&&(w=m.fileUploadPublishRetryLimit),Object.assign(Object.assign({},m),{keySet:f,dedupeOnSubscribe:O,maximumCacheSize:k,useRequestId:C,announceSuccessfulHeartbeats:v,announceFailedHeartbeats:S,fileUploadPublishRetryLimit:w})})(e)),{listenToBrowserNetworkEvents:null===(t=e.listenToBrowserNetworkEvents)||void 0===t||t,subscriptionWorkerUrl:e.subscriptionWorkerUrl,subscriptionWorkerOfflineClientsCheckInterval:null!==(s=e.subscriptionWorkerOfflineClientsCheckInterval)&&void 0!==s?s:10,subscriptionWorkerUnsubscribeOfflineClients:null!==(n=e.subscriptionWorkerUnsubscribeOfflineClients)&&void 0!==n&&n,subscriptionWorkerLogVerbosity:null!==(r=e.subscriptionWorkerLogVerbosity)&&void 0!==r&&r,transport:null!==(i=e.transport)&&void 0!==i?i:"fetch",keepAlive:null===(a=e.keepAlive)||void 0===a||a})};var F;!function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Info=2]="Info",e[e.Warn=3]="Warn",e[e.Error=4]="Error",e[e.None=5]="None"}(F||(F={}));const R=e=>encodeURIComponent(e).replace(/[!~*'()]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`)),$=(e,t)=>{const s=e.map((e=>R(e)));return s.length?s.join(","):null!=t?t:""},x=(e,t)=>{const s=Object.fromEntries(t.map((e=>[e,!1])));return e.filter((e=>!(t.includes(e)&&!s[e])||(s[e]=!0,!1)))},q=(e,t)=>[...e].filter((s=>t.includes(s)&&e.indexOf(s)===e.lastIndexOf(s)&&t.indexOf(s)===t.lastIndexOf(s))),L=e=>Object.keys(e).map((t=>{const s=e[t];return Array.isArray(s)?s.map((e=>`${t}=${R(e)}`)).join("&"):`${t}=${R(s)}`})).join("&"),G=(e,t)=>{if("0"===t||"0"===e)return;const s=H(`${Date.now()}0000`,t,!1);return H(e,s,!0)},K=(e,t,s)=>{if(e&&0!==e.length){if(t&&t.length>0&&"0"!==t){const n=H(e,t,!1);return H(null!=s?s:`${Date.now()}0000`,n.replace("-",""),Number(n)<0)}return s&&s.length>0&&"0"!==s?s:`${Date.now()}0000`}},H=(e,t,s)=>{t.startsWith("-")&&(t=t.replace("-",""),s=!1),t=t.padStart(17,"0");const n=e.slice(0,10),r=e.slice(10,17),i=t.slice(0,10),a=t.slice(10,17);let o=Number(n),c=Number(r);return o+=Number(i)*(s?1:-1),c+=Number(a)*(s?1:-1),c>=1e7?(o+=Math.floor(c/1e7),c%=1e7):c<0?o>0?(o-=1,c+=1e7):o<0&&(c*=-1):o<0&&c>0&&(o+=1,c=1e7-c),0!==o?`${o}${`${c}`.padStart(7,"0")}`:`${c}`},B=e=>{const t="string"!=typeof e?JSON.stringify(e):e,s=new Uint32Array(1);let n=0,r=t.length;for(;r-- >0;)s[0]=(s[0]<<5)-s[0]+t.charCodeAt(n++);return s[0].toString(16).padStart(8,"0")};class W{debug(e){this.log(e)}error(e){this.log(e)}info(e){this.log(e)}trace(e){this.log(e)}warn(e){this.log(e)}toString(){return"ConsoleLogger {}"}log(e){const t=F[e.level],s=t.toLowerCase();console["trace"===s?"debug":s](`${e.timestamp.toISOString()} PubNub-${e.pubNubId} ${t.padEnd(5," ")}${e.location?` ${e.location}`:""} ${this.logMessage(e)}`)}logMessage(e){if("text"===e.messageType)return e.message;if("object"===e.messageType)return`${e.details?`${e.details}\n`:""}${this.formattedObject(e)}`;if("network-request"===e.messageType){const t=!!e.canceled||!!e.failed,s=e.minimumLevel!==F.Trace||t?void 0:this.formattedHeaders(e),n=e.message,r=n.queryParameters&&Object.keys(n.queryParameters).length>0?L(n.queryParameters):void 0,i=`${n.origin}${n.path}${r?`?${r}`:""}`,a=t?void 0:this.formattedBody(e);let o="Sending";t&&(o=`${e.canceled?"Canceled":"Failed"}${e.details?` (${e.details})`:""}`);const c=((null==a?void 0:a.formData)?"FormData":"Method").length;return`${o} HTTP request:\n ${this.paddedString("Method",c)}: ${n.method}\n ${this.paddedString("URL",c)}: ${i}${s?`\n ${this.paddedString("Headers",c)}:\n${s}`:""}${(null==a?void 0:a.formData)?`\n ${this.paddedString("FormData",c)}:\n${a.formData}`:""}${(null==a?void 0:a.body)?`\n ${this.paddedString("Body",c)}:\n${a.body}`:""}`}if("network-response"===e.messageType){const t=e.minimumLevel===F.Trace?this.formattedHeaders(e):void 0,s=this.formattedBody(e),n=((null==s?void 0:s.formData)?"Headers":"Status").length,r=e.message;return`Received HTTP response:\n ${this.paddedString("URL",n)}: ${r.url}\n ${this.paddedString("Status",n)}: ${r.status}${t?`\n ${this.paddedString("Headers",n)}:\n${t}`:""}${(null==s?void 0:s.body)?`\n ${this.paddedString("Body",n)}:\n${s.body}`:""}`}if("error"===e.messageType){const t=this.formattedErrorStatus(e),s=e.message;return`${s.name}: ${s.message}${t?`\n${t}`:""}`}return""}formattedObject(e){const t=(s,n=1,r=!1)=>{const i=10===n,a=" ".repeat(2*n),o=[],c=(t,s)=>!!e.ignoredKeys&&("function"==typeof e.ignoredKeys?e.ignoredKeys(t,s):e.ignoredKeys.includes(t));if("string"==typeof s)o.push(`${a}- ${s}`);else if("number"==typeof s)o.push(`${a}- ${s}`);else if("boolean"==typeof s)o.push(`${a}- ${s}`);else if(null===s)o.push(`${a}- null`);else if(void 0===s)o.push(`${a}- undefined`);else if("function"==typeof s)o.push(`${a}- `);else if("object"==typeof s)if(Array.isArray(s)||"function"!=typeof s.toString||0===s.toString().indexOf("[object"))if(Array.isArray(s))for(const e of s){const s=r?"":a;if(null===e)o.push(`${s}- null`);else if(void 0===e)o.push(`${s}- undefined`);else if("function"==typeof e)o.push(`${s}- `);else if("object"==typeof e){const r=Array.isArray(e),a=i?"...":t(e,n+1,!r);o.push(`${s}-${r&&!i?"\n":" "}${a}`)}else o.push(`${s}- ${e}`);r=!1}else{const e=s,u=Object.keys(e),l=u.reduce(((t,s)=>Math.max(t,c(s,e)?t:s.length)),0);for(const s of u){if(c(s,e))continue;const u=r?"":a,h=e[s],d=s.padEnd(l," ");if(null===h)o.push(`${u}${d}: null`);else if(void 0===h)o.push(`${u}${d}: undefined`);else if("function"==typeof h)o.push(`${u}${d}: `);else if("object"==typeof h){const e=Array.isArray(h),s=e&&0===h.length,r=!(e||h instanceof String||0!==Object.keys(h).length),a=!e&&"function"==typeof h.toString&&0!==h.toString().indexOf("[object"),c=i?"...":s?"[]":r?"{}":t(h,n+1,a);o.push(`${u}${d}:${i||a||s||r?" ":"\n"}${c}`)}else o.push(`${u}${d}: ${h}`);r=!1}}else o.push(`${r?"":a}${s.toString()}`),r=!1;return o.join("\n")};return t(e.message)}formattedHeaders(e){if(!e.message.headers)return;const t=e.message.headers,s=Object.keys(t).reduce(((e,t)=>Math.max(e,t.length)),0);return Object.keys(t).map((e=>` - ${e.toLowerCase().padEnd(s," ")}: ${t[e]}`)).join("\n")}formattedBody(e){var t;if(!e.message.headers)return;let s,n;const r=e.message.headers,i=null!==(t=r["content-type"])&&void 0!==t?t:r["Content-Type"],a="formData"in e.message?e.message.formData:void 0,o=e.message.body;if(a){const e=a.reduce(((e,{key:t})=>Math.max(e,t.length)),0);s=a.map((({key:t,value:s})=>` - ${t.padEnd(e," ")}: ${s}`)).join("\n")}return o?(n="string"==typeof o?` ${o}`:o instanceof ArrayBuffer||"[object ArrayBuffer]"===Object.prototype.toString.call(o)?!i||-1===i.indexOf("javascript")&&-1===i.indexOf("json")?` ArrayBuffer { byteLength: ${o.byteLength} }`:` ${W.decoder.decode(o)}`:` File { name: ${o.name}${o.contentLength?`, contentLength: ${o.contentLength}`:""}${o.mimeType?`, mimeType: ${o.mimeType}`:""} }`,{body:n,formData:s}):{formData:s}}formattedErrorStatus(e){if(!e.message.status)return;const t=e.message.status,s=t.errorData;let n;if(W.isError(s))n=` ${s.name}: ${s.message}`,s.stack&&(n+=`\n${s.stack.split("\n").map((e=>` ${e}`)).join("\n")}`);else if(s)try{n=` ${JSON.stringify(s)}`}catch(e){n=` ${s}`}return` Category : ${t.category}\n Operation : ${t.operation}\n Status : ${t.statusCode}${n?`\n Error data:\n${n}`:""}`}paddedString(e,t){return e.padEnd(t-e.length," ")}static isError(e){return!!e&&(e instanceof Error||"[object Error]"===Object.prototype.toString.call(e))}}var z;W.decoder=new TextDecoder,function(e){e.Unknown="UnknownEndpoint",e.MessageSend="MessageSendEndpoint",e.Subscribe="SubscribeEndpoint",e.Presence="PresenceEndpoint",e.Files="FilesEndpoint",e.MessageStorage="MessageStorageEndpoint",e.ChannelGroups="ChannelGroupsEndpoint",e.DevicePushNotifications="DevicePushNotificationsEndpoint",e.AppContext="AppContextEndpoint",e.MessageReactions="MessageReactionsEndpoint"}(z||(z={}));class V{static None(){return{shouldRetry:(e,t,s,n)=>!1,getDelay:(e,t)=>-1,validate:()=>!0}}static LinearRetryPolicy(e){var t;return{delay:e.delay,maximumRetry:e.maximumRetry,excluded:null!==(t=e.excluded)&&void 0!==t?t:[],shouldRetry(e,t,s,n){return J(e,t,s,null!=n?n:0,this.maximumRetry,this.excluded)},getDelay(e,t){let s=-1;return t&&void 0!==t.headers["retry-after"]&&(s=parseInt(t.headers["retry-after"],10)),-1===s&&(s=this.delay),1e3*(s+Math.random())},validate(){if(this.delay<2)throw new Error("Delay can not be set less than 2 seconds for retry");if(this.maximumRetry>10)throw new Error("Maximum retry for linear retry policy can not be more than 10")}}}static ExponentialRetryPolicy(e){var t;return{minimumDelay:e.minimumDelay,maximumDelay:e.maximumDelay,maximumRetry:e.maximumRetry,excluded:null!==(t=e.excluded)&&void 0!==t?t:[],shouldRetry(e,t,s,n){return J(e,t,s,null!=n?n:0,this.maximumRetry,this.excluded)},getDelay(e,t){let s=-1;return t&&void 0!==t.headers["retry-after"]&&(s=parseInt(t.headers["retry-after"],10)),-1===s&&(s=Math.min(Math.pow(2,e),this.maximumDelay)),1e3*(s+Math.random())},validate(){if(this.minimumDelay<2)throw new Error("Minimum delay can not be set less than 2 seconds for retry");if(this.maximumDelay>150)throw new Error("Maximum delay can not be set more than 150 seconds for retry");if(this.maximumRetry>6)throw new Error("Maximum retry for exponential retry policy can not be more than 6")}}}}const J=(e,t,s,n,r,i)=>(!s||s!==h.PNCancelledCategory&&s!==h.PNBadRequestCategory&&s!==h.PNAccessDeniedCategory)&&(!X(e,i)&&(!(n>r)&&(!t||(429===t.status||t.status>=500)))),X=(e,t)=>!!(t&&t.length>0)&&t.includes(Q(e)),Q=e=>{let t=z.Unknown;return e.path.startsWith("/v2/subscribe")?t=z.Subscribe:e.path.startsWith("/publish/")||e.path.startsWith("/signal/")?t=z.MessageSend:e.path.startsWith("/v2/presence")?t=z.Presence:e.path.startsWith("/v2/history")||e.path.startsWith("/v3/history")?t=z.MessageStorage:e.path.startsWith("/v1/message-actions/")?t=z.MessageReactions:e.path.startsWith("/v1/channel-registration/")||e.path.startsWith("/v2/objects/")?t=z.ChannelGroups:e.path.startsWith("/v1/push/")||e.path.startsWith("/v2/push/")?t=z.DevicePushNotifications:e.path.startsWith("/v1/files/")&&(t=z.Files),t};class Y{constructor(e,t,s){this.previousEntryTimestamp=0,this.pubNubId=e,this.minLogLevel=t,this.loggers=s}get logLevel(){return this.minLogLevel}trace(e,t){this.log(F.Trace,e,t)}debug(e,t){this.log(F.Debug,e,t)}info(e,t){this.log(F.Info,e,t)}warn(e,t){this.log(F.Warn,e,t)}error(e,t){this.log(F.Error,e,t)}log(e,t,s){if(ee[r](i)))}}var Z={exports:{}}; +/*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */!function(e,t){!function(e){var t="0.1.0",s={3:/^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,4:/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,5:/^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,all:/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i};function n(){var e,t,s="";for(e=0;e<32;e++)t=16*Math.random()|0,8!==e&&12!==e&&16!==e&&20!==e||(s+="-"),s+=(12===e?4:16===e?3&t|8:t).toString(16);return s}function r(e,t){var n=s[t||"all"];return n&&n.test(e)||!1}n.isUUID=r,n.VERSION=t,e.uuid=n,e.isUUID=r}(t),null!==e&&(e.exports=t.uuid)}(Z,Z.exports);var ee=t(Z.exports),te={createUUID:()=>ee.uuid?ee.uuid():ee()};const se=(e,t)=>{var s,n,r,i;!e.retryConfiguration&&e.enableEventEngine&&(e.retryConfiguration=V.ExponentialRetryPolicy({minimumDelay:2,maximumDelay:150,maximumRetry:6,excluded:[z.MessageSend,z.Presence,z.Files,z.MessageStorage,z.ChannelGroups,z.DevicePushNotifications,z.AppContext,z.MessageReactions]}));const a=`pn-${te.createUUID()}`;e.logVerbosity?e.logLevel=F.Debug:void 0===e.logLevel&&(e.logLevel=F.None);const o=new Y(re(a),e.logLevel,[...null!==(s=e.loggers)&&void 0!==s?s:[],new W]);void 0!==e.logVerbosity&&o.warn("Configuration","'logVerbosity' is deprecated. Use 'logLevel' instead."),null===(n=e.retryConfiguration)||void 0===n||n.validate(),null!==(r=e.useRandomIVs)&&void 0!==r||(e.useRandomIVs=true),e.useRandomIVs&&o.warn("Configuration","'useRandomIVs' is deprecated. Use 'cryptoModule' instead."),e.origin=ne(null!==(i=e.ssl)&&void 0!==i&&i,e.origin);const c=e.cryptoModule;c&&delete e.cryptoModule;const u=Object.assign(Object.assign({},e),{_pnsdkSuffix:{},_loggerManager:o,_instanceId:a,_cryptoModule:void 0,_cipherKey:void 0,_setupCryptoModule:t,get instanceId(){if(e.useInstanceId)return this._instanceId},getInstanceId(){if(e.useInstanceId)return this._instanceId},getUserId(){return this.userId},setUserId(e){if(!e||"string"!=typeof e||0===e.trim().length)throw new Error("Missing or invalid userId parameter. Provide a valid string userId");this.userId=e},logger(){return this._loggerManager},getAuthKey(){return this.authKey},setAuthKey(e){this.authKey=e},getFilterExpression(){return this.filterExpression},setFilterExpression(e){this.filterExpression=e},getCipherKey(){return this._cipherKey},setCipherKey(t){this._cipherKey=t,t||!this._cryptoModule?t&&this._setupCryptoModule&&(this._cryptoModule=this._setupCryptoModule({cipherKey:t,useRandomIVs:e.useRandomIVs,customEncrypt:this.getCustomEncrypt(),customDecrypt:this.getCustomDecrypt(),logger:this.logger()})):this._cryptoModule=void 0},getCryptoModule(){return this._cryptoModule},getUseRandomIVs:()=>e.useRandomIVs,isSharedWorkerEnabled:()=>"Web"===e.sdkFamily&&e.subscriptionWorkerUrl,getKeepPresenceChannelsInPresenceRequests:()=>"Web"===e.sdkFamily&&e.subscriptionWorkerUrl,setPresenceTimeout(e){this.heartbeatInterval=e/2-1,this.presenceTimeout=e},getPresenceTimeout(){return this.presenceTimeout},getHeartbeatInterval(){return this.heartbeatInterval},setHeartbeatInterval(e){this.heartbeatInterval=e},getTransactionTimeout(){return this.transactionalRequestTimeout},getSubscribeTimeout(){return this.subscribeRequestTimeout},getFileTimeout(){return this.fileRequestTimeout},get PubNubFile(){return e.PubNubFile},get version(){return"9.9.0"},getVersion(){return this.version},_addPnsdkSuffix(e,t){this._pnsdkSuffix[e]=`${t}`},_getPnsdkSuffix(e){const t=Object.values(this._pnsdkSuffix).join(e);return t.length>0?e+t:""},getUUID(){return this.getUserId()},setUUID(e){this.setUserId(e)},getCustomEncrypt:()=>e.customEncrypt,getCustomDecrypt:()=>e.customDecrypt});return e.cipherKey?(o.warn("Configuration","'cipherKey' is deprecated. Use 'cryptoModule' instead."),u.setCipherKey(e.cipherKey)):c&&(u._cryptoModule=c),u},ne=(e,t)=>{const s=e?"https://":"http://";return"string"==typeof t?`${s}${t}`:`${s}${t[Math.floor(Math.random()*t.length)]}`},re=e=>{let t=2166136261;for(let s=0;s>>0;return t.toString(16).padStart(8,"0")};class ie{constructor(e){this.cbor=e}setToken(e){e&&e.length>0?this.token=e:this.token=void 0}getToken(){return this.token}parseToken(e){const t=this.cbor.decodeToken(e);if(void 0!==t){const e=t.res.uuid?Object.keys(t.res.uuid):[],s=Object.keys(t.res.chan),n=Object.keys(t.res.grp),r=t.pat.uuid?Object.keys(t.pat.uuid):[],i=Object.keys(t.pat.chan),a=Object.keys(t.pat.grp),o={version:t.v,timestamp:t.t,ttl:t.ttl,authorized_uuid:t.uuid,signature:t.sig},c=e.length>0,u=s.length>0,l=n.length>0;if(c||u||l){if(o.resources={},c){const s=o.resources.uuids={};e.forEach((e=>s[e]=this.extractPermissions(t.res.uuid[e])))}if(u){const e=o.resources.channels={};s.forEach((s=>e[s]=this.extractPermissions(t.res.chan[s])))}if(l){const e=o.resources.groups={};n.forEach((s=>e[s]=this.extractPermissions(t.res.grp[s])))}}const h=r.length>0,d=i.length>0,p=a.length>0;if(h||d||p){if(o.patterns={},h){const e=o.patterns.uuids={};r.forEach((s=>e[s]=this.extractPermissions(t.pat.uuid[s])))}if(d){const e=o.patterns.channels={};i.forEach((s=>e[s]=this.extractPermissions(t.pat.chan[s])))}if(p){const e=o.patterns.groups={};a.forEach((s=>e[s]=this.extractPermissions(t.pat.grp[s])))}}return t.meta&&Object.keys(t.meta).length>0&&(o.meta=t.meta),o}}extractPermissions(e){const t={read:!1,write:!1,manage:!1,delete:!1,get:!1,update:!1,join:!1};return 128&~e||(t.join=!0),64&~e||(t.update=!0),32&~e||(t.get=!0),8&~e||(t.delete=!0),4&~e||(t.manage=!0),2&~e||(t.write=!0),1&~e||(t.read=!0),t}}var ae;!function(e){e.GET="GET",e.POST="POST",e.PATCH="PATCH",e.DELETE="DELETE",e.LOCAL="LOCAL"}(ae||(ae={}));class oe{constructor(e,t,s,n){this.publishKey=e,this.secretKey=t,this.hasher=s,this.logger=n}signature(e){const t=e.path.startsWith("/publish")?ae.GET:e.method;let s=`${t}\n${this.publishKey}\n${e.path}\n${this.queryParameters(e.queryParameters)}\n`;if(t===ae.POST||t===ae.PATCH){const t=e.body;let n;t&&t instanceof ArrayBuffer?n=oe.textDecoder.decode(t):t&&"object"!=typeof t&&(n=t),n&&(s+=n)}return this.logger.trace("RequestSignature",(()=>({messageType:"text",message:`Request signature input:\n${s}`}))),`v2.${this.hasher(s,this.secretKey)}`.replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}queryParameters(e){return Object.keys(e).sort().map((t=>{const s=e[t];return Array.isArray(s)?s.sort().map((e=>`${t}=${R(e)}`)).join("&"):`${t}=${R(s)}`})).join("&")}}oe.textDecoder=new TextDecoder("utf-8");class ce{constructor(e){this.configuration=e;const{clientConfiguration:{keySet:t},shaHMAC:s}=e;t.secretKey&&s&&(this.signatureGenerator=new oe(t.publishKey,t.secretKey,s,this.logger))}get logger(){return this.configuration.clientConfiguration.logger()}makeSendable(e){const t=this.configuration.clientConfiguration.retryConfiguration,s=this.configuration.transport;if(void 0!==t){let n,r,i=!1,a=0;const o={abort:e=>{i=!0,n&&clearTimeout(n),r&&r.abort(e)}};return[new Promise(((o,c)=>{const u=()=>{if(i)return;const[l,d]=s.makeSendable(this.request(e));r=d;const p=(s,r)=>{const i=!r||r.category!==h.PNCancelledCategory,l=!s||s.status>=400;let d=-1;i&&l&&t.shouldRetry(e,s,null==r?void 0:r.category,a+1)&&(d=t.getDelay(a,s)),d>0?(a++,this.logger.warn("PubNubMiddleware",`HTTP request retry #${a} in ${d}ms.`),n=setTimeout((()=>u()),d)):s?o(s):r&&c(r)};l.then((e=>p(e))).catch((e=>p(void 0,e)))};u()})),r?o:void 0]}return s.makeSendable(this.request(e))}request(e){var t;const{clientConfiguration:s}=this.configuration;return(e=this.configuration.transport.request(e)).queryParameters||(e.queryParameters={}),s.useInstanceId&&(e.queryParameters.instanceid=s.getInstanceId()),e.queryParameters.uuid||(e.queryParameters.uuid=s.userId),s.useRequestId&&(e.queryParameters.requestid=e.identifier),e.queryParameters.pnsdk=this.generatePNSDK(),null!==(t=e.origin)&&void 0!==t||(e.origin=s.origin),this.authenticateRequest(e),this.signRequest(e),e}authenticateRequest(e){var t;if(e.path.startsWith("/v2/auth/")||e.path.startsWith("/v3/pam/")||e.path.startsWith("/time"))return;const{clientConfiguration:s,tokenManager:n}=this.configuration,r=null!==(t=n&&n.getToken())&&void 0!==t?t:s.authKey;r&&(e.queryParameters.auth=r)}signRequest(e){this.signatureGenerator&&!e.path.startsWith("/time")&&(e.queryParameters.timestamp=String(Math.floor((new Date).getTime()/1e3)),e.queryParameters.signature=this.signatureGenerator.signature(e))}generatePNSDK(){const{clientConfiguration:e}=this.configuration;if(e.sdkName)return e.sdkName;let t=`PubNub-JS-${e.sdkFamily}`;e.partnerId&&(t+=`-${e.partnerId}`),t+=`/${e.getVersion()}`;const s=e._getPnsdkSuffix(" ");return s.length>0&&(t+=s),t}}class ue{constructor(e,t="fetch"){this.logger=e,this.transport=t,e.debug("WebTransport",`Create with configuration:\n - transport: ${t}`),"fetch"!==t||window&&window.fetch||(e.warn("WebTransport",`'${t}' not supported in this browser. Fallback to the 'xhr' transport.`),this.transport="xhr"),"fetch"===this.transport&&(ue.originalFetch=fetch.bind(window),this.isFetchMonkeyPatched()&&(ue.originalFetch=ue.getOriginalFetch(),e.warn("WebTransport","Native Web Fetch API 'fetch' function monkey patched."),this.isFetchMonkeyPatched(ue.originalFetch)?e.warn("WebTransport","Unable receive native Web Fetch API. There can be issues with subscribe long-poll cancellation"):e.info("WebTransport","Use native Web Fetch API 'fetch' implementation from iframe as APM workaround.")))}makeSendable(e){const t=new AbortController,s={abortController:t,abort:e=>{t.signal.aborted||(this.logger.trace("WebTransport",`On-demand request aborting: ${e}`),t.abort(e))}};return[this.webTransportRequestFromTransportRequest(e).then((t=>(this.logger.debug("WebTransport",(()=>({messageType:"network-request",message:e}))),this.sendRequest(t,s).then((e=>e.arrayBuffer().then((t=>[e,t])))).then((e=>{const s=e[1].byteLength>0?e[1]:void 0,{status:n,headers:r}=e[0],i={};r.forEach(((e,t)=>i[t]=e.toLowerCase()));const a={status:n,url:t.url,headers:i,body:s};if(this.logger.debug("WebTransport",(()=>({messageType:"network-response",message:a}))),n>=400)throw I.create(a);return a})).catch((t=>{const s=("string"==typeof t?t:t.message).toLowerCase();let n="string"==typeof t?new Error(t):t;throw s.includes("timeout")?this.logger.warn("WebTransport",(()=>({messageType:"network-request",message:e,details:"Timeout",canceled:!0}))):s.includes("cancel")||s.includes("abort")?(this.logger.debug("WebTransport",(()=>({messageType:"network-request",message:e,details:"Aborted",canceled:!0}))),n=new Error("Aborted"),n.name="AbortError"):s.includes("network")?this.logger.warn("WebTransport",(()=>({messageType:"network-request",message:e,details:"Network error",failed:!0}))):this.logger.warn("WebTransport",(()=>({messageType:"network-request",message:e,details:I.create(n).message,failed:!0}))),I.create(n)}))))),s]}request(e){return e}sendRequest(e,t){return i(this,void 0,void 0,(function*(){return"fetch"===this.transport?this.sendFetchRequest(e,t):this.sendXHRRequest(e,t)}))}sendFetchRequest(e,t){return i(this,void 0,void 0,(function*(){let s;const n=new Promise(((n,r)=>{s=setTimeout((()=>{clearTimeout(s),r(new Error("Request timeout")),t.abort("Cancel because of timeout")}),1e3*e.timeout)})),r=new Request(e.url,{method:e.method,headers:e.headers,redirect:"follow",body:e.body});return Promise.race([ue.originalFetch(r,{signal:t.abortController.signal,credentials:"omit",cache:"no-cache"}).then((e=>(s&&clearTimeout(s),e))),n])}))}sendXHRRequest(e,t){return i(this,void 0,void 0,(function*(){return new Promise(((s,n)=>{var r;const i=new XMLHttpRequest;i.open(e.method,e.url,!0);let a=!1;i.responseType="arraybuffer",i.timeout=1e3*e.timeout,t.abortController.signal.onabort=()=>{i.readyState!=XMLHttpRequest.DONE&&i.readyState!=XMLHttpRequest.UNSENT&&(a=!0,i.abort())},Object.entries(null!==(r=e.headers)&&void 0!==r?r:{}).forEach((([e,t])=>i.setRequestHeader(e,t))),i.onabort=()=>{n(new Error("Aborted"))},i.ontimeout=()=>{n(new Error("Request timeout"))},i.onerror=()=>{if(!a){const t=this.transportResponseFromXHR(e.url,i);n(new Error(I.create(t).message))}},i.onload=()=>{const e=new Headers;i.getAllResponseHeaders().split("\r\n").forEach((t=>{const[s,n]=t.split(": ");s.length>1&&n.length>1&&e.append(s,n)})),s(new Response(i.response,{status:i.status,headers:e,statusText:i.statusText}))},i.send(e.body)}))}))}webTransportRequestFromTransportRequest(e){return i(this,void 0,void 0,(function*(){let t,s=e.path;if(e.formData&&e.formData.length>0){e.queryParameters={};const s=e.body,n=new FormData;for(const{key:t,value:s}of e.formData)n.append(t,s);try{const e=yield s.toArrayBuffer();n.append("file",new Blob([e],{type:"application/octet-stream"}),s.name)}catch(e){this.logger.warn("WebTransport",(()=>({messageType:"error",message:e})));try{const e=yield s.toFileUri();n.append("file",e,s.name)}catch(e){this.logger.error("WebTransport",(()=>({messageType:"error",message:e})))}}t=n}else if(e.body&&("string"==typeof e.body||e.body instanceof ArrayBuffer))if(e.compressible&&"undefined"!=typeof CompressionStream){const s="string"==typeof e.body?ue.encoder.encode(e.body):e.body,n=s.byteLength,r=new ReadableStream({start(e){e.enqueue(s),e.close()}});t=yield new Response(r.pipeThrough(new CompressionStream("deflate"))).arrayBuffer(),this.logger.trace("WebTransport",(()=>{const e=t.byteLength,s=(e/n).toFixed(2);return{messageType:"text",message:`Body of ${n} bytes, compressed by ${s}x to ${e} bytes.`}}))}else t=e.body;return e.queryParameters&&0!==Object.keys(e.queryParameters).length&&(s=`${s}?${L(e.queryParameters)}`),{url:`${e.origin}${s}`,method:e.method,headers:e.headers,timeout:e.timeout,body:t}}))}isFetchMonkeyPatched(e){return!(null!=e?e:fetch).toString().includes("[native code]")&&"fetch"!==fetch.name}transportResponseFromXHR(e,t){const s=t.getAllResponseHeaders().split("\n"),n={};for(const e of s){const[t,s]=e.trim().split(":");t&&s&&(n[t.toLowerCase()]=s.trim())}return{status:t.status,url:e,headers:n,body:t.response}}static getOriginalFetch(){let e=document.querySelector('iframe[name="pubnub-context-unpatched-fetch"]');return e||(e=document.createElement("iframe"),e.style.display="none",e.name="pubnub-context-unpatched-fetch",e.src="about:blank",document.body.appendChild(e)),e.contentWindow?e.contentWindow.fetch.bind(e.contentWindow):fetch}}ue.encoder=new TextEncoder,ue.decoder=new TextDecoder;class le{constructor(e){this.params=e,this.requestIdentifier=te.createUUID(),this._cancellationController=null}get cancellationController(){return this._cancellationController}set cancellationController(e){this._cancellationController=e}abort(e){this&&this.cancellationController&&this.cancellationController.abort(e)}operation(){throw Error("Should be implemented by subclass.")}validate(){}parse(e){return i(this,void 0,void 0,(function*(){return this.deserializeResponse(e)}))}request(){var e,t,s,n,r,i;const a={method:null!==(t=null===(e=this.params)||void 0===e?void 0:e.method)&&void 0!==t?t:ae.GET,path:this.path,queryParameters:this.queryParameters,cancellable:null!==(n=null===(s=this.params)||void 0===s?void 0:s.cancellable)&&void 0!==n&&n,compressible:null!==(i=null===(r=this.params)||void 0===r?void 0:r.compressible)&&void 0!==i&&i,timeout:10,identifier:this.requestIdentifier},o=this.headers;if(o&&(a.headers=o),a.method===ae.POST||a.method===ae.PATCH){const[e,t]=[this.body,this.formData];t&&(a.formData=t),e&&(a.body=e)}return a}get headers(){var e,t;return Object.assign({"Accept-Encoding":"gzip, deflate"},null!==(t=null===(e=this.params)||void 0===e?void 0:e.compressible)&&void 0!==t&&t?{"Content-Encoding":"deflate"}:{})}get path(){throw Error("`path` getter should be implemented by subclass.")}get queryParameters(){return{}}get formData(){}get body(){}deserializeResponse(e){const t=le.decoder.decode(e.body),s=e.headers["content-type"];let n;if(!s||-1===s.indexOf("javascript")&&-1===s.indexOf("json"))throw new d("Service response error, check status for details",g(t,e.status));try{n=JSON.parse(t)}catch(s){throw console.error("Error parsing JSON response:",s),new d("Service response error, check status for details",g(t,e.status))}if("status"in n&&"number"==typeof n.status&&n.status>=400)throw I.create(e);return n}}le.decoder=new TextDecoder;var he;!function(e){e[e.Presence=-2]="Presence",e[e.Message=-1]="Message",e[e.Signal=1]="Signal",e[e.AppContext=2]="AppContext",e[e.MessageAction=3]="MessageAction",e[e.Files=4]="Files"}(he||(he={}));class de extends le{constructor(e){var t,s,n,r,i,a;super({cancellable:!0}),this.parameters=e,null!==(t=(r=this.parameters).withPresence)&&void 0!==t||(r.withPresence=false),null!==(s=(i=this.parameters).channelGroups)&&void 0!==s||(i.channelGroups=[]),null!==(n=(a=this.parameters).channels)&&void 0!==n||(a.channels=[])}operation(){return M.PNSubscribeOperation}validate(){const{keySet:{subscribeKey:e},channels:t,channelGroups:s}=this.parameters;return e?t||s?void 0:"`channels` and `channelGroups` both should not be empty":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){let t,s;try{s=le.decoder.decode(e.body);t=JSON.parse(s)}catch(e){console.error("Error parsing JSON response:",e)}if(!t)throw new d("Service response error, check status for details",g(s,e.status));const n=t.m.filter((e=>{const t=void 0===e.b?e.c:e.b;return this.parameters.channels&&this.parameters.channels.includes(t)||this.parameters.channelGroups&&this.parameters.channelGroups.includes(t)})).map((e=>{let{e:t}=e;return null!=t||(t=e.c.endsWith("-pnpres")?he.Presence:he.Message),t!=he.Signal&&"string"==typeof e.d?t==he.Message?{type:he.Message,data:this.messageFromEnvelope(e)}:{type:he.Files,data:this.fileFromEnvelope(e)}:t==he.Message?{type:he.Message,data:this.messageFromEnvelope(e)}:t===he.Presence?{type:he.Presence,data:this.presenceEventFromEnvelope(e)}:t==he.Signal?{type:he.Signal,data:this.signalFromEnvelope(e)}:t===he.AppContext?{type:he.AppContext,data:this.appContextFromEnvelope(e)}:t===he.MessageAction?{type:he.MessageAction,data:this.messageActionFromEnvelope(e)}:{type:he.Files,data:this.fileFromEnvelope(e)}}));return{cursor:{timetoken:t.t.t,region:t.t.r},messages:n}}))}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{accept:"text/javascript"})}presenceEventFromEnvelope(e){var t;const{d:s}=e,[n,r]=this.subscriptionChannelFromEnvelope(e),i=n.replace("-pnpres",""),a=null!==r?i:null,o=null!==r?r:i;return"string"!=typeof s&&("data"in s?(s.state=s.data,delete s.data):"action"in s&&"interval"===s.action&&(s.hereNowRefresh=null!==(t=s.here_now_refresh)&&void 0!==t&&t,delete s.here_now_refresh)),Object.assign({channel:i,subscription:r,actualChannel:a,subscribedChannel:o,timetoken:e.p.t},s)}messageFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),[n,r]=this.decryptedData(e.d),i={channel:t,subscription:s,actualChannel:null!==s?t:null,subscribedChannel:null!==s?s:t,timetoken:e.p.t,publisher:e.i,message:n};return e.u&&(i.userMetadata=e.u),e.cmt&&(i.customMessageType=e.cmt),r&&(i.error=r),i}signalFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),n={channel:t,subscription:s,timetoken:e.p.t,publisher:e.i,message:e.d};return e.u&&(n.userMetadata=e.u),e.cmt&&(n.customMessageType=e.cmt),n}messageActionFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),n=e.d;return{channel:t,subscription:s,timetoken:e.p.t,publisher:e.i,event:n.event,data:Object.assign(Object.assign({},n.data),{uuid:e.i})}}appContextFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),n=e.d;return{channel:t,subscription:s,timetoken:e.p.t,message:n}}fileFromEnvelope(e){const[t,s]=this.subscriptionChannelFromEnvelope(e),[n,r]=this.decryptedData(e.d);let i=r;const a={channel:t,subscription:s,timetoken:e.p.t,publisher:e.i};return e.u&&(a.userMetadata=e.u),n?"string"==typeof n?null!=i||(i="Unexpected file information payload data type."):(a.message=n.message,n.file&&(a.file={id:n.file.id,name:n.file.name,url:this.parameters.getFileUrl({id:n.file.id,name:n.file.name,channel:t})})):null!=i||(i="File information payload is missing."),e.cmt&&(a.customMessageType=e.cmt),i&&(a.error=i),a}subscriptionChannelFromEnvelope(e){return[e.c,void 0===e.b?e.c:e.b]}decryptedData(e){if(!this.parameters.crypto||"string"!=typeof e)return[e,void 0];let t,s;try{const s=this.parameters.crypto.decrypt(e);t=s instanceof ArrayBuffer?JSON.parse(pe.decoder.decode(s)):s}catch(e){t=null,s=`Error while decrypting message content: ${e.message}`}return[null!=t?t:e,s]}}class pe extends de{get path(){var e;const{keySet:{subscribeKey:t},channels:s}=this.parameters;return`/v2/subscribe/${t}/${$(null!==(e=null==s?void 0:s.sort())&&void 0!==e?e:[],",")}/0`}get queryParameters(){const{channelGroups:e,filterExpression:t,heartbeat:s,state:n,timetoken:r,region:i,onDemand:a}=this.parameters,o={};return a&&(o["on-demand"]=1),e&&e.length>0&&(o["channel-group"]=e.sort().join(",")),t&&t.length>0&&(o["filter-expr"]=t),s&&(o.heartbeat=s),n&&Object.keys(n).length>0&&(o.state=JSON.stringify(n)),void 0!==r&&"string"==typeof r?r.length>0&&"0"!==r&&(o.tt=r):void 0!==r&&r>0&&(o.tt=r),i&&(o.tr=i),o}}class ge{constructor(){this.hasListeners=!1,this.listeners=[{count:-1,listener:{}}]}set onStatus(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"status"})}set onMessage(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"message"})}set onPresence(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"presence"})}set onSignal(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"signal"})}set onObjects(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"objects"})}set onMessageAction(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"messageAction"})}set onFile(e){this.updateTypeOrObjectListener({add:!!e,listener:e,type:"file"})}handleEvent(e){if(this.hasListeners)if(e.type===he.Message)this.announce("message",e.data);else if(e.type===he.Signal)this.announce("signal",e.data);else if(e.type===he.Presence)this.announce("presence",e.data);else if(e.type===he.AppContext){const{data:t}=e,{message:s}=t;if(this.announce("objects",t),"uuid"===s.type){const{message:e,channel:n}=t,i=r(t,["message","channel"]),{event:a,type:o}=s,c=r(s,["event","type"]),u=Object.assign(Object.assign({},i),{spaceId:n,message:Object.assign(Object.assign({},c),{event:"set"===a?"updated":"removed",type:"user"})});this.announce("user",u)}else if("channel"===s.type){const{message:e,channel:n}=t,i=r(t,["message","channel"]),{event:a,type:o}=s,c=r(s,["event","type"]),u=Object.assign(Object.assign({},i),{spaceId:n,message:Object.assign(Object.assign({},c),{event:"set"===a?"updated":"removed",type:"space"})});this.announce("space",u)}else if("membership"===s.type){const{message:e,channel:n}=t,i=r(t,["message","channel"]),{event:a,data:o}=s,c=r(s,["event","data"]),{uuid:u,channel:l}=o,h=r(o,["uuid","channel"]),d=Object.assign(Object.assign({},i),{spaceId:n,message:Object.assign(Object.assign({},c),{event:"set"===a?"updated":"removed",data:Object.assign(Object.assign({},h),{user:u,space:l})})});this.announce("membership",d)}}else e.type===he.MessageAction?this.announce("messageAction",e.data):e.type===he.Files&&this.announce("file",e.data)}handleStatus(e){this.hasListeners&&this.announce("status",e)}addListener(e){this.updateTypeOrObjectListener({add:!0,listener:e})}removeListener(e){this.updateTypeOrObjectListener({add:!1,listener:e})}removeAllListeners(){this.listeners=[{count:-1,listener:{}}],this.hasListeners=!1}updateTypeOrObjectListener(e){if(e.type)"function"==typeof e.listener?this.listeners[0].listener[e.type]=e.listener:delete this.listeners[0].listener[e.type];else if(e.listener&&"function"!=typeof e.listener){let t,s=!1;for(t of this.listeners)if(t.listener===e.listener){e.add?(t.count++,s=!0):(t.count--,0===t.count&&this.listeners.splice(this.listeners.indexOf(t),1));break}e.add&&!s&&this.listeners.push({count:1,listener:e.listener})}this.hasListeners=this.listeners.length>1||Object.keys(this.listeners[0]).length>0}announce(e,t){this.listeners.forEach((({listener:s})=>{const n=s[e];n&&n(t)}))}}class be{constructor(e){this.time=e}onReconnect(e){this.callback=e}startPolling(){this.timeTimer=setInterval((()=>this.callTime()),3e3)}stopPolling(){this.timeTimer&&clearInterval(this.timeTimer),this.timeTimer=null}callTime(){this.time((e=>{e.error||(this.stopPolling(),this.callback&&this.callback())}))}}class ye{constructor(e){this.config=e,e.logger().debug("DedupingManager",(()=>({messageType:"object",message:{maximumCacheSize:e.maximumCacheSize},details:"Create with configuration:"}))),this.maximumCacheSize=e.maximumCacheSize,this.hashHistory=[]}getKey(e){var t;return`${e.timetoken}-${this.hashCode(JSON.stringify(null!==(t=e.message)&&void 0!==t?t:"")).toString()}`}isDuplicate(e){return this.hashHistory.includes(this.getKey(e))}addEntry(e){this.hashHistory.length>=this.maximumCacheSize&&this.hashHistory.shift(),this.hashHistory.push(this.getKey(e))}clearHistory(){this.hashHistory=[]}hashCode(e){let t=0;if(0===e.length)return t;for(let s=0;s{this.pendingChannelSubscriptions.add(e),this.channels[e]={},r&&(this.presenceChannels[e]={}),(i||this.configuration.getHeartbeatInterval())&&(this.heartbeatChannels[e]={})})),null==s||s.forEach((e=>{this.pendingChannelGroupSubscriptions.add(e),this.channelGroups[e]={},r&&(this.presenceChannelGroups[e]={}),(i||this.configuration.getHeartbeatInterval())&&(this.heartbeatChannelGroups[e]={})})),this.subscriptionStatusAnnounced=!1,this.reconnect()}unsubscribe(e,t=!1){let{channels:s,channelGroups:n}=e;const i=new Set,a=new Set;if(null==s||s.forEach((e=>{e in this.channels&&(delete this.channels[e],a.add(e),e in this.heartbeatChannels&&delete this.heartbeatChannels[e]),e in this.presenceState&&delete this.presenceState[e],e in this.presenceChannels&&(delete this.presenceChannels[e],a.add(e))})),null==n||n.forEach((e=>{e in this.channelGroups&&(delete this.channelGroups[e],i.add(e),e in this.heartbeatChannelGroups&&delete this.heartbeatChannelGroups[e]),e in this.presenceState&&delete this.presenceState[e],e in this.presenceChannelGroups&&(delete this.presenceChannelGroups[e],i.add(e))})),0===a.size&&0===i.size)return;const o=this.lastTimetoken,c=this.currentTimetoken;0===Object.keys(this.channels).length&&0===Object.keys(this.presenceChannels).length&&0===Object.keys(this.channelGroups).length&&0===Object.keys(this.presenceChannelGroups).length&&(this.lastTimetoken="0",this.currentTimetoken="0",this.referenceTimetoken=null,this.storedTimetoken=null,this.region=null,this.reconnectionManager.stopPolling()),this.reconnect(!0),!1!==this.configuration.suppressLeaveEvents||t||(n=Array.from(i),s=Array.from(a),this.leaveCall({channels:s,channelGroups:n},(e=>{const{error:t}=e,i=r(e,["error"]);let a;t&&(e.errorData&&"object"==typeof e.errorData&&"message"in e.errorData&&"string"==typeof e.errorData.message?a=e.errorData.message:"message"in e&&"string"==typeof e.message&&(a=e.message)),this.emitStatus(Object.assign(Object.assign({},i),{error:null!=a&&a,affectedChannels:s,affectedChannelGroups:n,currentTimetoken:c,lastTimetoken:o}))})))}unsubscribeAll(e=!1){this.disconnectedWhileHandledEvent=!0,this.unsubscribe({channels:this.subscribedChannels,channelGroups:this.subscribedChannelGroups},e)}startSubscribeLoop(e=!1){this.disconnectedWhileHandledEvent=!1,this.stopSubscribeLoop();const t=[...Object.keys(this.channelGroups)],s=[...Object.keys(this.channels)];Object.keys(this.presenceChannelGroups).forEach((e=>t.push(`${e}-pnpres`))),Object.keys(this.presenceChannels).forEach((e=>s.push(`${e}-pnpres`))),0===s.length&&0===t.length||(this.subscribeCall(Object.assign(Object.assign(Object.assign({channels:s,channelGroups:t,state:this.presenceState,heartbeat:this.configuration.getPresenceTimeout(),timetoken:this.currentTimetoken},null!==this.region?{region:this.region}:{}),this.configuration.filterExpression?{filterExpression:this.configuration.filterExpression}:{}),{onDemand:!this.subscriptionStatusAnnounced||e}),((e,t)=>{this.processSubscribeResponse(e,t)})),!e&&this.configuration.useSmartHeartbeat&&this.startHeartbeatTimer())}stopSubscribeLoop(){this._subscribeAbort&&(this._subscribeAbort(),this._subscribeAbort=null)}processSubscribeResponse(e,t){if(e.error){if("object"==typeof e.errorData&&"name"in e.errorData&&"AbortError"===e.errorData.name||e.category===h.PNCancelledCategory)return;return void(e.category===h.PNTimeoutCategory?this.startSubscribeLoop():e.category===h.PNNetworkIssuesCategory||e.category===h.PNMalformedResponseCategory?(this.disconnect(),e.error&&this.configuration.autoNetworkDetection&&this.isOnline&&(this.isOnline=!1,this.emitStatus({category:h.PNNetworkDownCategory})),this.reconnectionManager.onReconnect((()=>{this.configuration.autoNetworkDetection&&!this.isOnline&&(this.isOnline=!0,this.emitStatus({category:h.PNNetworkUpCategory})),this.reconnect(),this.subscriptionStatusAnnounced=!0;const t={category:h.PNReconnectedCategory,operation:e.operation,lastTimetoken:this.lastTimetoken,currentTimetoken:this.currentTimetoken};this.emitStatus(t)})),this.reconnectionManager.startPolling(),this.emitStatus(Object.assign(Object.assign({},e),{category:h.PNNetworkIssuesCategory}))):e.category===h.PNBadRequestCategory?(this.stopHeartbeatTimer(),this.emitStatus(e)):this.emitStatus(e))}if(this.referenceTimetoken=K(t.cursor.timetoken,this.storedTimetoken),this.storedTimetoken?(this.currentTimetoken=this.storedTimetoken,this.storedTimetoken=null):(this.lastTimetoken=this.currentTimetoken,this.currentTimetoken=t.cursor.timetoken),!this.subscriptionStatusAnnounced){const t={category:h.PNConnectedCategory,operation:e.operation,affectedChannels:Array.from(this.pendingChannelSubscriptions),subscribedChannels:this.subscribedChannels,affectedChannelGroups:Array.from(this.pendingChannelGroupSubscriptions),lastTimetoken:this.lastTimetoken,currentTimetoken:this.currentTimetoken};this.subscriptionStatusAnnounced=!0,this.emitStatus(t),this.pendingChannelGroupSubscriptions.clear(),this.pendingChannelSubscriptions.clear()}const{messages:s}=t,{requestMessageCountThreshold:n,dedupeOnSubscribe:r}=this.configuration;n&&s.length>=n&&this.emitStatus({category:h.PNRequestMessageCountExceededCategory,operation:e.operation});try{const e={timetoken:this.currentTimetoken,region:this.region?this.region:void 0};this.configuration.logger().debug("SubscriptionManager",(()=>({messageType:"object",message:s.map((e=>{const t=e.type===he.Message||e.type===he.Signal?B(e.data.message):void 0;return t?{type:e.type,data:Object.assign(Object.assign({},e.data),{pn_mfp:t})}:e})),details:"Received events:"}))),s.forEach((t=>{if(r&&"message"in t.data&&"timetoken"in t.data){if(this.dedupingManager.isDuplicate(t.data))return void this.configuration.logger().warn("SubscriptionManager",(()=>({messageType:"object",message:t.data,details:"Duplicate message detected (skipped):"})));this.dedupingManager.addEntry(t.data)}this.emitEvent(e,t)}))}catch(e){const t={error:!0,category:h.PNUnknownCategory,errorData:e,statusCode:0};this.emitStatus(t)}this.region=t.cursor.region,this.disconnectedWhileHandledEvent?this.disconnectedWhileHandledEvent=!1:this.startSubscribeLoop()}setState(e){const{state:t,channels:s,channelGroups:n}=e;null==s||s.forEach((e=>e in this.channels&&(this.presenceState[e]=t))),null==n||n.forEach((e=>e in this.channelGroups&&(this.presenceState[e]=t)))}changePresence(e){const{connected:t,channels:s,channelGroups:n}=e;t?(null==s||s.forEach((e=>this.heartbeatChannels[e]={})),null==n||n.forEach((e=>this.heartbeatChannelGroups[e]={}))):(null==s||s.forEach((e=>{e in this.heartbeatChannels&&delete this.heartbeatChannels[e]})),null==n||n.forEach((e=>{e in this.heartbeatChannelGroups&&delete this.heartbeatChannelGroups[e]})),!1===this.configuration.suppressLeaveEvents&&this.leaveCall({channels:s,channelGroups:n},(e=>this.emitStatus(e)))),this.reconnect()}startHeartbeatTimer(){this.stopHeartbeatTimer();const e=this.configuration.getHeartbeatInterval();e&&0!==e&&(this.configuration.useSmartHeartbeat||this.sendHeartbeat(),this.heartbeatTimer=setInterval((()=>this.sendHeartbeat()),1e3*e))}stopHeartbeatTimer(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}sendHeartbeat(){const e=Object.keys(this.heartbeatChannelGroups),t=Object.keys(this.heartbeatChannels);0===t.length&&0===e.length||this.heartbeatCall({channels:t,channelGroups:e,heartbeat:this.configuration.getPresenceTimeout(),state:this.presenceState},(e=>{e.error&&this.configuration.announceFailedHeartbeats&&this.emitStatus(e),e.error&&this.configuration.autoNetworkDetection&&this.isOnline&&(this.isOnline=!1,this.disconnect(),this.emitStatus({category:h.PNNetworkDownCategory}),this.reconnect()),!e.error&&this.configuration.announceSuccessfulHeartbeats&&this.emitStatus(e)}))}}class fe{constructor(e,t,s){this._payload=e,this.setDefaultPayloadStructure(),this.title=t,this.body=s}get payload(){return this._payload}set title(e){this._title=e}set subtitle(e){this._subtitle=e}set body(e){this._body=e}set badge(e){this._badge=e}set sound(e){this._sound=e}setDefaultPayloadStructure(){}toObject(){return{}}}class ve extends fe{constructor(){super(...arguments),this._apnsPushType="apns",this._isSilent=!1}get payload(){return this._payload}set configurations(e){e&&e.length&&(this._configurations=e)}get notification(){return this.payload.aps}get title(){return this._title}set title(e){e&&e.length&&(this.payload.aps.alert.title=e,this._title=e)}get subtitle(){return this._subtitle}set subtitle(e){e&&e.length&&(this.payload.aps.alert.subtitle=e,this._subtitle=e)}get body(){return this._body}set body(e){e&&e.length&&(this.payload.aps.alert.body=e,this._body=e)}get badge(){return this._badge}set badge(e){null!=e&&(this.payload.aps.badge=e,this._badge=e)}get sound(){return this._sound}set sound(e){e&&e.length&&(this.payload.aps.sound=e,this._sound=e)}set silent(e){this._isSilent=e}setDefaultPayloadStructure(){this.payload.aps={alert:{}}}toObject(){const e=Object.assign({},this.payload),{aps:t}=e;let{alert:s}=t;if(this._isSilent&&(t["content-available"]=1),"apns2"===this._apnsPushType){if(!this._configurations||!this._configurations.length)throw new ReferenceError("APNS2 configuration is missing");const t=[];this._configurations.forEach((e=>{t.push(this.objectFromAPNS2Configuration(e))})),t.length&&(e.pn_push=t)}return s&&Object.keys(s).length||delete t.alert,this._isSilent&&(delete t.alert,delete t.badge,delete t.sound,s={}),this._isSilent||s&&Object.keys(s).length?e:null}objectFromAPNS2Configuration(e){if(!e.targets||!e.targets.length)throw new ReferenceError("At least one APNS2 target should be provided");const{collapseId:t,expirationDate:s}=e,n={auth_method:"token",targets:e.targets.map((e=>this.objectFromAPNSTarget(e))),version:"v2"};return t&&t.length&&(n.collapse_id=t),s&&(n.expiration=s.toISOString()),n}objectFromAPNSTarget(e){if(!e.topic||!e.topic.length)throw new TypeError("Target 'topic' undefined.");const{topic:t,environment:s="development",excludedDevices:n=[]}=e,r={topic:t,environment:s};return n.length&&(r.excluded_devices=n),r}}class Se extends fe{get payload(){return this._payload}get notification(){return this.payload.notification}get data(){return this.payload.data}get title(){return this._title}set title(e){e&&e.length&&(this.payload.notification.title=e,this._title=e)}get body(){return this._body}set body(e){e&&e.length&&(this.payload.notification.body=e,this._body=e)}get sound(){return this._sound}set sound(e){e&&e.length&&(this.payload.notification.sound=e,this._sound=e)}get icon(){return this._icon}set icon(e){e&&e.length&&(this.payload.notification.icon=e,this._icon=e)}get tag(){return this._tag}set tag(e){e&&e.length&&(this.payload.notification.tag=e,this._tag=e)}set silent(e){this._isSilent=e}setDefaultPayloadStructure(){this.payload.notification={},this.payload.data={}}toObject(){let e=Object.assign({},this.payload.data),t=null;const s={};if(Object.keys(this.payload).length>2){const t=r(this.payload,["notification","data"]);e=Object.assign(Object.assign({},e),t)}return this._isSilent?e.notification=this.payload.notification:t=this.payload.notification,Object.keys(e).length&&(s.data=e),t&&Object.keys(t).length&&(s.notification=t),Object.keys(s).length?s:null}}class we{constructor(e,t){this._payload={apns:{},fcm:{}},this._title=e,this._body=t,this.apns=new ve(this._payload.apns,e,t),this.fcm=new Se(this._payload.fcm,e,t)}set debugging(e){this._debugging=e}get title(){return this._title}get subtitle(){return this._subtitle}set subtitle(e){this._subtitle=e,this.apns.subtitle=e,this.fcm.subtitle=e}get body(){return this._body}get badge(){return this._badge}set badge(e){this._badge=e,this.apns.badge=e,this.fcm.badge=e}get sound(){return this._sound}set sound(e){this._sound=e,this.apns.sound=e,this.fcm.sound=e}buildPayload(e){const t={};if(e.includes("apns")||e.includes("apns2")){this.apns._apnsPushType=e.includes("apns")?"apns":"apns2";const s=this.apns.toObject();s&&Object.keys(s).length&&(t.pn_apns=s)}if(e.includes("fcm")){const e=this.fcm.toObject();e&&Object.keys(e).length&&(t.pn_gcm=e)}return Object.keys(t).length&&this._debugging&&(t.pn_debug=!0),t}}class Oe{constructor(e=!1){this.sync=e,this.listeners=new Set}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}notify(e){const t=()=>{this.listeners.forEach((t=>{t(e)}))};this.sync?t():setTimeout(t,0)}}class ke{transition(e,t){var s;if(this.transitionMap.has(t.type))return null===(s=this.transitionMap.get(t.type))||void 0===s?void 0:s(e,t)}constructor(e){this.label=e,this.transitionMap=new Map,this.enterEffects=[],this.exitEffects=[]}on(e,t){return this.transitionMap.set(e,t),this}with(e,t){return[this,e,null!=t?t:[]]}onEnter(e){return this.enterEffects.push(e),this}onExit(e){return this.exitEffects.push(e),this}}class Ce extends Oe{constructor(e){super(!0),this.logger=e,this._pendingEvents=[],this._inTransition=!1}get currentState(){return this._currentState}get currentContext(){return this._currentContext}describe(e){return new ke(e)}start(e,t){this._currentState=e,this._currentContext=t,this.notify({type:"engineStarted",state:e,context:t})}transition(e){if(!this._currentState)throw this.logger.error("Engine","Finite state machine is not started"),new Error("Start the engine first");if(this._inTransition)return this.logger.trace("Engine",(()=>({messageType:"object",message:e,details:"Event engine in transition. Enqueue received event:"}))),void this._pendingEvents.push(e);this._inTransition=!0,this.logger.trace("Engine",(()=>({messageType:"object",message:e,details:"Event engine received event:"}))),this.notify({type:"eventReceived",event:e});const t=this._currentState.transition(this._currentContext,e);if(t){const[s,n,r]=t;this.logger.trace("Engine",`Exiting state: ${this._currentState.label}`);for(const e of this._currentState.exitEffects)this.notify({type:"invocationDispatched",invocation:e(this._currentContext)});this.logger.trace("Engine",(()=>({messageType:"object",details:`Entering '${s.label}' state with context:`,message:n})));const i=this._currentState;this._currentState=s;const a=this._currentContext;this._currentContext=n,this.notify({type:"transitionDone",fromState:i,fromContext:a,toState:s,toContext:n,event:e});for(const e of r)this.notify({type:"invocationDispatched",invocation:e});for(const e of this._currentState.enterEffects)this.notify({type:"invocationDispatched",invocation:e(this._currentContext)})}else this.logger.warn("Engine",`No transition from '${this._currentState.label}' found for event: ${e.type}`);if(this._inTransition=!1,this._pendingEvents.length>0){const e=this._pendingEvents.shift();e&&(this.logger.trace("Engine",(()=>({messageType:"object",message:e,details:"De-queueing pending event:"}))),this.transition(e))}}}class Pe{constructor(e,t){this.dependencies=e,this.logger=t,this.instances=new Map,this.handlers=new Map}on(e,t){this.handlers.set(e,t)}dispatch(e){if(this.logger.trace("Dispatcher",`Process invocation: ${e.type}`),"CANCEL"===e.type){if(this.instances.has(e.payload)){const t=this.instances.get(e.payload);null==t||t.cancel(),this.instances.delete(e.payload)}return}const t=this.handlers.get(e.type);if(!t)throw this.logger.error("Dispatcher",`Unhandled invocation '${e.type}'`),new Error(`Unhandled invocation '${e.type}'`);const s=t(e.payload,this.dependencies);this.logger.trace("Dispatcher",(()=>({messageType:"object",details:"Call invocation handler with parameters:",message:e.payload,ignoredKeys:["abortSignal"]}))),e.managed&&this.instances.set(e.type,s),s.start()}dispose(){for(const[e,t]of this.instances.entries())t.cancel(),this.instances.delete(e)}}function je(e,t){const s=function(...s){return{type:e,payload:null==t?void 0:t(...s)}};return s.type=e,s}function Ee(e,t){const s=(...s)=>({type:e,payload:t(...s),managed:!1});return s.type=e,s}function Ne(e,t){const s=(...s)=>({type:e,payload:t(...s),managed:!0});return s.type=e,s.cancel={type:"CANCEL",payload:e,managed:!1},s}class Te extends Error{constructor(){super("The operation was aborted."),this.name="AbortError",Object.setPrototypeOf(this,new.target.prototype)}}class _e extends Oe{constructor(){super(...arguments),this._aborted=!1}get aborted(){return this._aborted}throwIfAborted(){if(this._aborted)throw new Te}abort(){this._aborted=!0,this.notify(new Te)}}class Ie{constructor(e,t){this.payload=e,this.dependencies=t}}class Me extends Ie{constructor(e,t,s){super(e,t),this.asyncFunction=s,this.abortSignal=new _e}start(){this.asyncFunction(this.payload,this.abortSignal,this.dependencies).catch((e=>{}))}cancel(){this.abortSignal.abort()}}const Ae=e=>(t,s)=>new Me(t,s,e),Ue=Ne("HEARTBEAT",((e,t)=>({channels:e,groups:t}))),De=Ee("LEAVE",((e,t)=>({channels:e,groups:t}))),Fe=Ee("EMIT_STATUS",(e=>e)),Re=Ne("WAIT",(()=>({}))),$e=je("RECONNECT",(()=>({}))),xe=je("DISCONNECT",((e=!1)=>({isOffline:e}))),qe=je("JOINED",((e,t)=>({channels:e,groups:t}))),Le=je("LEFT",((e,t)=>({channels:e,groups:t}))),Ge=je("LEFT_ALL",((e=!1)=>({isOffline:e}))),Ke=je("HEARTBEAT_SUCCESS",(e=>({statusCode:e}))),He=je("HEARTBEAT_FAILURE",(e=>e)),Be=je("TIMES_UP",(()=>({})));class We extends Pe{constructor(e,t){super(t,t.config.logger()),this.on(Ue.type,Ae(((t,s,n)=>i(this,[t,s,n],void 0,(function*(t,s,{heartbeat:n,presenceState:r,config:i}){s.throwIfAborted();try{yield n(Object.assign(Object.assign({abortSignal:s,channels:t.channels,channelGroups:t.groups},i.maintainPresenceState&&{state:r}),{heartbeat:i.presenceTimeout}));e.transition(Ke(200))}catch(t){if(t instanceof d){if(t.status&&t.status.category==h.PNCancelledCategory)return;e.transition(He(t))}}}))))),this.on(De.type,Ae(((e,t,s)=>i(this,[e,t,s],void 0,(function*(e,t,{leave:s,config:n}){if(!n.suppressLeaveEvents)try{s({channels:e.channels,channelGroups:e.groups})}catch(e){}}))))),this.on(Re.type,Ae(((t,s,n)=>i(this,[t,s,n],void 0,(function*(t,s,{heartbeatDelay:n}){return s.throwIfAborted(),yield n(),s.throwIfAborted(),e.transition(Be())}))))),this.on(Fe.type,Ae(((e,t,s)=>i(this,[e,t,s],void 0,(function*(e,t,{emitStatus:s,config:n}){n.announceFailedHeartbeats&&!0===(null==e?void 0:e.error)?s(Object.assign(Object.assign({},e),{operation:M.PNHeartbeatOperation})):n.announceSuccessfulHeartbeats&&200===e.statusCode&&s(Object.assign(Object.assign({},e),{error:!1,operation:M.PNHeartbeatOperation,category:h.PNAcknowledgmentCategory}))})))))}}const ze=new ke("HEARTBEAT_STOPPED");ze.on(qe.type,((e,t)=>ze.with({channels:[...e.channels,...t.payload.channels.filter((t=>!e.channels.includes(t)))],groups:[...e.groups,...t.payload.groups.filter((t=>!e.groups.includes(t)))]}))),ze.on(Le.type,((e,t)=>ze.with({channels:e.channels.filter((e=>!t.payload.channels.includes(e))),groups:e.groups.filter((e=>!t.payload.groups.includes(e)))}))),ze.on($e.type,((e,t)=>Xe.with({channels:e.channels,groups:e.groups}))),ze.on(Ge.type,((e,t)=>Qe.with(void 0)));const Ve=new ke("HEARTBEAT_COOLDOWN");Ve.onEnter((()=>Re())),Ve.onExit((()=>Re.cancel)),Ve.on(Be.type,((e,t)=>Xe.with({channels:e.channels,groups:e.groups}))),Ve.on(qe.type,((e,t)=>Xe.with({channels:[...e.channels,...t.payload.channels.filter((t=>!e.channels.includes(t)))],groups:[...e.groups,...t.payload.groups.filter((t=>!e.groups.includes(t)))]}))),Ve.on(Le.type,((e,t)=>Xe.with({channels:e.channels.filter((e=>!t.payload.channels.includes(e))),groups:e.groups.filter((e=>!t.payload.groups.includes(e)))},[De(t.payload.channels,t.payload.groups)]))),Ve.on(xe.type,((e,t)=>ze.with({channels:e.channels,groups:e.groups},[...t.payload.isOffline?[]:[De(e.channels,e.groups)]]))),Ve.on(Ge.type,((e,t)=>Qe.with(void 0,[...t.payload.isOffline?[]:[De(e.channels,e.groups)]])));const Je=new ke("HEARTBEAT_FAILED");Je.on(qe.type,((e,t)=>Xe.with({channels:[...e.channels,...t.payload.channels.filter((t=>!e.channels.includes(t)))],groups:[...e.groups,...t.payload.groups.filter((t=>!e.groups.includes(t)))]}))),Je.on(Le.type,((e,t)=>Xe.with({channels:e.channels.filter((e=>!t.payload.channels.includes(e))),groups:e.groups.filter((e=>!t.payload.groups.includes(e)))},[De(t.payload.channels,t.payload.groups)]))),Je.on($e.type,((e,t)=>Xe.with({channels:e.channels,groups:e.groups}))),Je.on(xe.type,((e,t)=>ze.with({channels:e.channels,groups:e.groups},[...t.payload.isOffline?[]:[De(e.channels,e.groups)]]))),Je.on(Ge.type,((e,t)=>Qe.with(void 0,[...t.payload.isOffline?[]:[De(e.channels,e.groups)]])));const Xe=new ke("HEARTBEATING");Xe.onEnter((e=>Ue(e.channels,e.groups))),Xe.onExit((()=>Ue.cancel)),Xe.on(Ke.type,((e,t)=>Ve.with({channels:e.channels,groups:e.groups},[Fe(Object.assign({},t.payload))]))),Xe.on(qe.type,((e,t)=>Xe.with({channels:[...e.channels,...t.payload.channels.filter((t=>!e.channels.includes(t)))],groups:[...e.groups,...t.payload.groups.filter((t=>!e.groups.includes(t)))]}))),Xe.on(Le.type,((e,t)=>Xe.with({channels:e.channels.filter((e=>!t.payload.channels.includes(e))),groups:e.groups.filter((e=>!t.payload.groups.includes(e)))},[De(t.payload.channels,t.payload.groups)]))),Xe.on(He.type,((e,t)=>Je.with(Object.assign({},e),[...t.payload.status?[Fe(Object.assign({},t.payload.status))]:[]]))),Xe.on(xe.type,((e,t)=>ze.with({channels:e.channels,groups:e.groups},[...t.payload.isOffline?[]:[De(e.channels,e.groups)]]))),Xe.on(Ge.type,((e,t)=>Qe.with(void 0,[...t.payload.isOffline?[]:[De(e.channels,e.groups)]])));const Qe=new ke("HEARTBEAT_INACTIVE");Qe.on(qe.type,((e,t)=>Xe.with({channels:t.payload.channels,groups:t.payload.groups})));class Ye{get _engine(){return this.engine}constructor(e){this.dependencies=e,this.channels=[],this.groups=[],this.engine=new Ce(e.config.logger()),this.dispatcher=new We(this.engine,e),e.config.logger().debug("PresenceEventEngine","Create presence event engine."),this._unsubscribeEngine=this.engine.subscribe((e=>{"invocationDispatched"===e.type&&this.dispatcher.dispatch(e.invocation)})),this.engine.start(Qe,void 0)}join({channels:e,groups:t}){this.channels=[...this.channels,...(null!=e?e:[]).filter((e=>!this.channels.includes(e)))],this.groups=[...this.groups,...(null!=t?t:[]).filter((e=>!this.groups.includes(e)))],0===this.channels.length&&0===this.groups.length||this.engine.transition(qe(this.channels.slice(0),this.groups.slice(0)))}leave({channels:e,groups:t}){this.dependencies.presenceState&&(null==e||e.forEach((e=>delete this.dependencies.presenceState[e])),null==t||t.forEach((e=>delete this.dependencies.presenceState[e]))),this.engine.transition(Le(null!=e?e:[],null!=t?t:[]))}leaveAll(e=!1){this.engine.transition(Ge(e))}reconnect(){this.engine.transition($e())}disconnect(e=!1){this.engine.transition(xe(e))}dispose(){this.disconnect(!0),this._unsubscribeEngine(),this.dispatcher.dispose()}}const Ze=Ne("HANDSHAKE",((e,t,s)=>({channels:e,groups:t,onDemand:s}))),et=Ne("RECEIVE_MESSAGES",((e,t,s,n)=>({channels:e,groups:t,cursor:s,onDemand:n}))),tt=Ee("EMIT_MESSAGES",((e,t)=>({cursor:e,events:t}))),st=Ee("EMIT_STATUS",(e=>e)),nt=je("SUBSCRIPTION_CHANGED",((e,t,s=!1)=>({channels:e,groups:t,isOffline:s}))),rt=je("SUBSCRIPTION_RESTORED",((e,t,s,n)=>({channels:e,groups:t,cursor:{timetoken:s,region:null!=n?n:0}}))),it=je("HANDSHAKE_SUCCESS",(e=>e)),at=je("HANDSHAKE_FAILURE",(e=>e)),ot=je("RECEIVE_SUCCESS",((e,t)=>({cursor:e,events:t}))),ct=je("RECEIVE_FAILURE",(e=>e)),ut=je("DISCONNECT",((e=!1)=>({isOffline:e}))),lt=je("RECONNECT",((e,t)=>({cursor:{timetoken:null!=e?e:"",region:null!=t?t:0}}))),ht=je("UNSUBSCRIBE_ALL",(()=>({}))),dt=new ke("UNSUBSCRIBED");dt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,onDemand:!0}))),dt.on(rt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region},onDemand:!0})));const pt=new ke("HANDSHAKE_STOPPED");pt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):pt.with({channels:t.channels,groups:t.groups,cursor:e.cursor}))),pt.on(lt.type,((e,{payload:t})=>bt.with(Object.assign(Object.assign({},e),{cursor:t.cursor||e.cursor,onDemand:!0})))),pt.on(rt.type,((e,{payload:t})=>{var s;return 0===t.channels.length&&0===t.groups.length?dt.with(void 0):pt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||(null===(s=e.cursor)||void 0===s?void 0:s.region)||0}})})),pt.on(ht.type,(e=>dt.with()));const gt=new ke("HANDSHAKE_FAILED");gt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:e.cursor,onDemand:!0}))),gt.on(lt.type,((e,{payload:t})=>bt.with(Object.assign(Object.assign({},e),{cursor:t.cursor||e.cursor,onDemand:!0})))),gt.on(rt.type,((e,{payload:t})=>{var s,n;return 0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region?t.cursor.region:null!==(n=null===(s=null==e?void 0:e.cursor)||void 0===s?void 0:s.region)&&void 0!==n?n:0},onDemand:!0})})),gt.on(ht.type,(e=>dt.with()));const bt=new ke("HANDSHAKING");bt.onEnter((e=>{var t;return Ze(e.channels,e.groups,null!==(t=e.onDemand)&&void 0!==t&&t)})),bt.onExit((()=>Ze.cancel)),bt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:e.cursor,onDemand:!0}))),bt.on(it.type,((e,{payload:t})=>{var s,n,r,i,a;return ft.with({channels:e.channels,groups:e.groups,cursor:{timetoken:(null===(s=e.cursor)||void 0===s?void 0:s.timetoken)?null===(n=e.cursor)||void 0===n?void 0:n.timetoken:t.timetoken,region:t.region},referenceTimetoken:K(t.timetoken,null===(r=e.cursor)||void 0===r?void 0:r.timetoken)},[st({category:h.PNConnectedCategory,affectedChannels:e.channels.slice(0),affectedChannelGroups:e.groups.slice(0),currentTimetoken:(null===(i=e.cursor)||void 0===i?void 0:i.timetoken)?null===(a=e.cursor)||void 0===a?void 0:a.timetoken:t.timetoken})])})),bt.on(at.type,((e,t)=>{var s;return gt.with(Object.assign(Object.assign({},e),{reason:t.payload}),[st({category:h.PNConnectionErrorCategory,error:null===(s=t.payload.status)||void 0===s?void 0:s.category})])})),bt.on(ut.type,((e,t)=>{var s;if(t.payload.isOffline){const t=I.create(new Error("Network connection error")).toPubNubError(M.PNSubscribeOperation);return gt.with(Object.assign(Object.assign({},e),{reason:t}),[st({category:h.PNConnectionErrorCategory,error:null===(s=t.status)||void 0===s?void 0:s.category})])}return pt.with(Object.assign({},e))})),bt.on(rt.type,((e,{payload:t})=>{var s;return 0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||(null===(s=null==e?void 0:e.cursor)||void 0===s?void 0:s.region)||0},onDemand:!0})})),bt.on(ht.type,(e=>dt.with()));const yt=new ke("RECEIVE_STOPPED");yt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):yt.with({channels:t.channels,groups:t.groups,cursor:e.cursor}))),yt.on(rt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):yt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||e.cursor.region}}))),yt.on(lt.type,((e,{payload:t})=>{var s;return bt.with({channels:e.channels,groups:e.groups,cursor:{timetoken:t.cursor.timetoken?null===(s=t.cursor)||void 0===s?void 0:s.timetoken:e.cursor.timetoken,region:t.cursor.region||e.cursor.region},onDemand:!0})})),yt.on(ht.type,(()=>dt.with(void 0)));const mt=new ke("RECEIVE_FAILED");mt.on(lt.type,((e,{payload:t})=>{var s;return bt.with({channels:e.channels,groups:e.groups,cursor:{timetoken:t.cursor.timetoken?null===(s=t.cursor)||void 0===s?void 0:s.timetoken:e.cursor.timetoken,region:t.cursor.region||e.cursor.region},onDemand:!0})})),mt.on(nt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:e.cursor,onDemand:!0}))),mt.on(rt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0):bt.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||e.cursor.region},onDemand:!0}))),mt.on(ht.type,(e=>dt.with(void 0)));const ft=new ke("RECEIVING");ft.onEnter((e=>{var t;return et(e.channels,e.groups,e.cursor,null!==(t=e.onDemand)&&void 0!==t&&t)})),ft.onExit((()=>et.cancel)),ft.on(ot.type,((e,{payload:t})=>ft.with({channels:e.channels,groups:e.groups,cursor:t.cursor,referenceTimetoken:K(t.cursor.timetoken)},[tt(e.cursor,t.events)]))),ft.on(nt.type,((e,{payload:t})=>{var s;if(0===t.channels.length&&0===t.groups.length){let e;return t.isOffline&&(e=null===(s=I.create(new Error("Network connection error")).toPubNubError(M.PNSubscribeOperation).status)||void 0===s?void 0:s.category),dt.with(void 0,[st(Object.assign({category:t.isOffline?h.PNDisconnectedUnexpectedlyCategory:h.PNDisconnectedCategory},e?{error:e}:{}))])}return ft.with({channels:t.channels,groups:t.groups,cursor:e.cursor,referenceTimetoken:e.referenceTimetoken,onDemand:!0},[st({category:h.PNSubscriptionChangedCategory,affectedChannels:t.channels.slice(0),affectedChannelGroups:t.groups.slice(0),currentTimetoken:e.cursor.timetoken})])})),ft.on(rt.type,((e,{payload:t})=>0===t.channels.length&&0===t.groups.length?dt.with(void 0,[st({category:h.PNDisconnectedCategory})]):ft.with({channels:t.channels,groups:t.groups,cursor:{timetoken:`${t.cursor.timetoken}`,region:t.cursor.region||e.cursor.region},referenceTimetoken:K(e.cursor.timetoken,`${t.cursor.timetoken}`,e.referenceTimetoken),onDemand:!0},[st({category:h.PNSubscriptionChangedCategory,affectedChannels:t.channels.slice(0),affectedChannelGroups:t.groups.slice(0),currentTimetoken:t.cursor.timetoken})]))),ft.on(ct.type,((e,{payload:t})=>{var s;return mt.with(Object.assign(Object.assign({},e),{reason:t}),[st({category:h.PNDisconnectedUnexpectedlyCategory,error:null===(s=t.status)||void 0===s?void 0:s.category})])})),ft.on(ut.type,((e,t)=>{var s;if(t.payload.isOffline){const t=I.create(new Error("Network connection error")).toPubNubError(M.PNSubscribeOperation);return mt.with(Object.assign(Object.assign({},e),{reason:t}),[st({category:h.PNDisconnectedUnexpectedlyCategory,error:null===(s=t.status)||void 0===s?void 0:s.category})])}return yt.with(Object.assign({},e),[st({category:h.PNDisconnectedCategory})])})),ft.on(ht.type,(e=>dt.with(void 0,[st({category:h.PNDisconnectedCategory})])));class vt extends Pe{constructor(e,t){super(t,t.config.logger()),this.on(Ze.type,Ae(((t,s,n)=>i(this,[t,s,n],void 0,(function*(t,s,{handshake:n,presenceState:r,config:i}){s.throwIfAborted();try{const a=yield n(Object.assign(Object.assign({abortSignal:s,channels:t.channels,channelGroups:t.groups,filterExpression:i.filterExpression},i.maintainPresenceState&&{state:r}),{onDemand:t.onDemand}));return e.transition(it(a))}catch(t){if(t instanceof d){if(t.status&&t.status.category==h.PNCancelledCategory)return;return e.transition(at(t))}}}))))),this.on(et.type,Ae(((t,s,n)=>i(this,[t,s,n],void 0,(function*(t,s,{receiveMessages:n,config:r}){s.throwIfAborted();try{const i=yield n({abortSignal:s,channels:t.channels,channelGroups:t.groups,timetoken:t.cursor.timetoken,region:t.cursor.region,filterExpression:r.filterExpression,onDemand:t.onDemand});e.transition(ot(i.cursor,i.messages))}catch(t){if(t instanceof d){if(t.status&&t.status.category==h.PNCancelledCategory)return;if(!s.aborted)return e.transition(ct(t))}}}))))),this.on(tt.type,Ae(((e,t,s)=>i(this,[e,t,s],void 0,(function*({cursor:e,events:t},s,{emitMessages:n}){t.length>0&&n(e,t)}))))),this.on(st.type,Ae(((e,t,s)=>i(this,[e,t,s],void 0,(function*(e,t,{emitStatus:s}){return s(e)})))))}}class St{get _engine(){return this.engine}constructor(e){this.channels=[],this.groups=[],this.dependencies=e,this.engine=new Ce(e.config.logger()),this.dispatcher=new vt(this.engine,e),e.config.logger().debug("EventEngine","Create subscribe event engine."),this._unsubscribeEngine=this.engine.subscribe((e=>{"invocationDispatched"===e.type&&this.dispatcher.dispatch(e.invocation)})),this.engine.start(dt,void 0)}get subscriptionTimetoken(){const e=this.engine.currentState;if(!e)return;let t,s="0";if(e.label===ft.label){const e=this.engine.currentContext;s=e.cursor.timetoken,t=e.referenceTimetoken}return G(s,null!=t?t:"0")}subscribe({channels:e,channelGroups:t,timetoken:s,withPresence:n}){this.channels=[...this.channels,...null!=e?e:[]],this.groups=[...this.groups,...null!=t?t:[]],n&&(this.channels.map((e=>this.channels.push(`${e}-pnpres`))),this.groups.map((e=>this.groups.push(`${e}-pnpres`)))),s?this.engine.transition(rt(Array.from(new Set([...this.channels,...null!=e?e:[]])),Array.from(new Set([...this.groups,...null!=t?t:[]])),s)):this.engine.transition(nt(Array.from(new Set([...this.channels,...null!=e?e:[]])),Array.from(new Set([...this.groups,...null!=t?t:[]])))),this.dependencies.join&&this.dependencies.join({channels:Array.from(new Set(this.channels.filter((e=>!e.endsWith("-pnpres"))))),groups:Array.from(new Set(this.groups.filter((e=>!e.endsWith("-pnpres")))))})}unsubscribe({channels:e=[],channelGroups:t=[]}){const s=x(this.channels,[...e,...e.map((e=>`${e}-pnpres`))]),n=x(this.groups,[...t,...t.map((e=>`${e}-pnpres`))]);if(new Set(this.channels).size!==new Set(s).size||new Set(this.groups).size!==new Set(n).size){const r=q(this.channels,e),i=q(this.groups,t);this.dependencies.presenceState&&(null==r||r.forEach((e=>delete this.dependencies.presenceState[e])),null==i||i.forEach((e=>delete this.dependencies.presenceState[e]))),this.channels=s,this.groups=n,this.engine.transition(nt(Array.from(new Set(this.channels.slice(0))),Array.from(new Set(this.groups.slice(0))))),this.dependencies.leave&&this.dependencies.leave({channels:r.slice(0),groups:i.slice(0)})}}unsubscribeAll(e=!1){const t=this.getSubscribedChannelGroups(),s=this.getSubscribedChannels();this.channels=[],this.groups=[],this.dependencies.presenceState&&Object.keys(this.dependencies.presenceState).forEach((e=>{delete this.dependencies.presenceState[e]})),this.engine.transition(nt(this.channels.slice(0),this.groups.slice(0),e)),this.dependencies.leaveAll&&this.dependencies.leaveAll({channels:s,groups:t,isOffline:e})}reconnect({timetoken:e,region:t}){const s=this.getSubscribedChannels(),n=this.getSubscribedChannels();this.engine.transition(lt(e,t)),this.dependencies.presenceReconnect&&this.dependencies.presenceReconnect({channels:n,groups:s})}disconnect(e=!1){const t=this.getSubscribedChannels(),s=this.getSubscribedChannels();this.engine.transition(ut(e)),this.dependencies.presenceDisconnect&&this.dependencies.presenceDisconnect({channels:s,groups:t,isOffline:e})}getSubscribedChannels(){return Array.from(new Set(this.channels.slice(0)))}getSubscribedChannelGroups(){return Array.from(new Set(this.groups.slice(0)))}dispose(){this.disconnect(!0),this._unsubscribeEngine(),this.dispatcher.dispose()}}class wt extends le{constructor(e){var t;const s=null!==(t=e.sendByPost)&&void 0!==t&&t;super({method:s?ae.POST:ae.GET,compressible:s}),this.parameters=e,this.parameters.sendByPost=s}operation(){return M.PNPublishOperation}validate(){const{message:e,channel:t,keySet:{publishKey:s}}=this.parameters;return t?e?s?void 0:"Missing 'publishKey'":"Missing 'message'":"Missing 'channel'"}parse(e){return i(this,void 0,void 0,(function*(){return{timetoken:this.deserializeResponse(e)[2]}}))}get path(){const{message:e,channel:t,keySet:s}=this.parameters,n=this.prepareMessagePayload(e);return`/publish/${s.publishKey}/${s.subscribeKey}/0/${R(t)}/0${this.parameters.sendByPost?"":`/${R(n)}`}`}get queryParameters(){const{customMessageType:e,meta:t,replicate:s,storeInHistory:n,ttl:r}=this.parameters,i={};return e&&(i.custom_message_type=e),void 0!==n&&(i.store=n?"1":"0"),void 0!==r&&(i.ttl=r),void 0===s||s||(i.norep="true"),t&&"object"==typeof t&&(i.meta=JSON.stringify(t)),i}get headers(){var e;return this.parameters.sendByPost?Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"}):super.headers}get body(){return this.prepareMessagePayload(this.parameters.message)}prepareMessagePayload(e){const{crypto:t}=this.parameters;if(!t)return JSON.stringify(e)||"";const s=t.encrypt(JSON.stringify(e));return JSON.stringify("string"==typeof s?s:u(s))}}class Ot extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNSignalOperation}validate(){const{message:e,channel:t,keySet:{publishKey:s}}=this.parameters;return t?e?s?void 0:"Missing 'publishKey'":"Missing 'message'":"Missing 'channel'"}parse(e){return i(this,void 0,void 0,(function*(){return{timetoken:this.deserializeResponse(e)[2]}}))}get path(){const{keySet:{publishKey:e,subscribeKey:t},channel:s,message:n}=this.parameters,r=JSON.stringify(n);return`/signal/${e}/${t}/0/${R(s)}/0/${R(r)}`}get queryParameters(){const{customMessageType:e}=this.parameters,t={};return e&&(t.custom_message_type=e),t}}class kt extends de{operation(){return M.PNReceiveMessagesOperation}validate(){const e=super.validate();return e||(this.parameters.timetoken?this.parameters.region?void 0:"region can not be empty":"timetoken can not be empty")}get path(){const{keySet:{subscribeKey:e},channels:t=[]}=this.parameters;return`/v2/subscribe/${e}/${$(t.sort(),",")}/0`}get queryParameters(){const{channelGroups:e,filterExpression:t,timetoken:s,region:n,onDemand:r}=this.parameters,i={ee:""};return r&&(i["on-demand"]=1),e&&e.length>0&&(i["channel-group"]=e.sort().join(",")),t&&t.length>0&&(i["filter-expr"]=t),"string"==typeof s?s&&"0"!==s&&s.length>0&&(i.tt=s):s&&s>0&&(i.tt=s),n&&(i.tr=n),i}}class Ct extends de{operation(){return M.PNHandshakeOperation}get path(){const{keySet:{subscribeKey:e},channels:t=[]}=this.parameters;return`/v2/subscribe/${e}/${$(t.sort(),",")}/0`}get queryParameters(){const{channelGroups:e,filterExpression:t,state:s,onDemand:n}=this.parameters,r={ee:""};return n&&(r["on-demand"]=1),e&&e.length>0&&(r["channel-group"]=e.sort().join(",")),t&&t.length>0&&(r["filter-expr"]=t),s&&Object.keys(s).length>0&&(r.state=JSON.stringify(s)),r}}var Pt;!function(e){e[e.Channel=0]="Channel",e[e.ChannelGroup=1]="ChannelGroup"}(Pt||(Pt={}));class jt{constructor({channels:e,channelGroups:t}){this.isEmpty=!0,this._channelGroups=new Set((null!=t?t:[]).filter((e=>e.length>0))),this._channels=new Set((null!=e?e:[]).filter((e=>e.length>0))),this.isEmpty=0===this._channels.size&&0===this._channelGroups.size}get length(){return this.isEmpty?0:this._channels.size+this._channelGroups.size}get channels(){return this.isEmpty?[]:Array.from(this._channels)}get channelGroups(){return this.isEmpty?[]:Array.from(this._channelGroups)}contains(e){return!this.isEmpty&&(this._channels.has(e)||this._channelGroups.has(e))}with(e){return new jt({channels:[...this._channels,...e._channels],channelGroups:[...this._channelGroups,...e._channelGroups]})}without(e){return new jt({channels:[...this._channels].filter((t=>!e._channels.has(t))),channelGroups:[...this._channelGroups].filter((t=>!e._channelGroups.has(t)))})}add(e){return e._channelGroups.size>0&&(this._channelGroups=new Set([...this._channelGroups,...e._channelGroups])),e._channels.size>0&&(this._channels=new Set([...this._channels,...e._channels])),this.isEmpty=0===this._channels.size&&0===this._channelGroups.size,this}remove(e){return e._channelGroups.size>0&&(this._channelGroups=new Set([...this._channelGroups].filter((t=>!e._channelGroups.has(t))))),e._channels.size>0&&(this._channels=new Set([...this._channels].filter((t=>!e._channels.has(t))))),this}removeAll(){return this._channels.clear(),this._channelGroups.clear(),this.isEmpty=!0,this}toString(){return`SubscriptionInput { channels: [${this.channels.join(", ")}], channelGroups: [${this.channelGroups.join(", ")}], is empty: ${this.isEmpty?"true":"false"}} }`}}class Et{constructor(e,t,s,n){this._isSubscribed=!1,this.clones={},this.parents=[],this._id=te.createUUID(),this.referenceTimetoken=n,this.subscriptionInput=t,this.options=s,this.client=e}get id(){return this._id}get isLastClone(){return 1===Object.keys(this.clones).length}get isSubscribed(){return!!this._isSubscribed||this.parents.length>0&&this.parents.some((e=>e.isSubscribed))}set isSubscribed(e){this.isSubscribed!==e&&(this._isSubscribed=e)}addParentState(e){this.parents.includes(e)||this.parents.push(e)}removeParentState(e){const t=this.parents.indexOf(e);-1!==t&&this.parents.splice(t,1)}storeClone(e,t){this.clones[e]||(this.clones[e]=t)}}class Nt{constructor(e,t="Subscription"){this.subscriptionType=t,this.id=te.createUUID(),this.eventDispatcher=new ge,this._state=e}get state(){return this._state}get channels(){return this.state.subscriptionInput.channels.slice(0)}get channelGroups(){return this.state.subscriptionInput.channelGroups.slice(0)}set onMessage(e){this.eventDispatcher.onMessage=e}set onPresence(e){this.eventDispatcher.onPresence=e}set onSignal(e){this.eventDispatcher.onSignal=e}set onObjects(e){this.eventDispatcher.onObjects=e}set onMessageAction(e){this.eventDispatcher.onMessageAction=e}set onFile(e){this.eventDispatcher.onFile=e}addListener(e){this.eventDispatcher.addListener(e)}removeListener(e){this.eventDispatcher.removeListener(e)}removeAllListeners(){this.eventDispatcher.removeAllListeners()}handleEvent(e,t){var s;if((!this.state.cursor||e>this.state.cursor)&&(this.state.cursor=e),this.state.referenceTimetoken&&t.data.timetoken({messageType:"text",message:`Event timetoken (${t.data.timetoken}) is older than reference timetoken (${this.state.referenceTimetoken}) for ${this.id} subscription object. Ignoring event.`})));if((null===(s=this.state.options)||void 0===s?void 0:s.filter)&&!this.state.options.filter(t))return void this.state.client.logger.trace(this.subscriptionType,`Event filtered out by filter function for ${this.id} subscription object. Ignoring event.`);const n=Object.values(this.state.clones);n.length>0&&this.state.client.logger.trace(this.subscriptionType,`Notify ${this.id} subscription object clones (count: ${n.length}) about received event.`),n.forEach((e=>e.eventDispatcher.handleEvent(t)))}dispose(){const e=Object.keys(this.state.clones);e.length>1?(this.state.client.logger.debug(this.subscriptionType,`Remove subscription object clone on dispose: ${this.id}`),delete this.state.clones[this.id]):1===e.length&&this.state.clones[this.id]&&(this.state.client.logger.debug(this.subscriptionType,`Unsubscribe subscription object on dispose: ${this.id}`),this.unsubscribe())}invalidate(e=!1){this.state._isSubscribed=!1,e&&(delete this.state.clones[this.id],0===Object.keys(this.state.clones).length&&(this.state.client.logger.trace(this.subscriptionType,"Last clone removed. Reset shared subscription state."),this.state.subscriptionInput.removeAll(),this.state.parents=[]))}subscribe(e){this.state.isSubscribed?this.state.client.logger.trace(this.subscriptionType,"Already subscribed. Ignoring subscribe request."):(this.state.client.logger.debug(this.subscriptionType,(()=>e?{messageType:"object",message:e,details:"Subscribe with parameters:"}:{messageType:"text",message:"Subscribe"})),this.state.isSubscribed=!0,this.updateSubscription({subscribing:!0,timetoken:null==e?void 0:e.timetoken}))}unsubscribe(){if(!this.state._isSubscribed||this.state.isSubscribed){if(!this.state._isSubscribed&&this.state.parents.length>0&&this.state.isSubscribed)return void this.state.client.logger.warn(this.subscriptionType,(()=>({messageType:"object",details:"Subscription is subscribed as part of a subscription set. Remove from active sets to unsubscribe:",message:this.state.parents.filter((e=>e.isSubscribed))})));if(!this.state._isSubscribed)return void this.state.client.logger.trace(this.subscriptionType,"Not subscribed. Ignoring unsubscribe request.")}this.state.client.logger.debug(this.subscriptionType,"Unsubscribe"),this.state.isSubscribed=!1,delete this.state.cursor,this.updateSubscription({subscribing:!1})}updateSubscription(e){var t,s;(null==e?void 0:e.timetoken)&&((null===(t=this.state.cursor)||void 0===t?void 0:t.timetoken)&&"0"!==(null===(s=this.state.cursor)||void 0===s?void 0:s.timetoken)?"0"!==e.timetoken&&e.timetoken>this.state.cursor.timetoken&&(this.state.cursor.timetoken=e.timetoken):this.state.cursor={timetoken:e.timetoken});const n=e.subscriptions&&e.subscriptions.length>0?e.subscriptions:void 0;e.subscribing?this.register(Object.assign(Object.assign({},e.timetoken?{cursor:this.state.cursor}:{}),n?{subscriptions:n}:{})):this.unregister(n)}}class Tt extends Et{constructor(e){const t=new jt({});e.subscriptions.forEach((e=>t.add(e.state.subscriptionInput))),super(e.client,t,e.options,e.client.subscriptionTimetoken),this.subscriptions=e.subscriptions}addSubscription(e){this.subscriptions.includes(e)||(e.state.addParentState(this),this.subscriptions.push(e),this.subscriptionInput.add(e.state.subscriptionInput))}removeSubscription(e,t){const s=this.subscriptions.indexOf(e);-1!==s&&(this.subscriptions.splice(s,1),t||e.state.removeParentState(this),this.subscriptionInput.remove(e.state.subscriptionInput))}removeAllSubscriptions(){this.subscriptions.forEach((e=>e.state.removeParentState(this))),this.subscriptions.splice(0,this.subscriptions.length),this.subscriptionInput.removeAll()}}class _t extends Nt{constructor(e){let t;if("client"in e){let s=[];!e.subscriptions&&e.entities?e.entities.forEach((t=>s.push(t.subscription(e.options)))):e.subscriptions&&(s=e.subscriptions),t=new Tt({client:e.client,subscriptions:s,options:e.options}),s.forEach((e=>e.state.addParentState(t))),t.client.logger.debug("SubscriptionSet",(()=>({messageType:"object",details:"Create subscription set with parameters:",message:Object.assign({subscriptions:t.subscriptions},e.options?e.options:{})})))}else t=e.state,t.client.logger.debug("SubscriptionSet","Create subscription set clone");super(t,"SubscriptionSet"),this.state.storeClone(this.id,this),t.subscriptions.forEach((e=>e.addParentSet(this)))}get state(){return super.state}get subscriptions(){return this.state.subscriptions.slice(0)}handleEvent(e,t){var s;this.state.subscriptionInput.contains(null!==(s=t.data.subscription)&&void 0!==s?s:t.data.channel)&&(this.state._isSubscribed?(super.handleEvent(e,t),this.state.subscriptions.length>0&&this.state.client.logger.trace(this.subscriptionType,`Notify ${this.id} subscription set subscriptions (count: ${this.state.subscriptions.length}) about received event.`),this.state.subscriptions.forEach((s=>s.handleEvent(e,t)))):this.state.client.logger.trace(this.subscriptionType,`Subscription set ${this.id} is not subscribed. Ignoring event.`))}subscriptionInput(e=!1){let t=this.state.subscriptionInput;return this.state.subscriptions.forEach((s=>{e&&s.state.entity.subscriptionsCount>0&&(t=t.without(s.state.subscriptionInput))})),t}cloneEmpty(){return new _t({state:this.state})}dispose(){const e=this.state.isLastClone;this.state.subscriptions.forEach((t=>{t.removeParentSet(this),e&&t.state.removeParentState(this.state)})),super.dispose()}invalidate(e=!1){(e?this.state.subscriptions.slice(0):this.state.subscriptions).forEach((t=>{e&&(t.state.entity.decreaseSubscriptionCount(this.state.id),t.removeParentSet(this)),t.invalidate(e)})),e&&this.state.removeAllSubscriptions(),super.invalidate()}addSubscription(e){this.addSubscriptions([e])}addSubscriptions(e){const t=[],s=[];this.state.client.logger.debug(this.subscriptionType,(()=>{const t=[],s=[];return e.forEach((e=>{this.state.subscriptions.includes(e)?t.push(e):s.push(e)})),{messageType:"object",details:`Add subscriptions to ${this.id} (subscriptions count: ${this.state.subscriptions.length+s.length}):`,message:{addedSubscriptions:s,ignoredSubscriptions:t}}})),e.filter((e=>!this.state.subscriptions.includes(e))).forEach((e=>{e.state.isSubscribed?s.push(e):t.push(e),e.addParentSet(this),this.state.addSubscription(e)})),0===s.length&&0===t.length||!this.state.isSubscribed||(s.forEach((({state:e})=>e.entity.increaseSubscriptionCount(this.state.id))),t.length>0&&this.updateSubscription({subscribing:!0,subscriptions:t}))}removeSubscription(e){this.removeSubscriptions([e])}removeSubscriptions(e){const t=[];this.state.client.logger.debug(this.subscriptionType,(()=>{const t=[],s=[];return e.forEach((e=>{this.state.subscriptions.includes(e)?s.push(e):t.push(e)})),{messageType:"object",details:`Remove subscriptions from ${this.id} (subscriptions count: ${this.state.subscriptions.length}):`,message:{removedSubscriptions:s,ignoredSubscriptions:t}}})),e.filter((e=>this.state.subscriptions.includes(e))).forEach((e=>{e.state.isSubscribed&&t.push(e),e.removeParentSet(this),this.state.removeSubscription(e,e.parentSetsCount>1)})),0!==t.length&&this.state.isSubscribed&&this.updateSubscription({subscribing:!1,subscriptions:t})}addSubscriptionSet(e){this.addSubscriptions(e.subscriptions)}removeSubscriptionSet(e){this.removeSubscriptions(e.subscriptions)}register(e){var t;const s=null!==(t=e.subscriptions)&&void 0!==t?t:this.state.subscriptions;s.forEach((({state:e})=>e.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,e.cursor,s)}unregister(e){const t=null!=e?e:this.state.subscriptions;t.forEach((({state:e})=>e.entity.decreaseSubscriptionCount(this.state.id))),this.state.client.logger.trace(this.subscriptionType,(()=>e?{messageType:"object",message:{subscription:this,subscriptions:e},details:"Unregister subscriptions of subscription set from real-time events:"}:{messageType:"text",message:`Unregister subscription from real-time events: ${this}`})),this.state.client.unregisterEventHandleCapable(this,t)}toString(){const e=this.state;return`${this.subscriptionType} { id: ${this.id}, stateId: ${e.id}, clonesCount: ${Object.keys(this.state.clones).length}, isSubscribed: ${e.isSubscribed}, subscriptions: [${e.subscriptions.map((e=>e.toString())).join(", ")}] }`}}class It extends Et{constructor(e){var t,s;const n=e.entity.subscriptionNames(null!==(s=null===(t=e.options)||void 0===t?void 0:t.receivePresenceEvents)&&void 0!==s&&s),r=new jt({[e.entity.subscriptionType==Pt.Channel?"channels":"channelGroups"]:n});super(e.client,r,e.options,e.client.subscriptionTimetoken),this.entity=e.entity}}class Mt extends Nt{constructor(e){"client"in e?e.client.logger.debug("Subscription",(()=>({messageType:"object",details:"Create subscription with parameters:",message:Object.assign({entity:e.entity},e.options?e.options:{})}))):e.state.client.logger.debug("Subscription","Create subscription clone"),super("state"in e?e.state:new It(e)),this.parents=[],this.handledUpdates=[],this.state.storeClone(this.id,this)}get state(){return super.state}get parentSetsCount(){return this.parents.length}handleEvent(e,t){var s,n;if(this.state.isSubscribed&&this.state.subscriptionInput.contains(null!==(s=t.data.subscription)&&void 0!==s?s:t.data.channel)){if(this.parentSetsCount>0){const e=B(t.data);if(this.handledUpdates.includes(e))return void this.state.client.logger.trace(this.subscriptionType,`Event (${e}) already handled by ${this.id}. Ignoring.`);this.handledUpdates.push(e),this.handledUpdates.length>10&&this.handledUpdates.shift()}this.state.subscriptionInput.contains(null!==(n=t.data.subscription)&&void 0!==n?n:t.data.channel)&&super.handleEvent(e,t)}}subscriptionInput(e=!1){return e&&this.state.entity.subscriptionsCount>0?new jt({}):this.state.subscriptionInput}cloneEmpty(){return new Mt({state:this.state})}dispose(){this.parentSetsCount>0?this.state.client.logger.debug(this.subscriptionType,(()=>({messageType:"text",message:`'${this.state.entity.subscriptionNames()}' subscription still in use. Ignore dispose request.`}))):(this.handledUpdates.splice(0,this.handledUpdates.length),super.dispose())}invalidate(e=!1){e&&this.state.entity.decreaseSubscriptionCount(this.state.id),this.handledUpdates.splice(0,this.handledUpdates.length),super.invalidate(e)}addParentSet(e){this.parents.includes(e)||(this.parents.push(e),this.state.client.logger.trace(this.subscriptionType,`Add parent subscription set for ${this.id}: ${e.id}. Parent subscription set count: ${this.parentSetsCount}`))}removeParentSet(e){const t=this.parents.indexOf(e);-1!==t&&(this.parents.splice(t,1),this.state.client.logger.trace(this.subscriptionType,`Remove parent subscription set from ${this.id}: ${e.id}. Parent subscription set count: ${this.parentSetsCount}`)),0===this.parentSetsCount&&this.handledUpdates.splice(0,this.handledUpdates.length)}addSubscription(e){this.state.client.logger.debug(this.subscriptionType,(()=>({messageType:"text",message:`Create set with subscription: ${e}`})));const t=new _t({client:this.state.client,subscriptions:[this,e],options:this.state.options});return this.state.isSubscribed||e.state.isSubscribed?(this.state.client.logger.trace(this.subscriptionType,"Subscribe resulting set because the receiver is already subscribed."),t.subscribe(),t):t}register(e){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,e.cursor)}unregister(e){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)}toString(){const e=this.state;return`${this.subscriptionType} { id: ${this.id}, stateId: ${e.id}, entity: ${e.entity.subscriptionNames(!1).pop()}, clonesCount: ${Object.keys(e.clones).length}, isSubscribed: ${e.isSubscribed}, parentSetsCount: ${this.parentSetsCount}, cursor: ${e.cursor?e.cursor.timetoken:"not set"}, referenceTimetoken: ${e.referenceTimetoken?e.referenceTimetoken:"not set"} }`}}class At extends le{constructor(e){var t,s,n,r;super(),this.parameters=e,null!==(t=(n=this.parameters).channels)&&void 0!==t||(n.channels=[]),null!==(s=(r=this.parameters).channelGroups)&&void 0!==s||(r.channelGroups=[])}operation(){return M.PNGetStateOperation}validate(){const{keySet:{subscribeKey:e},channels:t,channelGroups:s}=this.parameters;if(!e)return"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e),{channels:s=[],channelGroups:n=[]}=this.parameters,r={channels:{}};return 1===s.length&&0===n.length?r.channels[s[0]]=t.payload:r.channels=t.payload,r}))}get path(){const{keySet:{subscribeKey:e},uuid:t,channels:s}=this.parameters;return`/v2/presence/sub-key/${e}/channel/${$(null!=s?s:[],",")}/uuid/${t}`}get queryParameters(){const{channelGroups:e}=this.parameters;return e&&0!==e.length?{"channel-group":e.join(",")}:{}}}class Ut extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNSetStateOperation}validate(){const{keySet:{subscribeKey:e},state:t,channels:s=[],channelGroups:n=[]}=this.parameters;return e?t?0===(null==s?void 0:s.length)&&0===(null==n?void 0:n.length)?"Please provide a list of channels and/or channel-groups":void 0:"Missing State":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){return{state:this.deserializeResponse(e).payload}}))}get path(){const{keySet:{subscribeKey:e},uuid:t,channels:s}=this.parameters;return`/v2/presence/sub-key/${e}/channel/${$(null!=s?s:[],",")}/uuid/${R(t)}/data`}get queryParameters(){const{channelGroups:e,state:t}=this.parameters,s={state:JSON.stringify(t)};return e&&0!==e.length&&(s["channel-group"]=e.join(",")),s}}class Dt extends le{constructor(e){super({cancellable:!0}),this.parameters=e}operation(){return M.PNHeartbeatOperation}validate(){const{keySet:{subscribeKey:e},channels:t=[],channelGroups:s=[]}=this.parameters;return e?0===t.length&&0===s.length?"Please provide a list of channels and/or channel-groups":void 0:"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channels:t}=this.parameters;return`/v2/presence/sub-key/${e}/channel/${$(null!=t?t:[],",")}/heartbeat`}get queryParameters(){const{channelGroups:e,state:t,heartbeat:s}=this.parameters,n={heartbeat:`${s}`};return e&&0!==e.length&&(n["channel-group"]=e.join(",")),t&&(n.state=JSON.stringify(t)),n}}class Ft extends le{constructor(e){super(),this.parameters=e,this.parameters.channelGroups&&(this.parameters.channelGroups=Array.from(new Set(this.parameters.channelGroups))),this.parameters.channels&&(this.parameters.channels=Array.from(new Set(this.parameters.channels)))}operation(){return M.PNUnsubscribeOperation}validate(){const{keySet:{subscribeKey:e},channels:t=[],channelGroups:s=[]}=this.parameters;return e?0===t.length&&0===s.length?"At least one `channel` or `channel group` should be provided.":void 0:"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){var e;const{keySet:{subscribeKey:t},channels:s}=this.parameters;return`/v2/presence/sub-key/${t}/channel/${$(null!==(e=null==s?void 0:s.sort())&&void 0!==e?e:[],",")}/leave`}get queryParameters(){const{channelGroups:e}=this.parameters;return e&&0!==e.length?{"channel-group":e.sort().join(",")}:{}}}class Rt extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNWhereNowOperation}validate(){if(!this.parameters.keySet.subscribeKey)return"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e);return t.payload?{channels:t.payload.channels}:{channels:[]}}))}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/presence/sub-key/${e}/uuid/${R(t)}`}}class $t extends le{constructor(e){var t,s,n,r,i,a;super(),this.parameters=e,null!==(t=(r=this.parameters).queryParameters)&&void 0!==t||(r.queryParameters={}),null!==(s=(i=this.parameters).includeUUIDs)&&void 0!==s||(i.includeUUIDs=true),null!==(n=(a=this.parameters).includeState)&&void 0!==n||(a.includeState=false)}operation(){const{channels:e=[],channelGroups:t=[]}=this.parameters;return 0===e.length&&0===t.length?M.PNGlobalHereNowOperation:M.PNHereNowOperation}validate(){if(!this.parameters.keySet.subscribeKey)return"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){var t,s;const n=this.deserializeResponse(e),r="occupancy"in n?1:n.payload.total_channels,i="occupancy"in n?n.occupancy:n.payload.total_occupancy,a={};let o={};if("occupancy"in n){const e=this.parameters.channels[0];o[e]={uuids:null!==(t=n.uuids)&&void 0!==t?t:[],occupancy:i}}else o=null!==(s=n.payload.channels)&&void 0!==s?s:{};return Object.keys(o).forEach((e=>{const t=o[e];a[e]={occupants:this.parameters.includeUUIDs?t.uuids.map((e=>"string"==typeof e?{uuid:e,state:null}:e)):[],name:e,occupancy:t.occupancy}})),{totalChannels:r,totalOccupancy:i,channels:a}}))}get path(){const{keySet:{subscribeKey:e},channels:t,channelGroups:s}=this.parameters;let n=`/v2/presence/sub-key/${e}`;return(t&&t.length>0||s&&s.length>0)&&(n+=`/channel/${$(null!=t?t:[],",")}`),n}get queryParameters(){const{channelGroups:e,includeUUIDs:t,includeState:s,queryParameters:n}=this.parameters;return Object.assign(Object.assign(Object.assign(Object.assign({},t?{}:{disable_uuids:"1"}),null!=s&&s?{state:"1"}:{}),e&&e.length>0?{"channel-group":e.join(",")}:{}),n)}}class xt extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e}operation(){return M.PNDeleteMessagesOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channel?void 0:"Missing channel":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v3/history/sub-key/${e}/channel/${R(t)}`}get queryParameters(){const{start:e,end:t}=this.parameters;return Object.assign(Object.assign({},e?{start:e}:{}),t?{end:t}:{})}}class qt extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNMessageCounts}validate(){const{keySet:{subscribeKey:e},channels:t,timetoken:s,channelTimetokens:n}=this.parameters;return e?t?s&&n?"`timetoken` and `channelTimetokens` are incompatible together":s||n?n&&n.length>1&&n.length!==t.length?"Length of `channelTimetokens` and `channels` do not match":void 0:"`timetoken` or `channelTimetokens` need to be set":"Missing channels":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){return{channels:this.deserializeResponse(e).channels}}))}get path(){return`/v3/history/sub-key/${this.parameters.keySet.subscribeKey}/message-counts/${$(this.parameters.channels)}`}get queryParameters(){let{channelTimetokens:e}=this.parameters;return this.parameters.timetoken&&(e=[this.parameters.timetoken]),Object.assign(Object.assign({},1===e.length?{timetoken:e[0]}:{}),e.length>1?{channelsTimetoken:e.join(",")}:{})}}class Lt extends le{constructor(e){var t,s,n;super(),this.parameters=e,e.count?e.count=Math.min(e.count,100):e.count=100,null!==(t=e.stringifiedTimeToken)&&void 0!==t||(e.stringifiedTimeToken=false),null!==(s=e.includeMeta)&&void 0!==s||(e.includeMeta=false),null!==(n=e.logVerbosity)&&void 0!==n||(e.logVerbosity=false)}operation(){return M.PNHistoryOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channel?void 0:"Missing channel":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e),s=t[0],n=t[1],r=t[2];return Array.isArray(s)?{messages:s.map((e=>{const t=this.processPayload(e.message),s={entry:t.payload,timetoken:e.timetoken};return t.error&&(s.error=t.error),e.meta&&(s.meta=e.meta),s})),startTimeToken:n,endTimeToken:r}:{messages:[],startTimeToken:n,endTimeToken:r}}))}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/history/sub-key/${e}/channel/${R(t)}`}get queryParameters(){const{start:e,end:t,reverse:s,count:n,stringifiedTimeToken:r,includeMeta:i}=this.parameters;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:n,include_token:"true"},e?{start:e}:{}),t?{end:t}:{}),r?{string_message_token:"true"}:{}),null!=s?{reverse:s.toString()}:{}),i?{include_meta:"true"}:{})}processPayload(e){const{crypto:t,logVerbosity:s}=this.parameters;if(!t||"string"!=typeof e)return{payload:e};let n,r;try{const s=t.decrypt(e);n=s instanceof ArrayBuffer?JSON.parse(Lt.decoder.decode(s)):s}catch(t){s&&console.log("decryption error",t.message),n=e,r=`Error while decrypting message content: ${t.message}`}return{payload:n,error:r}}}var Gt;!function(e){e[e.Message=-1]="Message",e[e.Files=4]="Files"}(Gt||(Gt={}));class Kt extends le{constructor(e){var t,s,n,r,i;super(),this.parameters=e;const a=null!==(t=e.includeMessageActions)&&void 0!==t&&t,o=e.channels.length>1||a?25:100;e.count?e.count=Math.min(e.count,o):e.count=o,e.includeUuid?e.includeUUID=e.includeUuid:null!==(s=e.includeUUID)&&void 0!==s||(e.includeUUID=true),null!==(n=e.stringifiedTimeToken)&&void 0!==n||(e.stringifiedTimeToken=false),null!==(r=e.includeMessageType)&&void 0!==r||(e.includeMessageType=true),null!==(i=e.logVerbosity)&&void 0!==i||(e.logVerbosity=false)}operation(){return M.PNFetchMessagesOperation}validate(){const{keySet:{subscribeKey:e},channels:t,includeMessageActions:s}=this.parameters;return e?t?void 0!==s&&s&&t.length>1?"History can return actions data for a single channel only. Either pass a single channel or disable the includeMessageActions flag.":void 0:"Missing channels":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){var t;const s=this.deserializeResponse(e),n=null!==(t=s.channels)&&void 0!==t?t:{},r={};return Object.keys(n).forEach((e=>{r[e]=n[e].map((t=>{null===t.message_type&&(t.message_type=Gt.Message);const s=this.processPayload(e,t),n=Object.assign(Object.assign({channel:e,timetoken:t.timetoken,message:s.payload,messageType:t.message_type},t.custom_message_type?{customMessageType:t.custom_message_type}:{}),{uuid:t.uuid});if(t.actions){const e=n;e.actions=t.actions,e.data=t.actions}return t.meta&&(n.meta=t.meta),s.error&&(n.error=s.error),n}))})),s.more?{channels:r,more:s.more}:{channels:r}}))}get path(){const{keySet:{subscribeKey:e},channels:t,includeMessageActions:s}=this.parameters;return`/v3/${s?"history-with-actions":"history"}/sub-key/${e}/channel/${$(t)}`}get queryParameters(){const{start:e,end:t,count:s,includeCustomMessageType:n,includeMessageType:r,includeMeta:i,includeUUID:a,stringifiedTimeToken:o}=this.parameters;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({max:s},e?{start:e}:{}),t?{end:t}:{}),o?{string_message_token:"true"}:{}),void 0!==i&&i?{include_meta:"true"}:{}),a?{include_uuid:"true"}:{}),null!=n?{include_custom_message_type:n?"true":"false"}:{}),r?{include_message_type:"true"}:{})}processPayload(e,t){const{crypto:s,logVerbosity:n}=this.parameters;if(!s||"string"!=typeof t.message)return{payload:t.message};let r,i;try{const e=s.decrypt(t.message);r=e instanceof ArrayBuffer?JSON.parse(Kt.decoder.decode(e)):e}catch(e){n&&console.log("decryption error",e.message),r=t.message,i=`Error while decrypting message content: ${e.message}`}if(!i&&r&&t.message_type==Gt.Files&&"object"==typeof r&&this.isFileMessage(r)){const t=r;return{payload:{message:t.message,file:Object.assign(Object.assign({},t.file),{url:this.parameters.getFileUrl({channel:e,id:t.file.id,name:t.file.name})})},error:i}}return{payload:r,error:i}}isFileMessage(e){return void 0!==e.file}}class Ht extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNGetMessageActionsOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channel?void 0:"Missing message channel":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e);let s=null,n=null;return t.data.length>0&&(s=t.data[0].actionTimetoken,n=t.data[t.data.length-1].actionTimetoken),{data:t.data,more:t.more,start:s,end:n}}))}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v1/message-actions/${e}/channel/${R(t)}`}get queryParameters(){const{limit:e,start:t,end:s}=this.parameters;return Object.assign(Object.assign(Object.assign({},t?{start:t}:{}),s?{end:s}:{}),e?{limit:e}:{})}}class Bt extends le{constructor(e){super({method:ae.POST}),this.parameters=e}operation(){return M.PNAddMessageActionOperation}validate(){const{keySet:{subscribeKey:e},action:t,channel:s,messageTimetoken:n}=this.parameters;return e?s?n?t?t.value?t.type?t.type.length>15?"Action.type value exceed maximum length of 15":void 0:"Missing Action.type":"Missing Action.value":"Missing Action":"Missing message timetoken":"Missing message channel":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((({data:e})=>({data:e})))}))}get path(){const{keySet:{subscribeKey:e},channel:t,messageTimetoken:s}=this.parameters;return`/v1/message-actions/${e}/channel/${R(t)}/message/${s}`}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"})}get body(){return JSON.stringify(this.parameters.action)}}class Wt extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e}operation(){return M.PNRemoveMessageActionOperation}validate(){const{keySet:{subscribeKey:e},channel:t,messageTimetoken:s,actionTimetoken:n}=this.parameters;return e?t?s?n?void 0:"Missing action timetoken":"Missing message timetoken":"Missing message action channel":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((({data:e})=>({data:e})))}))}get path(){const{keySet:{subscribeKey:e},channel:t,actionTimetoken:s,messageTimetoken:n}=this.parameters;return`/v1/message-actions/${e}/channel/${R(t)}/message/${n}/action/${s}`}}class zt extends le{constructor(e){var t,s;super(),this.parameters=e,null!==(t=(s=this.parameters).storeInHistory)&&void 0!==t||(s.storeInHistory=true)}operation(){return M.PNPublishFileMessageOperation}validate(){const{channel:e,fileId:t,fileName:s}=this.parameters;return e?t?s?void 0:"file name can't be empty":"file id can't be empty":"channel can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){return{timetoken:this.deserializeResponse(e)[2]}}))}get path(){const{message:e,channel:t,keySet:{publishKey:s,subscribeKey:n},fileId:r,fileName:i}=this.parameters,a=Object.assign({file:{name:i,id:r}},e?{message:e}:{});return`/v1/files/publish-file/${s}/${n}/0/${R(t)}/0/${R(this.prepareMessagePayload(a))}`}get queryParameters(){const{customMessageType:e,storeInHistory:t,ttl:s,meta:n}=this.parameters;return Object.assign(Object.assign(Object.assign({store:t?"1":"0"},e?{custom_message_type:e}:{}),s?{ttl:s}:{}),n&&"object"==typeof n?{meta:JSON.stringify(n)}:{})}prepareMessagePayload(e){const{crypto:t}=this.parameters;if(!t)return JSON.stringify(e)||"";const s=t.encrypt(JSON.stringify(e));return JSON.stringify("string"==typeof s?s:u(s))}}class Vt extends le{constructor(e){super({method:ae.LOCAL}),this.parameters=e}operation(){return M.PNGetFileUrlOperation}validate(){const{channel:e,id:t,name:s}=this.parameters;return e?t?s?void 0:"file name can't be empty":"file id can't be empty":"channel can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){return e.url}))}get path(){const{channel:e,id:t,name:s,keySet:{subscribeKey:n}}=this.parameters;return`/v1/files/${n}/channels/${R(e)}/files/${t}/${s}`}}class Jt extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e}operation(){return M.PNDeleteFileOperation}validate(){const{channel:e,id:t,name:s}=this.parameters;return e?t?s?void 0:"file name can't be empty":"file id can't be empty":"channel can't be empty"}get path(){const{keySet:{subscribeKey:e},id:t,channel:s,name:n}=this.parameters;return`/v1/files/${e}/channels/${R(s)}/files/${t}/${n}`}}class Xt extends le{constructor(e){var t,s;super(),this.parameters=e,null!==(t=(s=this.parameters).limit)&&void 0!==t||(s.limit=100)}operation(){return M.PNListFilesOperation}validate(){if(!this.parameters.channel)return"channel can't be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v1/files/${e}/channels/${R(t)}/files`}get queryParameters(){const{limit:e,next:t}=this.parameters;return Object.assign({limit:e},t?{next:t}:{})}}class Qt extends le{constructor(e){super({method:ae.POST}),this.parameters=e}operation(){return M.PNGenerateUploadUrlOperation}validate(){return this.parameters.channel?this.parameters.name?void 0:"'name' can't be empty":"channel can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){const t=this.deserializeResponse(e);return{id:t.data.id,name:t.data.name,url:t.file_upload_request.url,formFields:t.file_upload_request.form_fields}}))}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v1/files/${e}/channels/${R(t)}/generate-upload-url`}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"})}get body(){return JSON.stringify({name:this.parameters.name})}}class Yt extends le{constructor(e){super({method:ae.POST}),this.parameters=e;const t=e.file.mimeType;t&&(e.formFields=e.formFields.map((e=>"Content-Type"===e.name?{name:e.name,value:t}:e)))}operation(){return M.PNPublishFileOperation}validate(){const{fileId:e,fileName:t,file:s,uploadUrl:n}=this.parameters;return e?t?s?n?void 0:"Validation failed: file upload 'url' can't be empty":"Validation failed: 'file' can't be empty":"Validation failed: file 'name' can't be empty":"Validation failed: file 'id' can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){return{status:e.status,message:e.body?Yt.decoder.decode(e.body):"OK"}}))}request(){return Object.assign(Object.assign({},super.request()),{origin:new URL(this.parameters.uploadUrl).origin,timeout:300})}get path(){const{pathname:e,search:t}=new URL(this.parameters.uploadUrl);return`${e}${t}`}get body(){return this.parameters.file}get formData(){return this.parameters.formFields}}class Zt{constructor(e){var t;if(this.parameters=e,this.file=null===(t=this.parameters.PubNubFile)||void 0===t?void 0:t.create(e.file),!this.file)throw new Error("File upload error: unable to create File object.")}process(){return i(this,void 0,void 0,(function*(){let e,t;return this.generateFileUploadUrl().then((s=>(e=s.name,t=s.id,this.uploadFile(s)))).then((e=>{if(204!==e.status)throw new d("Upload to bucket was unsuccessful",{error:!0,statusCode:e.status,category:h.PNUnknownCategory,operation:M.PNPublishFileOperation,errorData:{message:e.message}})})).then((()=>this.publishFileMessage(t,e))).catch((e=>{if(e instanceof d)throw e;const t=e instanceof I?e:I.create(e);throw new d("File upload error.",t.toStatus(M.PNPublishFileOperation))}))}))}generateFileUploadUrl(){return i(this,void 0,void 0,(function*(){const e=new Qt(Object.assign(Object.assign({},this.parameters),{name:this.file.name,keySet:this.parameters.keySet}));return this.parameters.sendRequest(e)}))}uploadFile(e){return i(this,void 0,void 0,(function*(){const{cipherKey:t,PubNubFile:s,crypto:n,cryptography:r}=this.parameters,{id:i,name:a,url:o,formFields:c}=e;return this.parameters.PubNubFile.supportsEncryptFile&&(!t&&n?this.file=yield n.encryptFile(this.file,s):t&&r&&(this.file=yield r.encryptFile(t,this.file,s))),this.parameters.sendRequest(new Yt({fileId:i,fileName:a,file:this.file,uploadUrl:o,formFields:c}))}))}publishFileMessage(e,t){return i(this,void 0,void 0,(function*(){var s,n,r,i;let a,o={timetoken:"0"},c=this.parameters.fileUploadPublishRetryLimit,u=!1;do{try{o=yield this.parameters.publishFile(Object.assign(Object.assign({},this.parameters),{fileId:e,fileName:t})),u=!0}catch(e){e instanceof d&&(a=e),c-=1}}while(!u&&c>0);if(u)return{status:200,timetoken:o.timetoken,id:e,name:t};throw new d("Publish failed. You may want to execute that operation manually using pubnub.publishFile",{error:!0,category:null!==(n=null===(s=a.status)||void 0===s?void 0:s.category)&&void 0!==n?n:h.PNUnknownCategory,statusCode:null!==(i=null===(r=a.status)||void 0===r?void 0:r.statusCode)&&void 0!==i?i:0,channel:this.parameters.channel,id:e,name:t})}))}}class es{constructor(e,t){this.subscriptionStateIds=[],this.client=t,this._nameOrId=e}get entityType(){return"Channel"}get subscriptionType(){return Pt.Channel}subscriptionNames(e){return[this._nameOrId,...e&&!this._nameOrId.endsWith("-pnpres")?[`${this._nameOrId}-pnpres`]:[]]}subscription(e){return new Mt({client:this.client,entity:this,options:e})}get subscriptionsCount(){return this.subscriptionStateIds.length}increaseSubscriptionCount(e){this.subscriptionStateIds.includes(e)||this.subscriptionStateIds.push(e)}decreaseSubscriptionCount(e){{const t=this.subscriptionStateIds.indexOf(e);t>=0&&this.subscriptionStateIds.splice(t,1)}}toString(){return`${this.entityType} { nameOrId: ${this._nameOrId}, subscriptionsCount: ${this.subscriptionsCount} }`}}class ts extends es{get entityType(){return"ChannelMetadata"}get id(){return this._nameOrId}subscriptionNames(e){return[this.id]}}class ss extends es{get entityType(){return"ChannelGroups"}get name(){return this._nameOrId}get subscriptionType(){return Pt.ChannelGroup}}class ns extends es{get entityType(){return"UserMetadata"}get id(){return this._nameOrId}subscriptionNames(e){return[this.id]}}class rs extends es{get entityType(){return"Channel"}get name(){return this._nameOrId}}class is extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNRemoveChannelsFromGroupOperation}validate(){const{keySet:{subscribeKey:e},channels:t,channelGroup:s}=this.parameters;return e?s?t?void 0:"Missing channels":"Missing Channel Group":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channelGroup:t}=this.parameters;return`/v1/channel-registration/sub-key/${e}/channel-group/${R(t)}`}get queryParameters(){return{remove:this.parameters.channels.join(",")}}}class as extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNAddChannelsToGroupOperation}validate(){const{keySet:{subscribeKey:e},channels:t,channelGroup:s}=this.parameters;return e?s?t?void 0:"Missing channels":"Missing Channel Group":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channelGroup:t}=this.parameters;return`/v1/channel-registration/sub-key/${e}/channel-group/${R(t)}`}get queryParameters(){return{add:this.parameters.channels.join(",")}}}class os extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNChannelsForGroupOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channelGroup?void 0:"Missing Channel Group":"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){return{channels:this.deserializeResponse(e).payload.channels}}))}get path(){const{keySet:{subscribeKey:e},channelGroup:t}=this.parameters;return`/v1/channel-registration/sub-key/${e}/channel-group/${R(t)}`}}class cs extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNRemoveGroupOperation}validate(){return this.parameters.keySet.subscribeKey?this.parameters.channelGroup?void 0:"Missing Channel Group":"Missing Subscribe Key"}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}get path(){const{keySet:{subscribeKey:e},channelGroup:t}=this.parameters;return`/v1/channel-registration/sub-key/${e}/channel-group/${R(t)}/remove`}}class us extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNChannelGroupsOperation}validate(){if(!this.parameters.keySet.subscribeKey)return"Missing Subscribe Key"}parse(e){return i(this,void 0,void 0,(function*(){return{groups:this.deserializeResponse(e).payload.groups}}))}get path(){return`/v1/channel-registration/sub-key/${this.parameters.keySet.subscribeKey}/channel-group`}}class ls{constructor(e,t,s){this.sendRequest=s,this.logger=e,this.keySet=t}listChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"List channel group channels with parameters:"})));const s=new os(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=e=>{e&&this.logger.info("PubNub",`List channel group channels success. Received ${e.channels.length} channels.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}listGroups(e){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub","List all channel groups.");const t=new us({keySet:this.keySet}),s=e=>{e&&this.logger.info("PubNub",`List all channel groups success. Received ${e.groups.length} groups.`)};return e?this.sendRequest(t,((t,n)=>{s(n),e(t,n)})):this.sendRequest(t).then((e=>(s(e),e)))}))}addChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Add channels to the channel group with parameters:"})));const s=new as(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.info("PubNub","Add channels to the channel group success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}removeChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove channels from the channel group with parameters:"})));const s=new is(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.info("PubNub","Remove channels from the channel group success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}deleteGroup(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove a channel group with parameters:"})));const s=new cs(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.info("PubNub",`Remove a channel group success. Removed '${e.channelGroup}' channel group.'`)};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}}class hs extends le{constructor(e){var t,s;super(),this.parameters=e,"apns2"===this.parameters.pushGateway&&(null!==(t=(s=this.parameters).environment)&&void 0!==t||(s.environment="development")),this.parameters.count&&this.parameters.count>1e3&&(this.parameters.count=1e3)}operation(){throw Error("Should be implemented in subclass.")}validate(){const{keySet:{subscribeKey:e},action:t,device:s,pushGateway:n}=this.parameters;return e?s?"add"!==t&&"remove"!==t||"channels"in this.parameters&&0!==this.parameters.channels.length?n?"apns2"!==this.parameters.pushGateway||this.parameters.topic?void 0:"Missing APNS2 topic":"Missing GW Type (pushGateway: gcm or apns2)":"Missing Channels":"Missing Device ID (device)":"Missing Subscribe Key"}get path(){const{keySet:{subscribeKey:e},action:t,device:s,pushGateway:n}=this.parameters;let r="apns2"===n?`/v2/push/sub-key/${e}/devices-apns2/${s}`:`/v1/push/sub-key/${e}/devices/${s}`;return"remove-device"===t&&(r=`${r}/remove`),r}get queryParameters(){const{start:e,count:t}=this.parameters;let s=Object.assign(Object.assign({type:this.parameters.pushGateway},e?{start:e}:{}),t&&t>0?{count:t}:{});if("channels"in this.parameters&&(s[this.parameters.action]=this.parameters.channels.join(",")),"apns2"===this.parameters.pushGateway){const{environment:e,topic:t}=this.parameters;s=Object.assign(Object.assign({},s),{environment:e,topic:t})}return s}}class ds extends hs{constructor(e){super(Object.assign(Object.assign({},e),{action:"remove"}))}operation(){return M.PNRemovePushNotificationEnabledChannelsOperation}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}}class ps extends hs{constructor(e){super(Object.assign(Object.assign({},e),{action:"list"}))}operation(){return M.PNPushNotificationEnabledChannelsOperation}parse(e){return i(this,void 0,void 0,(function*(){return{channels:this.deserializeResponse(e)}}))}}class gs extends hs{constructor(e){super(Object.assign(Object.assign({},e),{action:"add"}))}operation(){return M.PNAddPushNotificationEnabledChannelsOperation}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}}class bs extends hs{constructor(e){super(Object.assign(Object.assign({},e),{action:"remove-device"}))}operation(){return M.PNRemoveAllPushNotificationsOperation}parse(e){const t=Object.create(null,{parse:{get:()=>super.parse}});return i(this,void 0,void 0,(function*(){return t.parse.call(this,e).then((e=>({})))}))}}class ys{constructor(e,t,s){this.sendRequest=s,this.logger=e,this.keySet=t}listChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"List push-enabled channels with parameters:"})));const s=new ps(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=e=>{e&&this.logger.debug("PubNub",`List push-enabled channels success. Received ${e.channels.length} channels.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}addChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Add push-enabled channels with parameters:"})));const s=new gs(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.debug("PubNub","Add push-enabled channels success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}removeChannels(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove push-enabled channels with parameters:"})));const s=new ds(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.debug("PubNub","Remove push-enabled channels success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}deleteDevice(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove push notifications for device with parameters:"})));const s=new bs(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=()=>{this.logger.debug("PubNub","Remove push notifications for device success.")};return t?this.sendRequest(s,(e=>{e.error||n(),t(e)})):this.sendRequest(s).then((e=>(n(),e)))}))}}class ms extends le{constructor(e){var t,s,n,r,i,a;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(i=e.include).customFields)&&void 0!==s||(i.customFields=false),null!==(n=(a=e.include).totalCount)&&void 0!==n||(a.totalCount=false),null!==(r=e.limit)&&void 0!==r||(e.limit=100)}operation(){return M.PNGetAllChannelMetadataOperation}get path(){return`/v2/objects/${this.parameters.keySet.subscribeKey}/channels`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";return i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e)),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({include:["status","type",...e.customFields?["custom"]:[]].join(","),count:`${e.totalCount}`},s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}}class fs extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e}operation(){return M.PNRemoveChannelMetadataOperation}validate(){if(!this.parameters.channel)return"Channel cannot be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${R(t)}`}}class vs extends le{constructor(e){var t,s,n,r,i,a,o,c,u,l,h,d,p,g,b,y,m,f;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(h=e.include).customFields)&&void 0!==s||(h.customFields=false),null!==(n=(d=e.include).totalCount)&&void 0!==n||(d.totalCount=false),null!==(r=(p=e.include).statusField)&&void 0!==r||(p.statusField=false),null!==(i=(g=e.include).typeField)&&void 0!==i||(g.typeField=false),null!==(a=(b=e.include).channelFields)&&void 0!==a||(b.channelFields=false),null!==(o=(y=e.include).customChannelFields)&&void 0!==o||(y.customChannelFields=false),null!==(c=(m=e.include).channelStatusField)&&void 0!==c||(m.channelStatusField=false),null!==(u=(f=e.include).channelTypeField)&&void 0!==u||(f.channelTypeField=false),null!==(l=e.limit)&&void 0!==l||(e.limit=100),this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNGetMembershipsOperation}validate(){if(!this.parameters.uuid)return"'uuid' cannot be empty"}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${R(t)}/channels`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e));const a=[];return e.statusField&&a.push("status"),e.typeField&&a.push("type"),e.customFields&&a.push("custom"),e.channelFields&&a.push("channel"),e.channelStatusField&&a.push("channel.status"),e.channelTypeField&&a.push("channel.type"),e.customChannelFields&&a.push("channel.custom"),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:`${e.totalCount}`},a.length>0?{include:a.join(",")}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}}class Ss extends le{constructor(e){var t,s,n,r,i,a,o,c,u,l,h,d,p,g,b,y,m,f;super({method:ae.PATCH}),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(h=e.include).customFields)&&void 0!==s||(h.customFields=false),null!==(n=(d=e.include).totalCount)&&void 0!==n||(d.totalCount=false),null!==(r=(p=e.include).statusField)&&void 0!==r||(p.statusField=false),null!==(i=(g=e.include).typeField)&&void 0!==i||(g.typeField=false),null!==(a=(b=e.include).channelFields)&&void 0!==a||(b.channelFields=false),null!==(o=(y=e.include).customChannelFields)&&void 0!==o||(y.customChannelFields=false),null!==(c=(m=e.include).channelStatusField)&&void 0!==c||(m.channelStatusField=false),null!==(u=(f=e.include).channelTypeField)&&void 0!==u||(f.channelTypeField=false),null!==(l=e.limit)&&void 0!==l||(e.limit=100),this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNSetMembershipsOperation}validate(){const{uuid:e,channels:t}=this.parameters;return e?t&&0!==t.length?void 0:"Channels cannot be empty":"'uuid' cannot be empty"}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${R(t)}/channels`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e));const a=["channel.status","channel.type","status"];return e.statusField&&a.push("status"),e.typeField&&a.push("type"),e.customFields&&a.push("custom"),e.channelFields&&a.push("channel"),e.channelStatusField&&a.push("channel.status"),e.channelTypeField&&a.push("channel.type"),e.customChannelFields&&a.push("channel.custom"),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:`${e.totalCount}`},a.length>0?{include:a.join(",")}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"})}get body(){const{channels:e,type:t}=this.parameters;return JSON.stringify({[`${t}`]:e.map((e=>"string"==typeof e?{channel:{id:e}}:{channel:{id:e.id},status:e.status,type:e.type,custom:e.custom}))})}}class ws extends le{constructor(e){var t,s,n,r;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(r=e.include).customFields)&&void 0!==s||(r.customFields=false),null!==(n=e.limit)&&void 0!==n||(e.limit=100)}operation(){return M.PNGetAllUUIDMetadataOperation}get path(){return`/v2/objects/${this.parameters.keySet.subscribeKey}/uuids`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";return i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e)),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({include:["status","type",...e.customFields?["custom"]:[]].join(",")},void 0!==e.totalCount?{count:`${e.totalCount}`}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}}class Os extends le{constructor(e){var t,s,n;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(n=e.include).customFields)&&void 0!==s||(n.customFields=true)}operation(){return M.PNGetChannelMetadataOperation}validate(){if(!this.parameters.channel)return"Channel cannot be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${R(t)}`}get queryParameters(){return{include:["status","type",...this.parameters.include.customFields?["custom"]:[]].join(",")}}}class ks extends le{constructor(e){var t,s,n;super({method:ae.PATCH}),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(n=e.include).customFields)&&void 0!==s||(n.customFields=true)}operation(){return M.PNSetChannelMetadataOperation}validate(){return this.parameters.channel?this.parameters.data?void 0:"Data cannot be empty":"Channel cannot be empty"}get headers(){var e;let t=null!==(e=super.headers)&&void 0!==e?e:{};return this.parameters.ifMatchesEtag&&(t=Object.assign(Object.assign({},t),{"If-Match":this.parameters.ifMatchesEtag})),Object.assign(Object.assign({},t),{"Content-Type":"application/json"})}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${R(t)}`}get queryParameters(){return{include:["status","type",...this.parameters.include.customFields?["custom"]:[]].join(",")}}get body(){return JSON.stringify(this.parameters.data)}}class Cs extends le{constructor(e){super({method:ae.DELETE}),this.parameters=e,this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNRemoveUUIDMetadataOperation}validate(){if(!this.parameters.uuid)return"'uuid' cannot be empty"}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${R(t)}`}}class Ps extends le{constructor(e){var t,s,n,r,i,a,o,c,u,l,h,d,p,g,b,y,m,f;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(h=e.include).customFields)&&void 0!==s||(h.customFields=false),null!==(n=(d=e.include).totalCount)&&void 0!==n||(d.totalCount=false),null!==(r=(p=e.include).statusField)&&void 0!==r||(p.statusField=false),null!==(i=(g=e.include).typeField)&&void 0!==i||(g.typeField=false),null!==(a=(b=e.include).UUIDFields)&&void 0!==a||(b.UUIDFields=false),null!==(o=(y=e.include).customUUIDFields)&&void 0!==o||(y.customUUIDFields=false),null!==(c=(m=e.include).UUIDStatusField)&&void 0!==c||(m.UUIDStatusField=false),null!==(u=(f=e.include).UUIDTypeField)&&void 0!==u||(f.UUIDTypeField=false),null!==(l=e.limit)&&void 0!==l||(e.limit=100)}operation(){return M.PNSetMembersOperation}validate(){if(!this.parameters.channel)return"Channel cannot be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${R(t)}/uuids`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e));const a=[];return e.statusField&&a.push("status"),e.typeField&&a.push("type"),e.customFields&&a.push("custom"),e.UUIDFields&&a.push("uuid"),e.UUIDStatusField&&a.push("uuid.status"),e.UUIDTypeField&&a.push("uuid.type"),e.customUUIDFields&&a.push("uuid.custom"),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:`${e.totalCount}`},a.length>0?{include:a.join(",")}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}}class js extends le{constructor(e){var t,s,n,r,i,a,o,c,u,l,h,d,p,g,b,y,m,f;super({method:ae.PATCH}),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(h=e.include).customFields)&&void 0!==s||(h.customFields=false),null!==(n=(d=e.include).totalCount)&&void 0!==n||(d.totalCount=false),null!==(r=(p=e.include).statusField)&&void 0!==r||(p.statusField=false),null!==(i=(g=e.include).typeField)&&void 0!==i||(g.typeField=false),null!==(a=(b=e.include).UUIDFields)&&void 0!==a||(b.UUIDFields=false),null!==(o=(y=e.include).customUUIDFields)&&void 0!==o||(y.customUUIDFields=false),null!==(c=(m=e.include).UUIDStatusField)&&void 0!==c||(m.UUIDStatusField=false),null!==(u=(f=e.include).UUIDTypeField)&&void 0!==u||(f.UUIDTypeField=false),null!==(l=e.limit)&&void 0!==l||(e.limit=100)}operation(){return M.PNSetMembersOperation}validate(){const{channel:e,uuids:t}=this.parameters;return e?t&&0!==t.length?void 0:"UUIDs cannot be empty":"Channel cannot be empty"}get path(){const{keySet:{subscribeKey:e},channel:t}=this.parameters;return`/v2/objects/${e}/channels/${R(t)}/uuids`}get queryParameters(){const{include:e,page:t,filter:s,sort:n,limit:r}=this.parameters;let i="";i="string"==typeof n?n:Object.entries(null!=n?n:{}).map((([e,t])=>null!==t?`${e}:${t}`:e));const a=["uuid.status","uuid.type","type"];return e.statusField&&a.push("status"),e.typeField&&a.push("type"),e.customFields&&a.push("custom"),e.UUIDFields&&a.push("uuid"),e.UUIDStatusField&&a.push("uuid.status"),e.UUIDTypeField&&a.push("uuid.type"),e.customUUIDFields&&a.push("uuid.custom"),Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({count:`${e.totalCount}`},a.length>0?{include:a.join(",")}:{}),s?{filter:s}:{}),(null==t?void 0:t.next)?{start:t.next}:{}),(null==t?void 0:t.prev)?{end:t.prev}:{}),r?{limit:r}:{}),i.length?{sort:i}:{})}get headers(){var e;return Object.assign(Object.assign({},null!==(e=super.headers)&&void 0!==e?e:{}),{"Content-Type":"application/json"})}get body(){const{uuids:e,type:t}=this.parameters;return JSON.stringify({[`${t}`]:e.map((e=>"string"==typeof e?{uuid:{id:e}}:{uuid:{id:e.id},status:e.status,type:e.type,custom:e.custom}))})}}class Es extends le{constructor(e){var t,s,n;super(),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(n=e.include).customFields)&&void 0!==s||(n.customFields=true),this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNGetUUIDMetadataOperation}validate(){if(!this.parameters.uuid)return"'uuid' cannot be empty"}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${R(t)}`}get queryParameters(){const{include:e}=this.parameters;return{include:["status","type",...e.customFields?["custom"]:[]].join(",")}}}class Ns extends le{constructor(e){var t,s,n;super({method:ae.PATCH}),this.parameters=e,null!==(t=e.include)&&void 0!==t||(e.include={}),null!==(s=(n=e.include).customFields)&&void 0!==s||(n.customFields=true),this.parameters.userId&&(this.parameters.uuid=this.parameters.userId)}operation(){return M.PNSetUUIDMetadataOperation}validate(){return this.parameters.uuid?this.parameters.data?void 0:"Data cannot be empty":"'uuid' cannot be empty"}get headers(){var e;let t=null!==(e=super.headers)&&void 0!==e?e:{};return this.parameters.ifMatchesEtag&&(t=Object.assign(Object.assign({},t),{"If-Match":this.parameters.ifMatchesEtag})),Object.assign(Object.assign({},t),{"Content-Type":"application/json"})}get path(){const{keySet:{subscribeKey:e},uuid:t}=this.parameters;return`/v2/objects/${e}/uuids/${R(t)}`}get queryParameters(){return{include:["status","type",...this.parameters.include.customFields?["custom"]:[]].join(",")}}get body(){return JSON.stringify(this.parameters.data)}}class Ts{constructor(e,t){this.keySet=e.keySet,this.configuration=e,this.sendRequest=t}get logger(){return this.configuration.logger()}getAllUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{},details:"Get all UUID metadata objects with parameters:"}))),this._getAllUUIDMetadata(e,t)}))}_getAllUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0);const n=new ws(Object.assign(Object.assign({},s),{keySet:this.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Get all UUID metadata success. Received ${e.totalCount} UUID metadata objects.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}getUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{uuid:this.configuration.userId},details:`Get ${e&&"function"!=typeof e?"":" current"} UUID metadata object with parameters:`}))),this._getUUIDMetadata(e,t)}))}_getUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){var s;const n=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0),n.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),n.uuid=n.userId),null!==(s=n.uuid)&&void 0!==s||(n.uuid=this.configuration.userId);const r=new Es(Object.assign(Object.assign({},n),{keySet:this.keySet})),i=e=>{e&&this.logger.debug("PubNub",`Get UUID metadata object success. Received '${n.uuid}' UUID metadata object.`)};return t?this.sendRequest(r,((e,s)=>{i(s),t(e,s)})):this.sendRequest(r).then((e=>(i(e),e)))}))}setUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set UUID metadata object with parameters:"}))),this._setUUIDMetadata(e,t)}))}_setUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){var s;e.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),e.uuid=e.userId),null!==(s=e.uuid)&&void 0!==s||(e.uuid=this.configuration.userId);const n=new Ns(Object.assign(Object.assign({},e),{keySet:this.keySet})),r=t=>{t&&this.logger.debug("PubNub",`Set UUID metadata object success. Updated '${e.uuid}' UUID metadata object.'`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}removeUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{uuid:this.configuration.userId},details:`Remove${e&&"function"!=typeof e?"":" current"} UUID metadata object with parameters:`}))),this._removeUUIDMetadata(e,t)}))}_removeUUIDMetadata(e,t){return i(this,void 0,void 0,(function*(){var s;const n=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0),n.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),n.uuid=n.userId),null!==(s=n.uuid)&&void 0!==s||(n.uuid=this.configuration.userId);const r=new Cs(Object.assign(Object.assign({},n),{keySet:this.keySet})),i=e=>{e&&this.logger.debug("PubNub",`Remove UUID metadata object success. Removed '${n.uuid}' UUID metadata object.`)};return t?this.sendRequest(r,((e,s)=>{i(s),t(e,s)})):this.sendRequest(r).then((e=>(i(e),e)))}))}getAllChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{},details:"Get all Channel metadata objects with parameters:"}))),this._getAllChannelMetadata(e,t)}))}_getAllChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0);const n=new ms(Object.assign(Object.assign({},s),{keySet:this.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Get all Channel metadata objects success. Received ${e.totalCount} Channel metadata objects.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}getChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get Channel metadata object with parameters:"}))),this._getChannelMetadata(e,t)}))}_getChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=new Os(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Get Channel metadata object success. Received '${e.channel}' Channel metadata object.'`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}setChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set Channel metadata object with parameters:"}))),this._setChannelMetadata(e,t)}))}_setChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=new ks(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Set Channel metadata object success. Updated '${e.channel}' Channel metadata object.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}removeChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove Channel metadata object with parameters:"}))),this._removeChannelMetadata(e,t)}))}_removeChannelMetadata(e,t){return i(this,void 0,void 0,(function*(){const s=new fs(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Remove Channel metadata object success. Removed '${e.channel}' Channel metadata object.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}getChannelMembers(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get channel members with parameters:"})));const s=new Ps(Object.assign(Object.assign({},e),{keySet:this.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Get channel members success. Received ${e.totalCount} channel members.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}setChannelMembers(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set channel members with parameters:"})));const s=new js(Object.assign(Object.assign({},e),{type:"set",keySet:this.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Set channel members success. There are ${e.totalCount} channel members now.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}removeChannelMembers(e,t){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove channel members with parameters:"})));const s=new js(Object.assign(Object.assign({},e),{type:"delete",keySet:this.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Remove channel members success. There are ${e.totalCount} channel members now.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}))}getMemberships(e,t){return i(this,void 0,void 0,(function*(){var s;const n=e&&"function"!=typeof e?e:{};null!=t||(t="function"==typeof e?e:void 0),n.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),n.uuid=n.userId),null!==(s=n.uuid)&&void 0!==s||(n.uuid=this.configuration.userId),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},n),details:"Get memberships with parameters:"})));const r=new vs(Object.assign(Object.assign({},n),{keySet:this.keySet})),i=e=>{e&&this.logger.debug("PubNub",`Get memberships success. Received ${e.totalCount} memberships.`)};return t?this.sendRequest(r,((e,s)=>{i(s),t(e,s)})):this.sendRequest(r).then((e=>(i(e),e)))}))}setMemberships(e,t){return i(this,void 0,void 0,(function*(){var s;e.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),e.uuid=e.userId),null!==(s=e.uuid)&&void 0!==s||(e.uuid=this.configuration.userId),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set memberships with parameters:"})));const n=new Ss(Object.assign(Object.assign({},e),{type:"set",keySet:this.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Set memberships success. There are ${e.totalCount} memberships now.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}removeMemberships(e,t){return i(this,void 0,void 0,(function*(){var s;e.userId&&(this.logger.warn("PubNub","'userId' parameter is deprecated. Use 'uuid' instead."),e.uuid=e.userId),null!==(s=e.uuid)&&void 0!==s||(e.uuid=this.configuration.userId),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove memberships with parameters:"})));const n=new Ss(Object.assign(Object.assign({},e),{type:"delete",keySet:this.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Remove memberships success. There are ${e.totalCount} memberships now.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}))}fetchMemberships(e,t){return i(this,void 0,void 0,(function*(){var s,n;if(this.logger.warn("PubNub","'fetchMemberships' is deprecated. Use 'pubnub.objects.getChannelMembers' or 'pubnub.objects.getMemberships' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fetch memberships with parameters:"}))),"spaceId"in e){const n=e,r={channel:null!==(s=n.spaceId)&&void 0!==s?s:n.channel,filter:n.filter,limit:n.limit,page:n.page,include:Object.assign({},n.include),sort:n.sort?Object.fromEntries(Object.entries(n.sort).map((([e,t])=>[e.replace("user","uuid"),t]))):void 0},i=e=>({status:e.status,data:e.data.map((e=>({user:e.uuid,custom:e.custom,updated:e.updated,eTag:e.eTag}))),totalCount:e.totalCount,next:e.next,prev:e.prev});return t?this.getChannelMembers(r,((e,s)=>{t(e,s?i(s):s)})):this.getChannelMembers(r).then(i)}const r=e,i={uuid:null!==(n=r.userId)&&void 0!==n?n:r.uuid,filter:r.filter,limit:r.limit,page:r.page,include:Object.assign({},r.include),sort:r.sort?Object.fromEntries(Object.entries(r.sort).map((([e,t])=>[e.replace("space","channel"),t]))):void 0},a=e=>({status:e.status,data:e.data.map((e=>({space:e.channel,custom:e.custom,updated:e.updated,eTag:e.eTag}))),totalCount:e.totalCount,next:e.next,prev:e.prev});return t?this.getMemberships(i,((e,s)=>{t(e,s?a(s):s)})):this.getMemberships(i).then(a)}))}addMemberships(e,t){return i(this,void 0,void 0,(function*(){var s,n,r,i,a,o;if(this.logger.warn("PubNub","'addMemberships' is deprecated. Use 'pubnub.objects.setChannelMembers' or 'pubnub.objects.setMemberships' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Add memberships with parameters:"}))),"spaceId"in e){const i=e,a={channel:null!==(s=i.spaceId)&&void 0!==s?s:i.channel,uuids:null!==(r=null===(n=i.users)||void 0===n?void 0:n.map((e=>"string"==typeof e?e:{id:e.userId,custom:e.custom})))&&void 0!==r?r:i.uuids,limit:0};return t?this.setChannelMembers(a,t):this.setChannelMembers(a)}const c=e,u={uuid:null!==(i=c.userId)&&void 0!==i?i:c.uuid,channels:null!==(o=null===(a=c.spaces)||void 0===a?void 0:a.map((e=>"string"==typeof e?e:{id:e.spaceId,custom:e.custom})))&&void 0!==o?o:c.channels,limit:0};return t?this.setMemberships(u,t):this.setMemberships(u)}))}}class _s extends le{constructor(){super()}operation(){return M.PNTimeOperation}parse(e){return i(this,void 0,void 0,(function*(){return{timetoken:this.deserializeResponse(e)[0]}}))}get path(){return"/time/0"}}class Is extends le{constructor(e){super(),this.parameters=e}operation(){return M.PNDownloadFileOperation}validate(){const{channel:e,id:t,name:s}=this.parameters;return e?t?s?void 0:"file name can't be empty":"file id can't be empty":"channel can't be empty"}parse(e){return i(this,void 0,void 0,(function*(){const{cipherKey:t,crypto:s,cryptography:n,name:r,PubNubFile:i}=this.parameters,a=e.headers["content-type"];let o,c=e.body;return i.supportsEncryptFile&&(t||s)&&(t&&n?c=yield n.decrypt(t,c):!t&&s&&(o=yield s.decryptFile(i.create({data:c,name:r,mimeType:a}),i))),o||i.create({data:c,name:r,mimeType:a})}))}get path(){const{keySet:{subscribeKey:e},channel:t,id:s,name:n}=this.parameters;return`/v1/files/${e}/channels/${R(t)}/files/${s}/${n}`}}class Ms{static notificationPayload(e,t){return new we(e,t)}static generateUUID(){return te.createUUID()}constructor(e){if(this.eventHandleCapable={},this.entities={},this._configuration=e.configuration,this.cryptography=e.cryptography,this.tokenManager=e.tokenManager,this.transport=e.transport,this.crypto=e.crypto,this.logger.debug("PubNub",(()=>({messageType:"object",message:e.configuration,details:"Create with configuration:",ignoredKeys:(e,t)=>"function"==typeof t[e]||e.startsWith("_")}))),this._objects=new Ts(this._configuration,this.sendRequest.bind(this)),this._channelGroups=new ls(this._configuration.logger(),this._configuration.keySet,this.sendRequest.bind(this)),this._push=new ys(this._configuration.logger(),this._configuration.keySet,this.sendRequest.bind(this)),this.eventDispatcher=new ge,this._configuration.enableEventEngine){this.logger.debug("PubNub","Using new subscription loop management.");let e=this._configuration.getHeartbeatInterval();this.presenceState={},e&&(this.presenceEventEngine=new Ye({heartbeat:(e,t)=>(this.logger.trace("PresenceEventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Heartbeat with parameters:"}))),this.heartbeat(e,t)),leave:e=>{this.logger.trace("PresenceEventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave with parameters:"}))),this.makeUnsubscribe(e,(()=>{}))},heartbeatDelay:()=>new Promise(((t,s)=>{e=this._configuration.getHeartbeatInterval(),e?setTimeout(t,1e3*e):s(new d("Heartbeat interval has been reset."))})),emitStatus:e=>this.emitStatus(e),config:this._configuration,presenceState:this.presenceState})),this.eventEngine=new St({handshake:e=>(this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Handshake with parameters:",ignoredKeys:["abortSignal","crypto","timeout","keySet","getFileUrl"]}))),this.subscribeHandshake(e)),receiveMessages:e=>(this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Receive messages with parameters:",ignoredKeys:["abortSignal","crypto","timeout","keySet","getFileUrl"]}))),this.subscribeReceiveMessages(e)),delay:e=>new Promise((t=>setTimeout(t,e))),join:e=>{var t,s;this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Join with parameters:"}))),e&&0===(null!==(t=e.channels)&&void 0!==t?t:[]).length&&0===(null!==(s=e.groups)&&void 0!==s?s:[]).length?this.logger.trace("EventEngine","Ignoring 'join' announcement request."):this.join(e)},leave:e=>{var t,s;this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave with parameters:"}))),e&&0===(null!==(t=e.channels)&&void 0!==t?t:[]).length&&0===(null!==(s=e.groups)&&void 0!==s?s:[]).length?this.logger.trace("EventEngine","Ignoring 'leave' announcement request."):this.leave(e)},leaveAll:e=>{this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave all with parameters:"}))),this.leaveAll(e)},presenceReconnect:e=>{this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Reconnect with parameters:"}))),this.presenceReconnect(e)},presenceDisconnect:e=>{this.logger.trace("EventEngine",(()=>({messageType:"object",message:Object.assign({},e),details:"Disconnect with parameters:"}))),this.presenceDisconnect(e)},presenceState:this.presenceState,config:this._configuration,emitMessages:(e,t)=>{try{this.logger.debug("EventEngine",(()=>({messageType:"object",message:t.map((e=>{const t=e.type===he.Message||e.type===he.Signal?B(e.data.message):void 0;return t?{type:e.type,data:Object.assign(Object.assign({},e.data),{pn_mfp:t})}:e})),details:"Received events:"}))),t.forEach((t=>this.emitEvent(e,t)))}catch(e){const t={error:!0,category:h.PNUnknownCategory,errorData:e,statusCode:0};this.emitStatus(t)}},emitStatus:e=>this.emitStatus(e)})}else this.logger.debug("PubNub","Using legacy subscription loop management."),this.subscriptionManager=new me(this._configuration,((e,t)=>{try{this.emitEvent(e,t)}catch(e){const t={error:!0,category:h.PNUnknownCategory,errorData:e,statusCode:0};this.emitStatus(t)}}),this.emitStatus.bind(this),((e,t)=>{this.logger.trace("SubscriptionManager",(()=>({messageType:"object",message:Object.assign({},e),details:"Subscribe with parameters:",ignoredKeys:["crypto","timeout","keySet","getFileUrl"]}))),this.makeSubscribe(e,t)}),((e,t)=>(this.logger.trace("SubscriptionManager",(()=>({messageType:"object",message:Object.assign({},e),details:"Heartbeat with parameters:",ignoredKeys:["crypto","timeout","keySet","getFileUrl"]}))),this.heartbeat(e,t))),((e,t)=>{this.logger.trace("SubscriptionManager",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave with parameters:"}))),this.makeUnsubscribe(e,t)}),this.time.bind(this))}get configuration(){return this._configuration}get _config(){return this.configuration}get authKey(){var e;return null!==(e=this._configuration.authKey)&&void 0!==e?e:void 0}getAuthKey(){return this.authKey}setAuthKey(e){this.logger.debug("PubNub",`Set auth key: ${e}`),this._configuration.setAuthKey(e),this.onAuthenticationChange&&this.onAuthenticationChange(e)}get userId(){return this._configuration.userId}set userId(e){if(!e||"string"!=typeof e||0===e.trim().length){const e=new Error("Missing or invalid userId parameter. Provide a valid string userId");throw this.logger.error("PubNub",(()=>({messageType:"error",message:e}))),e}this.logger.debug("PubNub",`Set user ID: ${e}`),this._configuration.userId=e,this.onUserIdChange&&this.onUserIdChange(this._configuration.userId)}getUserId(){return this._configuration.userId}setUserId(e){this.userId=e}get filterExpression(){var e;return null!==(e=this._configuration.getFilterExpression())&&void 0!==e?e:void 0}getFilterExpression(){return this.filterExpression}set filterExpression(e){this.logger.debug("PubNub",`Set filter expression: ${e}`),this._configuration.setFilterExpression(e)}setFilterExpression(e){this.logger.debug("PubNub",`Set filter expression: ${e}`),this.filterExpression=e}get cipherKey(){return this._configuration.getCipherKey()}set cipherKey(e){this._configuration.setCipherKey(e)}setCipherKey(e){this.logger.debug("PubNub",`Set cipher key: ${e}`),this.cipherKey=e}set heartbeatInterval(e){var t;this.logger.debug("PubNub",`Set heartbeat interval: ${e}`),this._configuration.setHeartbeatInterval(e),this.onHeartbeatIntervalChange&&this.onHeartbeatIntervalChange(null!==(t=this._configuration.getHeartbeatInterval())&&void 0!==t?t:0)}setHeartbeatInterval(e){this.heartbeatInterval=e}get logger(){return this._configuration.logger()}getVersion(){return this._configuration.getVersion()}_addPnsdkSuffix(e,t){this.logger.debug("PubNub",`Add '${e}' 'pnsdk' suffix: ${t}`),this._configuration._addPnsdkSuffix(e,t)}getUUID(){return this.userId}setUUID(e){this.logger.warn("PubNub","'setUserId` is deprecated, please use 'setUserId' or 'userId' setter instead."),this.logger.debug("PubNub",`Set UUID: ${e}`),this.userId=e}get customEncrypt(){return this._configuration.getCustomEncrypt()}get customDecrypt(){return this._configuration.getCustomDecrypt()}channel(e){let t=this.entities[`${e}_ch`];return t||(t=this.entities[`${e}_ch`]=new rs(e,this)),t}channelGroup(e){let t=this.entities[`${e}_chg`];return t||(t=this.entities[`${e}_chg`]=new ss(e,this)),t}channelMetadata(e){let t=this.entities[`${e}_chm`];return t||(t=this.entities[`${e}_chm`]=new ts(e,this)),t}userMetadata(e){let t=this.entities[`${e}_um`];return t||(t=this.entities[`${e}_um`]=new ns(e,this)),t}subscriptionSet(e){var t,s;{const n=[];return null===(t=e.channels)||void 0===t||t.forEach((e=>n.push(this.channel(e)))),null===(s=e.channelGroups)||void 0===s||s.forEach((e=>n.push(this.channelGroup(e)))),new _t({client:this,entities:n,options:e.subscriptionOptions})}}sendRequest(e,t){return i(this,void 0,void 0,(function*(){const s=e.validate();if(s){const e=(n=s,p(Object.assign({message:n},{}),h.PNValidationErrorCategory));if(this.logger.error("PubNub",(()=>({messageType:"error",message:e}))),t)return t(e,null);throw new d("Validation failed, check status for details",e)}var n;const r=e.request(),i=e.operation();r.formData&&r.formData.length>0||i===M.PNDownloadFileOperation?r.timeout=this._configuration.getFileTimeout():i===M.PNSubscribeOperation||i===M.PNReceiveMessagesOperation?r.timeout=this._configuration.getSubscribeTimeout():r.timeout=this._configuration.getTransactionTimeout();const a={error:!1,operation:i,category:h.PNAcknowledgmentCategory,statusCode:0},[o,c]=this.transport.makeSendable(r);return e.cancellationController=c||null,o.then((t=>{if(a.statusCode=t.status,200!==t.status&&204!==t.status){const e=Ms.decoder.decode(t.body),s=t.headers["content-type"];if(s||-1!==s.indexOf("javascript")||-1!==s.indexOf("json")){const t=JSON.parse(e);"object"==typeof t&&"error"in t&&t.error&&"object"==typeof t.error&&(a.errorData=t.error)}else a.responseText=e}return e.parse(t)})).then((e=>t?t(a,e):e)).catch((e=>{const s=e instanceof I?e:I.create(e);if(t)return s.category!==h.PNCancelledCategory&&this.logger.error("PubNub",(()=>({messageType:"error",message:s.toPubNubError(i,"REST API request processing error, check status for details")}))),t(s.toStatus(i),null);const n=s.toPubNubError(i,"REST API request processing error, check status for details");throw s.category!==h.PNCancelledCategory&&this.logger.error("PubNub",(()=>({messageType:"error",message:n}))),n}))}))}destroy(e=!1){this.logger.info("PubNub","Destroying PubNub client."),this._globalSubscriptionSet&&(this._globalSubscriptionSet.invalidate(!0),this._globalSubscriptionSet=void 0),Object.values(this.eventHandleCapable).forEach((e=>e.invalidate(!0))),this.eventHandleCapable={},this.subscriptionManager?(this.subscriptionManager.unsubscribeAll(e),this.subscriptionManager.disconnect()):this.eventEngine&&this.eventEngine.unsubscribeAll(e),this.presenceEventEngine&&this.presenceEventEngine.leaveAll(e)}stop(){this.logger.warn("PubNub","'stop' is deprecated, please use 'destroy' instead."),this.destroy()}publish(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Publish with parameters:"})));const s=!1===e.replicate&&!1===e.storeInHistory,n=new wt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule()})),r=e=>{e&&this.logger.debug("PubNub",`${s?"Fire":"Publish"} success with timetoken: ${e.timetoken}`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}}))}signal(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Signal with parameters:"})));const s=new Ot(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Publish success with timetoken: ${e.timetoken}`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}fire(e,t){return i(this,void 0,void 0,(function*(){return this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fire with parameters:"}))),null!=t||(t=()=>{}),this.publish(Object.assign(Object.assign({},e),{replicate:!1,storeInHistory:!1}),t)}))}get globalSubscriptionSet(){return this._globalSubscriptionSet||(this._globalSubscriptionSet=this.subscriptionSet({})),this._globalSubscriptionSet}get subscriptionTimetoken(){return this.subscriptionManager?this.subscriptionManager.subscriptionTimetoken:this.eventEngine?this.eventEngine.subscriptionTimetoken:void 0}getSubscribedChannels(){return this.subscriptionManager?this.subscriptionManager.subscribedChannels:this.eventEngine?this.eventEngine.getSubscribedChannels():[]}getSubscribedChannelGroups(){return this.subscriptionManager?this.subscriptionManager.subscribedChannelGroups:this.eventEngine?this.eventEngine.getSubscribedChannelGroups():[]}registerEventHandleCapable(e,t,s){{let n;this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign(Object.assign({subscription:e},t?{cursor:t}:[]),s?{subscriptions:s}:{}),details:"Register event handle capable:"}))),this.eventHandleCapable[e.state.id]||(this.eventHandleCapable[e.state.id]=e),s&&0!==s.length?(n=new jt({}),s.forEach((e=>n.add(e.subscriptionInput(!1))))):n=e.subscriptionInput(!1);const r={};r.channels=n.channels,r.channelGroups=n.channelGroups,t&&(r.timetoken=t.timetoken),this.subscriptionManager?this.subscriptionManager.subscribe(r):this.eventEngine&&this.eventEngine.subscribe(r)}}unregisterEventHandleCapable(e,t){{if(!this.eventHandleCapable[e.state.id])return;const s=[];this.logger.trace("PubNub",(()=>({messageType:"object",message:{subscription:e,subscriptions:t},details:"Unregister event handle capable:"})));let n,r=!t||0===t.length;if(!r&&e instanceof _t&&e.subscriptions.length===(null==t?void 0:t.length)&&(r=e.subscriptions.every((e=>t.includes(e)))),r&&delete this.eventHandleCapable[e.state.id],t&&0!==t.length?(n=new jt({}),t.forEach((e=>{const t=e.subscriptionInput(!0);t.isEmpty?s.push(e):n.add(t)}))):(n=e.subscriptionInput(!0),n.isEmpty&&s.push(e)),s.length>0&&this.logger.trace("PubNub",(()=>{const e=[];return s[0]instanceof _t?s[0].subscriptions.forEach((t=>e.push(t.state.entity))):s.forEach((t=>e.push(t.state.entity))),{messageType:"object",message:{entities:e},details:"Can't unregister event handle capable because entities still in use:"}})),n.isEmpty)return;{const e=[],t=[];if(Object.values(this.eventHandleCapable).forEach((s=>{const r=s.subscriptionInput(!1),i=r.channelGroups,a=r.channels;e.push(...n.channelGroups.filter((e=>i.includes(e)))),t.push(...n.channels.filter((e=>a.includes(e))))})),(t.length>0||e.length>0)&&(this.logger.trace("PubNub",(()=>{const s=[],r=n=>{const r=n.subscriptionNames(!0),i=n.subscriptionType===Pt.Channel?t:e;r.some((e=>i.includes(e)))&&s.push(n)};Object.values(this.eventHandleCapable).forEach((e=>{e instanceof _t?e.subscriptions.forEach((e=>{r(e.state.entity)})):e instanceof Mt&&r(e.state.entity)}));let i="Some entities still in use:";return t.length+e.length===n.length&&(i="Can't unregister event handle capable because entities still in use:"),{messageType:"object",message:{entities:s},details:i}})),n.remove(new jt({channels:t,channelGroups:e})),n.isEmpty))return}const i={};i.channels=n.channels,i.channelGroups=n.channelGroups,this.subscriptionManager?this.subscriptionManager.unsubscribe(i):this.eventEngine&&this.eventEngine.unsubscribe(i)}}subscribe(e){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Subscribe with parameters:"})));const t=this.subscriptionSet(Object.assign(Object.assign({},e),{subscriptionOptions:{receivePresenceEvents:e.withPresence}}));this.globalSubscriptionSet.addSubscriptionSet(t),t.dispose();const s="number"==typeof e.timetoken?`${e.timetoken}`:e.timetoken;this.globalSubscriptionSet.subscribe({timetoken:s})}}makeSubscribe(e,t){{this._configuration.isSharedWorkerEnabled()||(e.onDemand=!1);const s=new pe(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule(),getFileUrl:this.getFileUrl.bind(this)}));if(this.sendRequest(s,((e,n)=>{var r;this.subscriptionManager&&(null===(r=this.subscriptionManager.abort)||void 0===r?void 0:r.identifier)===s.requestIdentifier&&(this.subscriptionManager.abort=null),t(e,n)})),this.subscriptionManager){const e=()=>s.abort("Cancel long-poll subscribe request");e.identifier=s.requestIdentifier,this.subscriptionManager.abort=e}}}unsubscribe(e){{if(this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Unsubscribe with parameters:"}))),!this._globalSubscriptionSet)return void this.logger.debug("PubNub","There are no active subscriptions. Ignore.");const t=this.globalSubscriptionSet.subscriptions.filter((t=>{var s,n;const r=t.subscriptionInput(!1);if(r.isEmpty)return!1;for(const t of null!==(s=e.channels)&&void 0!==s?s:[])if(r.contains(t))return!0;for(const t of null!==(n=e.channelGroups)&&void 0!==n?n:[])if(r.contains(t))return!0}));t.length>0&&this.globalSubscriptionSet.removeSubscriptions(t)}}makeUnsubscribe(e,t){{let{channels:s,channelGroups:n}=e;if(this._configuration.getKeepPresenceChannelsInPresenceRequests()||(n&&(n=n.filter((e=>!e.endsWith("-pnpres")))),s&&(s=s.filter((e=>!e.endsWith("-pnpres"))))),0===(null!=n?n:[]).length&&0===(null!=s?s:[]).length)return t({error:!1,operation:M.PNUnsubscribeOperation,category:h.PNAcknowledgmentCategory,statusCode:200});this.sendRequest(new Ft({channels:s,channelGroups:n,keySet:this._configuration.keySet}),t)}}unsubscribeAll(){this.logger.debug("PubNub","Unsubscribe all channels and groups"),this._globalSubscriptionSet&&this._globalSubscriptionSet.invalidate(!1),Object.values(this.eventHandleCapable).forEach((e=>e.invalidate(!1))),this.eventHandleCapable={},this.subscriptionManager?this.subscriptionManager.unsubscribeAll():this.eventEngine&&this.eventEngine.unsubscribeAll()}disconnect(e=!1){this.logger.debug("PubNub",`Disconnect (while offline? ${e?"yes":"no"})`),this.subscriptionManager?this.subscriptionManager.disconnect():this.eventEngine&&this.eventEngine.disconnect(e)}reconnect(e){this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Reconnect with parameters:"}))),this.subscriptionManager?this.subscriptionManager.reconnect():this.eventEngine&&this.eventEngine.reconnect(null!=e?e:{})}subscribeHandshake(e){return i(this,void 0,void 0,(function*(){{this._configuration.isSharedWorkerEnabled()||(e.onDemand=!1);const t=new Ct(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule(),getFileUrl:this.getFileUrl.bind(this)})),s=e.abortSignal.subscribe((e=>{t.abort("Cancel subscribe handshake request")}));return this.sendRequest(t).then((e=>(s(),e.cursor)))}}))}subscribeReceiveMessages(e){return i(this,void 0,void 0,(function*(){{this._configuration.isSharedWorkerEnabled()||(e.onDemand=!1);const t=new kt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule(),getFileUrl:this.getFileUrl.bind(this)})),s=e.abortSignal.subscribe((e=>{t.abort("Cancel long-poll subscribe request")}));return this.sendRequest(t).then((e=>(s(),e)))}}))}getMessageActions(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get message actions with parameters:"})));const s=new Ht(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Get message actions success. Received ${e.data.length} message actions.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}addMessageAction(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Add message action with parameters:"})));const s=new Bt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Message action add success. Message action added with timetoken: ${e.data.actionTimetoken}`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}removeMessageAction(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove message action with parameters:"})));const s=new Wt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Message action remove success. Removed message action with ${e.actionTimetoken} timetoken.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}fetchMessages(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fetch messages with parameters:"})));const s=new Kt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule(),getFileUrl:this.getFileUrl.bind(this)})),n=e=>{if(!e)return;const t=Object.values(e.channels).reduce(((e,t)=>e+t.length),0);this.logger.debug("PubNub",`Fetch messages success. Received ${t} messages.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}deleteMessages(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Delete messages with parameters:"})));const s=new xt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub","Delete messages success.")};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}messageCounts(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get messages count with parameters:"})));const s=new qt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=t=>{if(!t)return;const s=Object.values(t.channels).reduce(((e,t)=>e+t),0);this.logger.debug("PubNub",`Get messages count success. There are ${s} messages since provided reference timetoken${e.channelTimetokens?e.channelTimetokens.join(","):""}.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}history(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fetch history with parameters:"})));const s=new Lt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule()})),n=e=>{e&&this.logger.debug("PubNub",`Fetch history success. Received ${e.messages.length} messages.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}hereNow(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Here now with parameters:"})));const s=new $t(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`Here now success. There are ${e.totalOccupancy} participants in ${e.totalChannels} channels.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}whereNow(e,t){return i(this,void 0,void 0,(function*(){var s;{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Where now with parameters:"})));const n=new Rt({uuid:null!==(s=e.uuid)&&void 0!==s?s:this._configuration.userId,keySet:this._configuration.keySet}),r=e=>{e&&this.logger.debug("PubNub",`Where now success. Currently present in ${e.channels.length} channels.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}}))}getState(e,t){return i(this,void 0,void 0,(function*(){var s;{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Get presence state with parameters:"})));const n=new At(Object.assign(Object.assign({},e),{uuid:null!==(s=e.uuid)&&void 0!==s?s:this._configuration.userId,keySet:this._configuration.keySet})),r=e=>{e&&this.logger.debug("PubNub",`Get presence state success. Received presence state for ${Object.keys(e.channels).length} channels.`)};return t?this.sendRequest(n,((e,s)=>{r(s),t(e,s)})):this.sendRequest(n).then((e=>(r(e),e)))}}))}setState(e,t){return i(this,void 0,void 0,(function*(){var s,n;{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Set presence state with parameters:"})));const{keySet:r,userId:i}=this._configuration,a=this._configuration.getPresenceTimeout();let o;if(this._configuration.enableEventEngine&&this.presenceState){const t=this.presenceState;null===(s=e.channels)||void 0===s||s.forEach((s=>t[s]=e.state)),"channelGroups"in e&&(null===(n=e.channelGroups)||void 0===n||n.forEach((s=>t[s]=e.state)))}o="withHeartbeat"in e&&e.withHeartbeat?new Dt(Object.assign(Object.assign({},e),{keySet:r,heartbeat:a})):new Ut(Object.assign(Object.assign({},e),{keySet:r,uuid:i}));const c=e=>{e&&this.logger.debug("PubNub","Set presence state success."+(o instanceof Dt?" Presence state has been set using heartbeat endpoint.":""))};return this.subscriptionManager&&this.subscriptionManager.setState(e),t?this.sendRequest(o,((e,s)=>{c(s),t(e,s)})):this.sendRequest(o).then((e=>(c(e),e)))}}))}presence(e){var t;this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Change presence with parameters:"}))),null===(t=this.subscriptionManager)||void 0===t||t.changePresence(e)}heartbeat(e,t){return i(this,void 0,void 0,(function*(){var s;{this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Heartbeat with parameters:"})));let{channels:n,channelGroups:r}=e;if(r&&(r=r.filter((e=>!e.endsWith("-pnpres")))),n&&(n=n.filter((e=>!e.endsWith("-pnpres")))),0===(null!=r?r:[]).length&&0===(null!=n?n:[]).length){const e={error:!1,operation:M.PNHeartbeatOperation,category:h.PNAcknowledgmentCategory,statusCode:200};return this.logger.trace("PubNub","There are no active subscriptions. Ignore."),t?t(e,{}):Promise.resolve(e)}const i=new Dt(Object.assign(Object.assign({},e),{channels:n,channelGroups:r,keySet:this._configuration.keySet})),a=e=>{e&&this.logger.trace("PubNub","Heartbeat success.")},o=null===(s=e.abortSignal)||void 0===s?void 0:s.subscribe((e=>{i.abort("Cancel long-poll subscribe request")}));return t?this.sendRequest(i,((e,s)=>{a(s),o&&o(),t(e,s)})):this.sendRequest(i).then((e=>(a(e),o&&o(),e)))}}))}join(e){var t,s;this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Join with parameters:"}))),e&&0===(null!==(t=e.channels)&&void 0!==t?t:[]).length&&0===(null!==(s=e.groups)&&void 0!==s?s:[]).length?this.logger.trace("PubNub","Ignoring 'join' announcement request."):this.presenceEventEngine?this.presenceEventEngine.join(e):this.heartbeat(Object.assign(Object.assign({channels:e.channels,channelGroups:e.groups},this._configuration.maintainPresenceState&&this.presenceState&&Object.keys(this.presenceState).length>0&&{state:this.presenceState}),{heartbeat:this._configuration.getPresenceTimeout()}),(()=>{}))}presenceReconnect(e){this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Presence reconnect with parameters:"}))),this.presenceEventEngine?this.presenceEventEngine.reconnect():this.heartbeat(Object.assign(Object.assign({channels:e.channels,channelGroups:e.groups},this._configuration.maintainPresenceState&&{state:this.presenceState}),{heartbeat:this._configuration.getPresenceTimeout()}),(()=>{}))}leave(e){var t,s,n;this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave with parameters:"}))),e&&0===(null!==(t=e.channels)&&void 0!==t?t:[]).length&&0===(null!==(s=e.groups)&&void 0!==s?s:[]).length?this.logger.trace("PubNub","Ignoring 'leave' announcement request."):this.presenceEventEngine?null===(n=this.presenceEventEngine)||void 0===n||n.leave(e):this.makeUnsubscribe({channels:e.channels,channelGroups:e.groups},(()=>{}))}leaveAll(e={}){this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Leave all with parameters:"}))),this.presenceEventEngine?this.presenceEventEngine.leaveAll(!!e.isOffline):e.isOffline||this.makeUnsubscribe({channels:e.channels,channelGroups:e.groups},(()=>{}))}presenceDisconnect(e){this.logger.trace("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Presence disconnect parameters:"}))),this.presenceEventEngine?this.presenceEventEngine.disconnect(!!e.isOffline):e.isOffline||this.makeUnsubscribe({channels:e.channels,channelGroups:e.groups},(()=>{}))}grantToken(e,t){return i(this,void 0,void 0,(function*(){throw new Error("Grant Token error: PAM module disabled")}))}revokeToken(e,t){return i(this,void 0,void 0,(function*(){throw new Error("Revoke Token error: PAM module disabled")}))}get token(){return this.tokenManager&&this.tokenManager.getToken()}getToken(){return this.token}set token(e){this.tokenManager&&this.tokenManager.setToken(e),this.onAuthenticationChange&&this.onAuthenticationChange(e)}setToken(e){this.token=e}parseToken(e){return this.tokenManager&&this.tokenManager.parseToken(e)}grant(e,t){return i(this,void 0,void 0,(function*(){throw new Error("Grant error: PAM module disabled")}))}audit(e,t){return i(this,void 0,void 0,(function*(){throw new Error("Grant Permissions error: PAM module disabled")}))}get objects(){return this._objects}fetchUsers(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'fetchUsers' is deprecated. Use 'pubnub.objects.getAllUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{},details:"Fetch all User objects with parameters:"}))),this.objects._getAllUUIDMetadata(e,t)}))}fetchUser(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'fetchUser' is deprecated. Use 'pubnub.objects.getUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{uuid:this.userId},details:`Fetch${e&&"function"!=typeof e?"":" current"} User object with parameters:`}))),this.objects._getUUIDMetadata(e,t)}))}createUser(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'createUser' is deprecated. Use 'pubnub.objects.setUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Create User object with parameters:"}))),this.objects._setUUIDMetadata(e,t)}))}updateUser(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'updateUser' is deprecated. Use 'pubnub.objects.setUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Update User object with parameters:"}))),this.objects._setUUIDMetadata(e,t)}))}removeUser(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'removeUser' is deprecated. Use 'pubnub.objects.removeUUIDMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{uuid:this.userId},details:`Remove${e&&"function"!=typeof e?"":" current"} User object with parameters:`}))),this.objects._removeUUIDMetadata(e,t)}))}fetchSpaces(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'fetchSpaces' is deprecated. Use 'pubnub.objects.getAllChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:e&&"function"!=typeof e?e:{},details:"Fetch all Space objects with parameters:"}))),this.objects._getAllChannelMetadata(e,t)}))}fetchSpace(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'fetchSpace' is deprecated. Use 'pubnub.objects.getChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Fetch Space object with parameters:"}))),this.objects._getChannelMetadata(e,t)}))}createSpace(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'createSpace' is deprecated. Use 'pubnub.objects.setChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Create Space object with parameters:"}))),this.objects._setChannelMetadata(e,t)}))}updateSpace(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'updateSpace' is deprecated. Use 'pubnub.objects.setChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Update Space object with parameters:"}))),this.objects._setChannelMetadata(e,t)}))}removeSpace(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'removeSpace' is deprecated. Use 'pubnub.objects.removeChannelMetadata' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove Space object with parameters:"}))),this.objects._removeChannelMetadata(e,t)}))}fetchMemberships(e,t){return i(this,void 0,void 0,(function*(){return this.objects.fetchMemberships(e,t)}))}addMemberships(e,t){return i(this,void 0,void 0,(function*(){return this.objects.addMemberships(e,t)}))}updateMemberships(e,t){return i(this,void 0,void 0,(function*(){return this.logger.warn("PubNub","'addMemberships' is deprecated. Use 'pubnub.objects.setChannelMembers' or 'pubnub.objects.setMemberships' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Update memberships with parameters:"}))),this.objects.addMemberships(e,t)}))}removeMemberships(e,t){return i(this,void 0,void 0,(function*(){var s,n,r;{if(this.logger.warn("PubNub","'removeMemberships' is deprecated. Use 'pubnub.objects.removeMemberships' or 'pubnub.objects.removeChannelMembers' instead."),this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Remove memberships with parameters:"}))),"spaceId"in e){const r=e,i={channel:null!==(s=r.spaceId)&&void 0!==s?s:r.channel,uuids:null!==(n=r.userIds)&&void 0!==n?n:r.uuids,limit:0};return t?this.objects.removeChannelMembers(i,t):this.objects.removeChannelMembers(i)}const i=e,a={uuid:i.userId,channels:null!==(r=i.spaceIds)&&void 0!==r?r:i.channels,limit:0};return t?this.objects.removeMemberships(a,t):this.objects.removeMemberships(a)}}))}get channelGroups(){return this._channelGroups}get push(){return this._push}sendFile(e,t){return i(this,void 0,void 0,(function*(){{if(!this._configuration.PubNubFile)throw new Error("Validation failed: 'PubNubFile' not configured or file upload not supported by the platform.");this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Send file with parameters:"})));const s=new Zt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,PubNubFile:this._configuration.PubNubFile,fileUploadPublishRetryLimit:this._configuration.fileUploadPublishRetryLimit,file:e.file,sendRequest:this.sendRequest.bind(this),publishFile:this.publishFile.bind(this),crypto:this._configuration.getCryptoModule(),cryptography:this.cryptography?this.cryptography:void 0})),n={error:!1,operation:M.PNPublishFileOperation,category:h.PNAcknowledgmentCategory,statusCode:0},r=e=>{e&&this.logger.debug("PubNub",`Send file success. File shared with ${e.id} ID.`)};return s.process().then((e=>(n.statusCode=e.status,r(e),t?t(n,e):e))).catch((e=>{let s;throw e instanceof d?s=e.status:e instanceof I&&(s=e.toStatus(n.operation)),this.logger.error("PubNub",(()=>({messageType:"error",message:new d("File sending error. Check status for details",s)}))),t&&s&&t(s,null),new d("REST API request processing error. Check status for details",s)}))}}))}publishFile(e,t){return i(this,void 0,void 0,(function*(){{if(!this._configuration.PubNubFile)throw new Error("Validation failed: 'PubNubFile' not configured or file upload not supported by the platform.");this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Publish file message with parameters:"})));const s=new zt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,crypto:this._configuration.getCryptoModule()})),n=e=>{e&&this.logger.debug("PubNub",`Publish file message success. File message published with timetoken: ${e.timetoken}`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}listFiles(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"List files with parameters:"})));const s=new Xt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=e=>{e&&this.logger.debug("PubNub",`List files success. There are ${e.count} uploaded files.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}getFileUrl(e){var t;{const s=this.transport.request(new Vt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})).request()),n=null!==(t=s.queryParameters)&&void 0!==t?t:{},r=Object.keys(n).map((e=>{const t=n[e];return Array.isArray(t)?t.map((t=>`${e}=${R(t)}`)).join("&"):`${e}=${R(t)}`})).join("&");return`${s.origin}${s.path}?${r}`}}downloadFile(e,t){return i(this,void 0,void 0,(function*(){{if(!this._configuration.PubNubFile)throw new Error("Validation failed: 'PubNubFile' not configured or file upload not supported by the platform.");this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Download file with parameters:"})));const s=new Is(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet,PubNubFile:this._configuration.PubNubFile,cryptography:this.cryptography?this.cryptography:void 0,crypto:this._configuration.getCryptoModule()})),n=e=>{e&&this.logger.debug("PubNub","Download file success.")};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):yield this.sendRequest(s).then((e=>(n(e),e)))}}))}deleteFile(e,t){return i(this,void 0,void 0,(function*(){{this.logger.debug("PubNub",(()=>({messageType:"object",message:Object.assign({},e),details:"Delete file with parameters:"})));const s=new Jt(Object.assign(Object.assign({},e),{keySet:this._configuration.keySet})),n=t=>{t&&this.logger.debug("PubNub",`Delete file success. Deleted file with ${e.id} ID.`)};return t?this.sendRequest(s,((e,s)=>{n(s),t(e,s)})):this.sendRequest(s).then((e=>(n(e),e)))}}))}time(e){return i(this,void 0,void 0,(function*(){this.logger.debug("PubNub","Get service time.");const t=new _s,s=e=>{e&&this.logger.debug("PubNub",`Get service time success. Current timetoken: ${e.timetoken}`)};return e?this.sendRequest(t,((t,n)=>{s(n),e(t,n)})):this.sendRequest(t).then((e=>(s(e),e)))}))}emitStatus(e){var t;null===(t=this.eventDispatcher)||void 0===t||t.handleStatus(e)}emitEvent(e,t){var s;this._globalSubscriptionSet&&this._globalSubscriptionSet.handleEvent(e,t),null===(s=this.eventDispatcher)||void 0===s||s.handleEvent(t),Object.values(this.eventHandleCapable).forEach((s=>{s!==this._globalSubscriptionSet&&s.handleEvent(e,t)}))}set onStatus(e){this.eventDispatcher&&(this.eventDispatcher.onStatus=e)}set onMessage(e){this.eventDispatcher&&(this.eventDispatcher.onMessage=e)}set onPresence(e){this.eventDispatcher&&(this.eventDispatcher.onPresence=e)}set onSignal(e){this.eventDispatcher&&(this.eventDispatcher.onSignal=e)}set onObjects(e){this.eventDispatcher&&(this.eventDispatcher.onObjects=e)}set onMessageAction(e){this.eventDispatcher&&(this.eventDispatcher.onMessageAction=e)}set onFile(e){this.eventDispatcher&&(this.eventDispatcher.onFile=e)}addListener(e){this.eventDispatcher&&this.eventDispatcher.addListener(e)}removeListener(e){this.eventDispatcher&&this.eventDispatcher.removeListener(e)}removeAllListeners(){this.eventDispatcher&&this.eventDispatcher.removeAllListeners()}encrypt(e,t){this.logger.warn("PubNub","'encrypt' is deprecated. Use cryptoModule instead.");const s=this._configuration.getCryptoModule();if(!t&&s&&"string"==typeof e){const t=s.encrypt(e);return"string"==typeof t?t:u(t)}if(!this.crypto)throw new Error("Encryption error: cypher key not set");return this.crypto.encrypt(e,t)}decrypt(e,t){this.logger.warn("PubNub","'decrypt' is deprecated. Use cryptoModule instead.");const s=this._configuration.getCryptoModule();if(!t&&s){const t=s.decrypt(e);return t instanceof ArrayBuffer?JSON.parse((new TextDecoder).decode(t)):t}if(!this.crypto)throw new Error("Decryption error: cypher key not set");return this.crypto.decrypt(e,t)}encryptFile(e,t){return i(this,void 0,void 0,(function*(){var s;if("string"!=typeof e&&(t=e),!t)throw new Error("File encryption error. Source file is missing.");if(!this._configuration.PubNubFile)throw new Error("File encryption error. File constructor not configured.");if("string"!=typeof e&&!this._configuration.getCryptoModule())throw new Error("File encryption error. Crypto module not configured.");if("string"==typeof e){if(!this.cryptography)throw new Error("File encryption error. File encryption not available");return this.cryptography.encryptFile(e,t,this._configuration.PubNubFile)}return null===(s=this._configuration.getCryptoModule())||void 0===s?void 0:s.encryptFile(t,this._configuration.PubNubFile)}))}decryptFile(e,t){return i(this,void 0,void 0,(function*(){var s;if("string"!=typeof e&&(t=e),!t)throw new Error("File encryption error. Source file is missing.");if(!this._configuration.PubNubFile)throw new Error("File decryption error. File constructor not configured.");if("string"==typeof e&&!this._configuration.getCryptoModule())throw new Error("File decryption error. Crypto module not configured.");if("string"==typeof e){if(!this.cryptography)throw new Error("File decryption error. File decryption not available");return this.cryptography.decryptFile(e,t,this._configuration.PubNubFile)}return null===(s=this._configuration.getCryptoModule())||void 0===s?void 0:s.decryptFile(t,this._configuration.PubNubFile)}))}}Ms.decoder=new TextDecoder,Ms.OPERATIONS=M,Ms.CATEGORIES=h,Ms.Endpoint=z,Ms.ExponentialRetryPolicy=V.ExponentialRetryPolicy,Ms.LinearRetryPolicy=V.LinearRetryPolicy,Ms.NoneRetryPolicy=V.None,Ms.LogLevel=F;class As{constructor(e,t){this.decode=e,this.base64ToBinary=t}decodeToken(e){let t="";e.length%4==3?t="=":e.length%4==2&&(t="==");const s=e.replace(/-/gi,"+").replace(/_/gi,"/")+t,n=this.decode(this.base64ToBinary(s));return"object"==typeof n?n:void 0}}class Us extends Ms{constructor(e){var t;const s=void 0!==e.subscriptionWorkerUrl,r=D(e),i=Object.assign(Object.assign({},r),{sdkFamily:"Web"});i.PubNubFile=o;const a=se(i,(e=>{if(e.cipherKey){return new N({default:new E(Object.assign(Object.assign({},e),e.logger?{}:{logger:a.logger()})),cryptors:[new k({cipherKey:e.cipherKey})]})}}));let u,l;e.subscriptionWorkerLogVerbosity?e.subscriptionWorkerLogLevel=F.Debug:void 0===e.subscriptionWorkerLogLevel&&(e.subscriptionWorkerLogLevel=F.None),void 0!==e.subscriptionWorkerLogVerbosity&&a.logger().warn("Configuration","'subscriptionWorkerLogVerbosity' is deprecated. Use 'subscriptionWorkerLogLevel' instead."),a.getCryptoModule()&&(a.getCryptoModule().logger=a.logger()),u=new ie(new As((e=>U(n.decode(e))),c)),(a.getCipherKey()||a.secretKey)&&(l=new P({secretKey:a.secretKey,cipherKey:a.getCipherKey(),useRandomIVs:a.getUseRandomIVs(),customEncrypt:a.getCustomEncrypt(),customDecrypt:a.getCustomDecrypt(),logger:a.logger()}));let h,d=()=>{},p=()=>{},g=()=>{};h=new j;let b=new ue(a.logger(),i.transport);if(r.subscriptionWorkerUrl)try{const e=new A({clientIdentifier:a._instanceId,subscriptionKey:a.subscribeKey,userId:a.getUserId(),workerUrl:r.subscriptionWorkerUrl,sdkVersion:a.getVersion(),heartbeatInterval:a.getHeartbeatInterval(),announceSuccessfulHeartbeats:a.announceSuccessfulHeartbeats,announceFailedHeartbeats:a.announceFailedHeartbeats,workerOfflineClientsCheckInterval:i.subscriptionWorkerOfflineClientsCheckInterval,workerUnsubscribeOfflineClients:i.subscriptionWorkerUnsubscribeOfflineClients,workerLogLevel:i.subscriptionWorkerLogLevel,tokenManager:u,transport:b,logger:a.logger()});d=t=>e.onHeartbeatIntervalChange(t),p=t=>e.onTokenChange(t),g=t=>e.onUserIdChange(t),b=e,r.subscriptionWorkerUnsubscribeOfflineClients&&window.addEventListener("pagehide",(t=>{t.persisted||e.terminate()}),{once:!0})}catch(e){a.logger().error("PubNub",(()=>({messageType:"error",message:e})))}else s&&a.logger().warn("PubNub","SharedWorker not supported in this browser. Fallback to the original transport.");const y=new ce({clientConfiguration:a,tokenManager:u,transport:b});if(super({configuration:a,transport:y,cryptography:h,tokenManager:u,crypto:l}),this.onHeartbeatIntervalChange=d,this.onAuthenticationChange=p,this.onUserIdChange=g,b instanceof A){b.emitStatus=this.emitStatus.bind(this);const e=this.disconnect.bind(this);this.disconnect=t=>{b.disconnect(),e()}}(null===(t=e.listenToBrowserNetworkEvents)||void 0===t||t)&&(window.addEventListener("offline",(()=>{this.networkDownDetected()})),window.addEventListener("online",(()=>{this.networkUpDetected()})))}networkDownDetected(){this.logger.debug("PubNub","Network down detected"),this.emitStatus({category:Us.CATEGORIES.PNNetworkDownCategory}),this._configuration.restore?this.disconnect(!0):this.destroy(!0)}networkUpDetected(){this.logger.debug("PubNub","Network up detected"),this.emitStatus({category:Us.CATEGORIES.PNNetworkUpCategory}),this.reconnect()}}return Us.CryptoModule=N,Us})); diff --git a/dist/web/pubnub.worker.js b/dist/web/pubnub.worker.js index 1984247ce..2ae6f0132 100644 --- a/dist/web/pubnub.worker.js +++ b/dist/web/pubnub.worker.js @@ -3,2057 +3,4728 @@ factory(); })((function () { 'use strict'; - /****************************************************************************** - Copyright (c) Microsoft Corporation. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - PERFORMANCE OF THIS SOFTWARE. - ***************************************************************************** */ - /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ - - - function __awaiter(thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - } - - typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { - var e = new Error(message); - return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; - }; - /** - * Enum representing possible transport methods for HTTP requests. - * - * @enum {number} + * Type with events which is emitted by PubNub client and can be handled with callback passed to the + * {@link EventTarget#addEventListener|addEventListener}. */ - var TransportMethod; - (function (TransportMethod) { + var PubNubClientEvent; + (function (PubNubClientEvent) { /** - * Request will be sent using `GET` method. + * Client unregistered (no connection through SharedWorker connection ports). + * */ - TransportMethod["GET"] = "GET"; + PubNubClientEvent["Unregister"] = "unregister"; /** - * Request will be sent using `POST` method. + * Client temporarily disconnected. */ - TransportMethod["POST"] = "POST"; + PubNubClientEvent["Disconnect"] = "disconnect"; /** - * Request will be sent using `PATCH` method. + * User ID for current PubNub client has been changed. + * + * On identity change for proper further operation expected following actions: + * - send immediate heartbeat with new `user ID` (if has been sent before) */ - TransportMethod["PATCH"] = "PATCH"; + PubNubClientEvent["IdentityChange"] = "identityChange"; /** - * Request will be sent using `DELETE` method. + * Authentication token change event. + * + * On authentication token change for proper further operation expected following actions: + * - cached `heartbeat` request query parameter updated */ - TransportMethod["DELETE"] = "DELETE"; + PubNubClientEvent["AuthChange"] = "authChange"; /** - * Local request. + * Presence heartbeat interval change event. * - * Request won't be sent to the service and probably used to compute URL. + * On heartbeat interval change for proper further operation expected following actions: + * - restart _backup_ heartbeat timer with new interval. */ - TransportMethod["LOCAL"] = "LOCAL"; - })(TransportMethod || (TransportMethod = {})); - - var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; - - function getDefaultExportFromCjs (x) { - return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; - } - - var uuid = {exports: {}}; - - /*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */ - uuid.exports; - - (function (module, exports) { - (function (root, factory) { - { - factory(exports); - if (module !== null) { - module.exports = exports.uuid; - } - } - }(commonjsGlobal, function (exports) { - var VERSION = '0.1.0'; - var uuidRegex = { - '3': /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i, - '4': /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, - '5': /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, - all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i - }; - - function uuid() { - var uuid = '', i, random; - for (i = 0; i < 32; i++) { - random = Math.random() * 16 | 0; - if (i === 8 || i === 12 || i === 16 || i === 20) uuid += '-'; - uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16); - } - return uuid - } - - function isUUID(str, version) { - var pattern = uuidRegex[version || 'all']; - return pattern && pattern.test(str) || false - } - - uuid.isUUID = isUUID; - uuid.VERSION = VERSION; - - exports.uuid = uuid; - exports.isUUID = isUUID; - })); - } (uuid, uuid.exports)); - - var uuidExports = uuid.exports; - var uuidGenerator$1 = /*@__PURE__*/getDefaultExportFromCjs(uuidExports); - - /** - * Random identifier generator helper module. - * - * @internal - */ - /** @internal */ - var uuidGenerator = { - createUUID() { - if (uuidGenerator$1.uuid) { - return uuidGenerator$1.uuid(); - } - // @ts-expect-error Depending on module type it may be callable. - return uuidGenerator$1(); - }, - }; - - /// + PubNubClientEvent["HeartbeatIntervalChange"] = "heartbeatIntervalChange"; + /** + * Core PubNub client module request to send `subscribe` request. + */ + PubNubClientEvent["SendSubscribeRequest"] = "sendSubscribeRequest"; + /** + * Core PubNub client module request to _cancel_ specific `subscribe` request. + */ + PubNubClientEvent["CancelSubscribeRequest"] = "cancelSubscribeRequest"; + /** + * Core PubNub client module request to send `heartbeat` request. + */ + PubNubClientEvent["SendHeartbeatRequest"] = "sendHeartbeatRequest"; + /** + * Core PubNub client module request to send `leave` request. + */ + PubNubClientEvent["SendLeaveRequest"] = "sendLeaveRequest"; + })(PubNubClientEvent || (PubNubClientEvent = {})); /** - * Subscription Service Worker Transport provider. - * - * Service worker provides support for PubNub subscription feature to give better user experience across - * multiple opened pages. - * - * @internal + * Base request processing event class. */ + class BasePubNubClientEvent extends CustomEvent { + /** + * Retrieve reference to PubNub client which dispatched event. + * + * @returns Reference to PubNub client which dispatched event. + */ + get client() { + return this.detail.client; + } + } /** - * Aggregation timer timeout. - * - * Timeout used by the timer to postpone `handleSendSubscribeRequestEvent` function call and let other clients for - * same subscribe key send next subscribe loop request (to make aggregation more efficient). + * Dispatched by PubNub client when it has been unregistered. */ - const subscribeAggregationTimeout = 50; + class PubNubClientUnregisterEvent extends BasePubNubClientEvent { + /** + * Create PubNub client unregister event. + * + * @param client - Reference to unregistered PubNub client. + */ + constructor(client) { + super(PubNubClientEvent.Unregister, { detail: { client } }); + } + /** + * Create a clone of `unregister` event to make it possible to forward event upstream. + * + * @returns Clone of `unregister` event. + */ + clone() { + return new PubNubClientUnregisterEvent(this.client); + } + } /** - * Map of clients aggregation keys to the started aggregation timeout timers with client and event information. + * Dispatched by PubNub client when it has been disconnected. */ - const aggregationTimers = new Map(); - // region State + class PubNubClientDisconnectEvent extends BasePubNubClientEvent { + /** + * Create PubNub client disconnect event. + * + * @param client - Reference to disconnected PubNub client. + */ + constructor(client) { + super(PubNubClientEvent.Disconnect, { detail: { client } }); + } + /** + * Create a clone of `disconnect` event to make it possible to forward event upstream. + * + * @returns Clone of `disconnect` event. + */ + clone() { + return new PubNubClientDisconnectEvent(this.client); + } + } /** - * Per-subscription key map of "offline" clients detection timeouts. + * Dispatched by PubNub client when it changes user identity (`userId` has been changed). */ - const pingTimeouts = {}; + class PubNubClientIdentityChangeEvent extends BasePubNubClientEvent { + /** + * Create PubNub client identity change event. + * + * @param client - Reference to the PubNub client which changed identity. + * @param oldUserId - User ID which has been previously used by the `client`. + * @param newUserId - User ID which will used by the `client`. + */ + constructor(client, oldUserId, newUserId) { + super(PubNubClientEvent.IdentityChange, { detail: { client, oldUserId, newUserId } }); + } + /** + * Retrieve `userId` which has been previously used by the `client`. + * + * @returns `userId` which has been previously used by the `client`. + */ + get oldUserId() { + return this.detail.oldUserId; + } + /** + * Retrieve `userId` which will used by the `client`. + * + * @returns `userId` which will used by the `client`. + */ + get newUserId() { + return this.detail.newUserId; + } + /** + * Create a clone of `identity` _change_ event to make it possible to forward event upstream. + * + * @returns Clone of `identity` _change_ event. + */ + clone() { + return new PubNubClientIdentityChangeEvent(this.client, this.oldUserId, this.newUserId); + } + } /** - * Unique shared worker instance identifier. + * Dispatched by PubNub client when it changes authentication data (`auth` has been changed). */ - const sharedWorkerIdentifier = uuidGenerator.createUUID(); + class PubNubClientAuthChangeEvent extends BasePubNubClientEvent { + /** + * Create PubNub client authentication change event. + * + * @param client - Reference to the PubNub client which changed authentication. + * @param [newAuth] - Authentication which will used by the `client`. + * @param [oldAuth] - Authentication which has been previously used by the `client`. + */ + constructor(client, newAuth, oldAuth) { + super(PubNubClientEvent.AuthChange, { detail: { client, oldAuth, newAuth } }); + } + /** + * Retrieve authentication which has been previously used by the `client`. + * + * @returns Authentication which has been previously used by the `client`. + */ + get oldAuth() { + return this.detail.oldAuth; + } + /** + * Retrieve authentication which will used by the `client`. + * + * @returns Authentication which will used by the `client`. + */ + get newAuth() { + return this.detail.newAuth; + } + /** + * Create a clone of `authentication` _change_ event to make it possible to forward event upstream. + * + * @returns Clone `authentication` _change_ event. + */ + clone() { + return new PubNubClientAuthChangeEvent(this.client, this.newAuth, this.oldAuth); + } + } /** - * Map of identifiers, scheduled by the Service Worker, to their abort controllers. - * - * **Note:** Because of message-based nature of interaction it will be impossible to pass actual {@link AbortController} - * to the transport provider code. + * Dispatched by PubNub client when it changes heartbeat interval. */ - const abortControllers = new Map(); + class PubNubClientHeartbeatIntervalChangeEvent extends BasePubNubClientEvent { + /** + * Create PubNub client heartbeat interval change event. + * + * @param client - Reference to the PubNub client which changed heartbeat interval. + * @param [newInterval] - New heartbeat request send interval. + * @param [oldInterval] - Previous heartbeat request send interval. + */ + constructor(client, newInterval, oldInterval) { + super(PubNubClientEvent.HeartbeatIntervalChange, { detail: { client, oldInterval, newInterval } }); + } + /** + * Retrieve previous heartbeat request send interval. + * + * @returns Previous heartbeat request send interval. + */ + get oldInterval() { + return this.detail.oldInterval; + } + /** + * Retrieve new heartbeat request send interval. + * + * @returns New heartbeat request send interval. + */ + get newInterval() { + return this.detail.newInterval; + } + /** + * Create a clone of the `heartbeat interval` _change_ event to make it possible to forward the event upstream. + * + * @returns Clone of `heartbeat interval` _change_ event. + */ + clone() { + return new PubNubClientHeartbeatIntervalChangeEvent(this.client, this.newInterval, this.oldInterval); + } + } /** - * Map of PubNub client identifiers to their state in the current Service Worker. + * Dispatched when the core PubNub client module requested to _send_ a `subscribe` request. */ - const pubNubClients = {}; + class PubNubClientSendSubscribeEvent extends BasePubNubClientEvent { + /** + * Create subscribe request send event. + * + * @param client - Reference to the PubNub client which requested to send request. + * @param request - Subscription request object. + */ + constructor(client, request) { + super(PubNubClientEvent.SendSubscribeRequest, { detail: { client, request } }); + } + /** + * Retrieve subscription request object. + * + * @returns Subscription request object. + */ + get request() { + return this.detail.request; + } + /** + * Create clone of _send_ `subscribe` request event to make it possible to forward event upstream. + * + * @returns Clone of _send_ `subscribe` request event. + */ + clone() { + return new PubNubClientSendSubscribeEvent(this.client, this.request); + } + } /** - * Per-subscription key list of PubNub client state. + * Dispatched when the core PubNub client module requested to _cancel_ `subscribe` request. */ - const pubNubClientsBySubscriptionKey = {}; + class PubNubClientCancelSubscribeEvent extends BasePubNubClientEvent { + /** + * Create `subscribe` request _cancel_ event. + * + * @param client - Reference to the PubNub client which requested to _send_ request. + * @param request - Subscription request object. + */ + constructor(client, request) { + super(PubNubClientEvent.CancelSubscribeRequest, { detail: { client, request } }); + } + /** + * Retrieve subscription request object. + * + * @returns Subscription request object. + */ + get request() { + return this.detail.request; + } + /** + * Create clone of _cancel_ `subscribe` request event to make it possible to forward event upstream. + * + * @returns Clone of _cancel_ `subscribe` request event. + */ + clone() { + return new PubNubClientCancelSubscribeEvent(this.client, this.request); + } + } /** - * Per-subscription key map of heartbeat request configurations recently used for user. + * Dispatched when the core PubNub client module requested to _send_ `heartbeat` request. */ - const serviceHeartbeatRequests = {}; + class PubNubClientSendHeartbeatEvent extends BasePubNubClientEvent { + /** + * Create `heartbeat` request _send_ event. + * + * @param client - Reference to the PubNub client which requested to send request. + * @param request - Heartbeat request object. + */ + constructor(client, request) { + super(PubNubClientEvent.SendHeartbeatRequest, { detail: { client, request } }); + } + /** + * Retrieve heartbeat request object. + * + * @returns Heartbeat request object. + */ + get request() { + return this.detail.request; + } + /** + * Create clone of _send_ `heartbeat` request event to make it possible to forward event upstream. + * + * @returns Clone of _send_ `heartbeat` request event. + */ + clone() { + return new PubNubClientSendHeartbeatEvent(this.client, this.request); + } + } /** - * Per-subscription key presence state associated with unique user identifiers with which {@link pubNubClients|clients} - * scheduled subscription request. + * Dispatched when the core PubNub client module requested to _send_ `leave` request. */ - const presenceState = {}; + class PubNubClientSendLeaveEvent extends BasePubNubClientEvent { + /** + * Create `leave` request _send_ event. + * + * @param client - Reference to the PubNub client which requested to send request. + * @param request - Leave request object. + */ + constructor(client, request) { + super(PubNubClientEvent.SendLeaveRequest, { detail: { client, request } }); + } + /** + * Retrieve leave request object. + * + * @returns Leave request object. + */ + get request() { + return this.detail.request; + } + /** + * Create clone of _send_ `leave` request event to make it possible to forward event upstream. + * + * @returns Clone of _send_ `leave` request event. + */ + clone() { + return new PubNubClientSendLeaveEvent(this.client, this.request); + } + } + + /** + * Type with events which is dispatched by PubNub clients manager and can be handled with callback passed to the + * {@link EventTarget#addEventListener|addEventListener}. + */ + var PubNubClientsManagerEvent; + (function (PubNubClientsManagerEvent) { + /** + * New PubNub client has been registered. + */ + PubNubClientsManagerEvent["Registered"] = "Registered"; + /** + * PubNub client has been unregistered. + */ + PubNubClientsManagerEvent["Unregistered"] = "Unregistered"; + })(PubNubClientsManagerEvent || (PubNubClientsManagerEvent = {})); + /** + * Dispatched by clients manager when new PubNub client registers within `SharedWorker`. + */ + class PubNubClientManagerRegisterEvent extends CustomEvent { + /** + * Create client registration event. + * + * @param client - Reference to the registered PubNub client. + */ + constructor(client) { + super(PubNubClientsManagerEvent.Registered, { detail: client }); + } + /** + * Retrieve reference to registered PubNub client. + * + * @returns Reference to registered PubNub client. + */ + get client() { + return this.detail; + } + /** + * Create clone of new client register event to make it possible to forward event upstream. + * + * @returns Client new client register event. + */ + clone() { + return new PubNubClientManagerRegisterEvent(this.client); + } + } + /** + * Dispatched by clients manager when PubNub client unregisters from `SharedWorker`. + */ + class PubNubClientManagerUnregisterEvent extends CustomEvent { + /** + * Create client unregistration event. + * + * @param client - Reference to the unregistered PubNub client. + * @param withLeave - Whether `leave` request should be sent or not. + */ + constructor(client, withLeave = false) { + super(PubNubClientsManagerEvent.Unregistered, { detail: { client, withLeave } }); + } + /** + * Retrieve reference to the unregistered PubNub client. + * + * @returns Reference to the unregistered PubNub client. + */ + get client() { + return this.detail.client; + } + /** + * Retrieve whether `leave` request should be sent or not. + * + * @returns `true` if `leave` request should be sent for previously used channels and groups. + */ + get withLeave() { + return this.detail.withLeave; + } + /** + * Create clone of client unregister event to make it possible to forward event upstream. + * + * @returns Client client unregister event. + */ + clone() { + return new PubNubClientManagerUnregisterEvent(this.client, this.withLeave); + } + } + + /** + * Type with events which is dispatched by subscription state in response to client-provided requests and PubNub + * client state change. + */ + var SubscriptionStateEvent; + (function (SubscriptionStateEvent) { + /** + * Subscription state has been changed. + */ + SubscriptionStateEvent["Changed"] = "changed"; + /** + * Subscription state has been invalidated after all clients' state was removed from it. + */ + SubscriptionStateEvent["Invalidated"] = "invalidated"; + })(SubscriptionStateEvent || (SubscriptionStateEvent = {})); + /** + * Dispatched by subscription state when state and service requests are changed. + */ + class SubscriptionStateChangeEvent extends CustomEvent { + /** + * Create subscription state change event. + * + * @param withInitialResponse - List of initial `client`-provided {@link SubscribeRequest|subscribe} requests with + * timetokens and regions that should be returned right away. + * @param newRequests - List of new service requests which need to be scheduled for processing. + * @param canceledRequests - List of previously scheduled service requests which should be cancelled. + * @param leaveRequest - Request which should be used to announce `leave` from part of the channels and groups. + */ + constructor(withInitialResponse, newRequests, canceledRequests, leaveRequest) { + super(SubscriptionStateEvent.Changed, { + detail: { withInitialResponse, newRequests, canceledRequests, leaveRequest }, + }); + } + /** + * Retrieve list of initial `client`-provided {@link SubscribeRequest|subscribe} requests with timetokens and regions + * that should be returned right away. + * + * @returns List of initial `client`-provided {@link SubscribeRequest|subscribe} requests with timetokens and regions + * that should be returned right away. + */ + get requestsWithInitialResponse() { + return this.detail.withInitialResponse; + } + /** + * Retrieve list of new service requests which need to be scheduled for processing. + * + * @returns List of new service requests which need to be scheduled for processing. + */ + get newRequests() { + return this.detail.newRequests; + } + /** + * Retrieve request which should be used to announce `leave` from part of the channels and groups. + * + * @returns Request which should be used to announce `leave` from part of the channels and groups. + */ + get leaveRequest() { + return this.detail.leaveRequest; + } + /** + * Retrieve list of previously scheduled service requests which should be cancelled. + * + * @returns List of previously scheduled service requests which should be cancelled. + */ + get canceledRequests() { + return this.detail.canceledRequests; + } + /** + * Create clone of subscription state change event to make it possible to forward event upstream. + * + * @returns Client subscription state change event. + */ + clone() { + return new SubscriptionStateChangeEvent(this.requestsWithInitialResponse, this.newRequests, this.canceledRequests, this.leaveRequest); + } + } + /** + * Dispatched by subscription state when it has been invalidated. + */ + class SubscriptionStateInvalidateEvent extends CustomEvent { + /** + * Create subscription state invalidation event. + */ + constructor() { + super(SubscriptionStateEvent.Invalidated); + } + /** + * Create clone of subscription state change event to make it possible to forward event upstream. + * + * @returns Client subscription state change event. + */ + clone() { + return new SubscriptionStateInvalidateEvent(); + } + } + + /** + * Type with events which is emitted by request and can be handled with callback passed to the + * {@link EventTarget#addEventListener|addEventListener}. + */ + var PubNubSharedWorkerRequestEvents; + (function (PubNubSharedWorkerRequestEvents) { + /** + * Request processing started. + */ + PubNubSharedWorkerRequestEvents["Started"] = "started"; + /** + * Request processing has been canceled. + * + * **Note:** This event dispatched only by client-provided requests. + */ + PubNubSharedWorkerRequestEvents["Canceled"] = "canceled"; + /** + * Request successfully completed. + */ + PubNubSharedWorkerRequestEvents["Success"] = "success"; + /** + * Request completed with error. + * + * Error can be caused by: + * - missing permissions (403) + * - network issues + */ + PubNubSharedWorkerRequestEvents["Error"] = "error"; + })(PubNubSharedWorkerRequestEvents || (PubNubSharedWorkerRequestEvents = {})); + /** + * Base request processing event class. + */ + class BaseRequestEvent extends CustomEvent { + /** + * Retrieve service (aggregated / updated) request. + * + * @returns Service (aggregated / updated) request. + */ + get request() { + return this.detail.request; + } + } + /** + * Dispatched by request when linked service request processing started. + */ + class RequestStartEvent extends BaseRequestEvent { + /** + * Create request processing start event. + * + * @param request - Service (aggregated / updated) request. + */ + constructor(request) { + super(PubNubSharedWorkerRequestEvents.Started, { detail: { request } }); + } + /** + * Create clone of request processing start event to make it possible to forward event upstream. + * + * @param request - Custom requests with this event should be cloned. + * @returns Client request processing start event. + */ + clone(request) { + return new RequestStartEvent(request !== null && request !== void 0 ? request : this.request); + } + } + /** + * Dispatched by request when linked service request processing completed. + */ + class RequestSuccessEvent extends BaseRequestEvent { + /** + * Create request processing success event. + * + * @param request - Service (aggregated / updated) request. + * @param fetchRequest - Actual request which has been used with {@link fetch}. + * @param response - PubNub service response. + */ + constructor(request, fetchRequest, response) { + super(PubNubSharedWorkerRequestEvents.Success, { detail: { request, fetchRequest, response } }); + } + /** + * Retrieve actual request which has been used with {@link fetch}. + * + * @returns Actual request which has been used with {@link fetch}. + */ + get fetchRequest() { + return this.detail.fetchRequest; + } + /** + * Retrieve PubNub service response. + * + * @returns Service response. + */ + get response() { + return this.detail.response; + } + /** + * Create clone of request processing success event to make it possible to forward event upstream. + * + * @param request - Custom requests with this event should be cloned. + * @returns Client request processing success event. + */ + clone(request) { + return new RequestSuccessEvent(request !== null && request !== void 0 ? request : this.request, request ? request.asFetchRequest : this.fetchRequest, this.response); + } + } + /** + * Dispatched by request when linked service request processing failed / service error response. + */ + class RequestErrorEvent extends BaseRequestEvent { + /** + * Create request processing error event. + * + * @param request - Service (aggregated / updated) request. + * @param fetchRequest - Actual request which has been used with {@link fetch}. + * @param error - Request processing error information. + */ + constructor(request, fetchRequest, error) { + super(PubNubSharedWorkerRequestEvents.Error, { detail: { request, fetchRequest, error } }); + } + /** + * Retrieve actual request which has been used with {@link fetch}. + * + * @returns Actual request which has been used with {@link fetch}. + */ + get fetchRequest() { + return this.detail.fetchRequest; + } + /** + * Retrieve request processing error description. + * + * @returns Request processing error description. + */ + get error() { + return this.detail.error; + } + /** + * Create clone of request processing failure event to make it possible to forward event upstream. + * + * @param request - Custom requests with this event should be cloned. + * @returns Client request processing failure event. + */ + clone(request) { + return new RequestErrorEvent(request !== null && request !== void 0 ? request : this.request, request ? request.asFetchRequest : this.fetchRequest, this.error); + } + } + /** + * Dispatched by request when it has been canceled. + */ + class RequestCancelEvent extends BaseRequestEvent { + /** + * Create request cancelling event. + * + * @param request - Client-provided (original) request. + */ + constructor(request) { + super(PubNubSharedWorkerRequestEvents.Canceled, { detail: { request } }); + } + /** + * Create clone of request cancel event to make it possible to forward event upstream. + * + * @param request - Custom requests with this event should be cloned. + * @returns Client request cancel event. + */ + clone(request) { + return new RequestCancelEvent(request !== null && request !== void 0 ? request : this.request); + } + } + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function getDefaultExportFromCjs (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + var uuid = {exports: {}}; + + /*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */ + uuid.exports; + + (function (module, exports) { + (function (root, factory) { + { + factory(exports); + if (module !== null) { + module.exports = exports.uuid; + } + } + }(commonjsGlobal, function (exports) { + var VERSION = '0.1.0'; + var uuidRegex = { + '3': /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i, + '4': /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, + '5': /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, + all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i + }; + + function uuid() { + var uuid = '', i, random; + for (i = 0; i < 32; i++) { + random = Math.random() * 16 | 0; + if (i === 8 || i === 12 || i === 16 || i === 20) uuid += '-'; + uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16); + } + return uuid + } + + function isUUID(str, version) { + var pattern = uuidRegex[version || 'all']; + return pattern && pattern.test(str) || false + } + + uuid.isUUID = isUUID; + uuid.VERSION = VERSION; + + exports.uuid = uuid; + exports.isUUID = isUUID; + })); + } (uuid, uuid.exports)); + + var uuidExports = uuid.exports; + var uuidGenerator$1 = /*@__PURE__*/getDefaultExportFromCjs(uuidExports); + + /** + * Random identifier generator helper module. + * + * @internal + */ + /** @internal */ + var uuidGenerator = { + createUUID() { + if (uuidGenerator$1.uuid) { + return uuidGenerator$1.uuid(); + } + // @ts-expect-error Depending on module type it may be callable. + return uuidGenerator$1(); + }, + }; + + /** + * Base shared worker request implementation. + * + * In the `SharedWorker` context, this base class is used both for `client`-provided (they won't be used for actual + * request) and those that are created by `SharedWorker` code (`service` request, which will be used in actual + * requests). + * + * **Note:** The term `service` request in inline documentation will mean request created by `SharedWorker` and used to + * call PubNub REST API. + */ + class BasePubNubRequest extends EventTarget { + // endregion + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + /** + * Create request object. + * + * @param request - Transport request. + * @param subscribeKey - Subscribe REST API access key. + * @param userId - Unique user identifier from the name of which request will be made. + * @param channels - List of channels used in request. + * @param channelGroups - List of channel groups used in request. + * @param [accessToken] - Access token with permissions to access provided `channels` and `channelGroups` on behalf of + * `userId`. + */ + constructor(request, subscribeKey, userId, channels, channelGroups, accessToken) { + super(); + this.request = request; + this.subscribeKey = subscribeKey; + this.channels = channels; + this.channelGroups = channelGroups; + /** + * Map of attached to the service request `client`-provided requests by their request identifiers. + * + * **Context:** `service`-provided requests only. + */ + this.dependents = {}; + /** + * Whether the request already received a service response or an error. + * + * **Important:** Any interaction with completed requests except requesting properties is prohibited. + */ + this._completed = false; + /** + * Whether request has been cancelled or not. + * + * **Important:** Any interaction with canceled requests except requesting properties is prohibited. + */ + this._canceled = false; + /** + * Stringify request query key/value pairs. + * + * @param query - Request query object. + * @returns Stringified query object. + */ + this.queryStringFromObject = (query) => { + return Object.keys(query) + .map((key) => { + const queryValue = query[key]; + if (!Array.isArray(queryValue)) + return `${key}=${this.encodeString(queryValue)}`; + return queryValue.map((value) => `${key}=${this.encodeString(value)}`).join('&'); + }) + .join('&'); + }; + this._accessToken = accessToken; + this._userId = userId; + } + // endregion + // -------------------------------------------------------- + // ---------------------- Properties ---------------------- + // -------------------------------------------------------- + // region Properties + /** + * Get the request's unique identifier. + * + * @returns Request's unique identifier. + */ + get identifier() { + return this.request.identifier; + } + /** + * Retrieve the origin that is used to access PubNub REST API. + * + * @returns Origin, which is used to access PubNub REST API. + */ + get origin() { + return this.request.origin; + } + /** + * Retrieve the unique user identifier from the name of which request will be made. + * + * @returns Unique user identifier from the name of which request will be made. + */ + get userId() { + return this._userId; + } + /** + * Update the unique user identifier from the name of which request will be made. + * + * @param value - New unique user identifier. + */ + set userId(value) { + this._userId = value; + // Patch underlying transport request query parameters to use new value. + this.request.queryParameters.uuid = value; + } + /** + * Retrieve access token with permissions to access provided `channels` and `channelGroups`. + * + * @returns Access token with permissions for {@link userId} or `undefined` if not set. + */ + get accessToken() { + return this._accessToken; + } + /** + * Update the access token which should be used to access provided `channels` and `channelGroups` by the user with + * {@link userId}. + * + * @param [value] - Access token with permissions for {@link userId}. + */ + set accessToken(value) { + this._accessToken = value; + // Patch underlying transport request query parameters to use new value. + if (value) + this.request.queryParameters.auth = value.toString(); + else + delete this.request.queryParameters.auth; + } + /** + * Retrieve {@link PubNubClient|PubNub} client associates with request. + * + * **Context:** `client`-provided requests only. + * + * @returns Reference to the {@link PubNubClient|PubNub} client that is sending the request. + */ + get client() { + return this._client; + } + /** + * Associate request with PubNub client. + * + * **Context:** `client`-provided requests only. + * + * @param value - {@link PubNubClient|PubNub} client that created request in `SharedWorker` context. + */ + set client(value) { + this._client = value; + } + /** + * Retrieve whether the request already received a service response or an error. + * + * @returns `true` if request already completed processing (not with {@link cancel}). + */ + get completed() { + return this._completed; + } + /** + * Retrieve whether the request can be cancelled or not. + * + * @returns `true` if there is a possibility and meaning to be able to cancel the request. + */ + get cancellable() { + return this.request.cancellable; + } + /** + * Retrieve whether the request has been canceled prior to completion or not. + * + * @returns `true` if the request didn't complete processing. + */ + get canceled() { + return this._canceled; + } + /** + * Update controller, which is used to cancel ongoing `service`-provided requests by signaling {@link fetch}. + * + * **Context:** `service`-provided requests only. + * + * @param value - Controller that has been used to signal {@link fetch} for request cancellation. + */ + set fetchAbortController(value) { + // There is no point in completed request `fetch` abort controller set. + if (this.completed || this.canceled) + return; + // Fetch abort controller can't be set for `client`-provided requests. + if (!this.isServiceRequest) { + console.error('Unexpected attempt to set fetch abort controller on client-provided request.'); + return; + } + if (this._fetchAbortController) { + console.error('Only one abort controller can be set for service-provided requests.'); + return; + } + this._fetchAbortController = value; + } + /** + * Retrieve `service`-provided fetch request abort controller. + * + * **Context:** `service`-provided requests only. + * + * @returns `service`-provided fetch request abort controller. + */ + get fetchAbortController() { + return this._fetchAbortController; + } + /** + * Represent transport request as {@link fetch} {@link Request}. + * + * @returns Ready-to-use {@link Request} instance. + */ + get asFetchRequest() { + const queryParameters = this.request.queryParameters; + const headers = {}; + let query = ''; + if (this.request.headers) + for (const [key, value] of Object.entries(this.request.headers)) + headers[key] = value; + if (queryParameters && Object.keys(queryParameters).length !== 0) + query = `?${this.queryStringFromObject(queryParameters)}`; + return new Request(`${this.origin}${this.request.path}${query}`, { + method: this.request.method, + headers: Object.keys(headers).length ? headers : undefined, + redirect: 'follow', + }); + } + /** + * Retrieve the service (aggregated/modified) request, which will actually be used to call the REST API endpoint. + * + * **Context:** `client`-provided requests only. + * + * @returns Service (aggregated/modified) request, which will actually be used to call the REST API endpoint. + */ + get serviceRequest() { + return this._serviceRequest; + } + /** + * Link request processing results to the service (aggregated/modified) request. + * + * **Context:** `client`-provided requests only. + * + * @param value - Service (aggregated/modified) request for which process progress should be observed. + */ + set serviceRequest(value) { + // This function shouldn't be called even unintentionally, on the `service`-provided requests. + if (this.isServiceRequest) { + console.error('Unexpected attempt to set service-provided request on service-provided request.'); + return; + } + const previousServiceRequest = this.serviceRequest; + this._serviceRequest = value; + // Detach from the previous service request if it has been changed (to a new one or unset). + if (previousServiceRequest && (!value || previousServiceRequest.identifier !== value.identifier)) + previousServiceRequest.detachRequest(this); + // There is no need to set attach to service request if either of them is already completed, or canceled. + if (this.completed || this.canceled || (value && (value.completed || value.canceled))) { + this._serviceRequest = undefined; + return; + } + if (previousServiceRequest && value && previousServiceRequest.identifier === value.identifier) + return; + // Attach the request to the service request processing results. + if (value) + value.attachRequest(this); + } + /** + * Retrieve whether the receiver is a `service`-provided request or not. + * + * @returns `true` if the request has been created by the `SharedWorker`. + */ + get isServiceRequest() { + return !this.client; + } + // endregion + // -------------------------------------------------------- + // ---------------------- Dependency ---------------------- + // -------------------------------------------------------- + // region Dependency + /** + * Retrieve a list of `client`-provided requests that have been attached to the `service`-provided request. + * + * **Context:** `service`-provided requests only. + * + * @returns List of attached `client`-provided requests. + */ + dependentRequests() { + // Return an empty list for `client`-provided requests. + if (!this.isServiceRequest) + return []; + return Object.values(this.dependents); + } + /** + * Attach the `client`-provided request to the receiver (`service`-provided request) to receive a response from the + * PubNub REST API. + * + * **Context:** `service`-provided requests only. + * + * @param request - `client`-provided request that should be attached to the receiver (`service`-provided request). + */ + attachRequest(request) { + // Request attachments works only on service requests. + if (!this.isServiceRequest || this.dependents[request.identifier]) { + if (!this.isServiceRequest) + console.error('Unexpected attempt to attach requests using client-provided request.'); + return; + } + this.dependents[request.identifier] = request; + this.addEventListenersForRequest(request); + } + /** + * Detach the `client`-provided request from the receiver (`service`-provided request) to ignore any response from the + * PubNub REST API. + * + * **Context:** `service`-provided requests only. + * + * @param request - `client`-provided request that should be attached to the receiver (`service`-provided request). + */ + detachRequest(request) { + // Request detachments works only on service requests. + if (!this.isServiceRequest || !this.dependents[request.identifier]) { + if (!this.isServiceRequest) + console.error('Unexpected attempt to detach requests using client-provided request.'); + return; + } + delete this.dependents[request.identifier]; + request.removeEventListenersFromRequest(); + // Because `service`-provided requests are created in response to the `client`-provided one we need to cancel the + // receiver if there are no more attached `client`-provided requests. + // This ensures that there will be no abandoned/dangling `service`-provided request in `SharedWorker` structures. + if (Object.keys(this.dependents).length === 0) + this.cancel('Cancel request'); + } + // endregion + // -------------------------------------------------------- + // ------------------ Request processing ------------------ + // -------------------------------------------------------- + // region Request processing + /** + * Notify listeners that ongoing request processing has been cancelled. + * + * **Note:** The current implementation doesn't let {@link PubNubClient|PubNub} directly call + * {@link cancel}, and it can be called from `SharedWorker` code logic. + * + * **Important:** Previously attached `client`-provided requests should be re-attached to another `service`-provided + * request or properly cancelled with {@link PubNubClient|PubNub} notification of the core PubNub client module. + * + * @param [reason] - Reason because of which the request has been cancelled. The request manager uses this to specify + * whether the `service`-provided request has been cancelled on-demand or because of timeout. + * @param [notifyDependent] - Whether dependent requests should receive cancellation error or not. + * @returns List of detached `client`-provided requests. + */ + cancel(reason, notifyDependent = false) { + // There is no point in completed request cancellation. + if (this.completed || this.canceled) { + return []; + } + const dependentRequests = this.dependentRequests(); + if (this.isServiceRequest) { + // Detach request if not interested in receiving request cancellation error (because of timeout). + // When switching between aggregated `service`-provided requests there is no need in handling cancellation of + // outdated request. + if (!notifyDependent) + dependentRequests.forEach((request) => (request.serviceRequest = undefined)); + if (this._fetchAbortController) { + this._fetchAbortController.abort(reason); + this._fetchAbortController = undefined; + } + } + else + this.serviceRequest = undefined; + this._canceled = true; + this.stopRequestTimeoutTimer(); + this.dispatchEvent(new RequestCancelEvent(this)); + return dependentRequests; + } + /** + * Create and return running request processing timeout timer. + * + * @returns Promise with timout timer resolution. + */ + requestTimeoutTimer() { + return new Promise((_, reject) => { + this._fetchTimeoutTimer = setTimeout(() => { + reject(new Error('Request timeout')); + this.cancel('Cancel because of timeout', true); + }, this.request.timeout * 1000); + }); + } + /** + * Stop request processing timeout timer without error. + */ + stopRequestTimeoutTimer() { + if (!this._fetchTimeoutTimer) + return; + clearTimeout(this._fetchTimeoutTimer); + this._fetchTimeoutTimer = undefined; + } + /** + * Handle request processing started by the request manager (actual sending). + */ + handleProcessingStarted() { + // Log out request processing start (will be made only for client-provided request). + this.logRequestStart(this); + this.dispatchEvent(new RequestStartEvent(this)); + } + /** + * Handle request processing successfully completed by request manager (actual sending). + * + * @param fetchRequest - Reference to the actual request that has been used with {@link fetch}. + * @param response - PubNub service response which is ready to be sent to the core PubNub client module. + */ + handleProcessingSuccess(fetchRequest, response) { + this.addRequestInformationForResult(this, fetchRequest, response); + this.logRequestSuccess(this, response); + this._completed = true; + this.stopRequestTimeoutTimer(); + this.dispatchEvent(new RequestSuccessEvent(this, fetchRequest, response)); + } + /** + * Handle request processing failed by request manager (actual sending). + * + * @param fetchRequest - Reference to the actual request that has been used with {@link fetch}. + * @param error - Request processing error description. + */ + handleProcessingError(fetchRequest, error) { + this.addRequestInformationForResult(this, fetchRequest, error); + this.logRequestError(this, error); + this._completed = true; + this.stopRequestTimeoutTimer(); + this.dispatchEvent(new RequestErrorEvent(this, fetchRequest, error)); + } + // endregion + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + /** + * Add `service`-provided request processing progress listeners for `client`-provided requests. + * + * **Context:** `service`-provided requests only. + * + * @param request - `client`-provided request that would like to observe `service`-provided request progress. + */ + addEventListenersForRequest(request) { + if (!this.isServiceRequest) { + console.error('Unexpected attempt to add listeners using a client-provided request.'); + return; + } + request.abortController = new AbortController(); + this.addEventListener(PubNubSharedWorkerRequestEvents.Started, (event) => { + if (!(event instanceof RequestStartEvent)) + return; + request.logRequestStart(event.request); + request.dispatchEvent(event.clone(request)); + }, { signal: request.abortController.signal, once: true }); + this.addEventListener(PubNubSharedWorkerRequestEvents.Success, (event) => { + if (!(event instanceof RequestSuccessEvent)) + return; + request.removeEventListenersFromRequest(); + request.addRequestInformationForResult(event.request, event.fetchRequest, event.response); + request.logRequestSuccess(event.request, event.response); + request._completed = true; + request.dispatchEvent(event.clone(request)); + }, { signal: request.abortController.signal, once: true }); + this.addEventListener(PubNubSharedWorkerRequestEvents.Error, (event) => { + if (!(event instanceof RequestErrorEvent)) + return; + request.removeEventListenersFromRequest(); + request.addRequestInformationForResult(event.request, event.fetchRequest, event.error); + request.logRequestError(event.request, event.error); + request._completed = true; + request.dispatchEvent(event.clone(request)); + }, { signal: request.abortController.signal, once: true }); + } + /** + * Remove listeners added to the `service` request. + * + * **Context:** `client`-provided requests only. + */ + removeEventListenersFromRequest() { + // Only client-provided requests add listeners. + if (this.isServiceRequest || !this.abortController) { + if (this.isServiceRequest) + console.error('Unexpected attempt to remove listeners using a client-provided request.'); + return; + } + this.abortController.abort(); + this.abortController = undefined; + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Check whether the request contains specified channels in the URI path and channel groups in the request query or + * not. + * + * @param channels - List of channels for which any entry should be checked in the request. + * @param channelGroups - List of channel groups for which any entry should be checked in the request. + * @returns `true` if receiver has at least one entry from provided `channels` or `channelGroups` in own URI. + */ + hasAnyChannelsOrGroups(channels, channelGroups) { + return (this.channels.some((channel) => channels.includes(channel)) || + this.channelGroups.some((channelGroup) => channelGroups.includes(channelGroup))); + } + /** + * Append request-specific information to the processing result. + * + * @param fetchRequest - Reference to the actual request that has been used with {@link fetch}. + * @param request - Reference to the client- or service-provided request with information for response. + * @param result - Request processing result that should be modified. + */ + addRequestInformationForResult(request, fetchRequest, result) { + if (this.isServiceRequest) + return; + result.clientIdentifier = this.client.identifier; + result.identifier = this.identifier; + result.url = fetchRequest.url; + } + /** + * Log to the core PubNub client module information about request processing start. + * + * @param request - Reference to the client- or service-provided request information about which should be logged. + */ + logRequestStart(request) { + if (this.isServiceRequest) + return; + this.client.logger.debug(() => ({ messageType: 'network-request', message: request.request })); + } + /** + * Log to the core PubNub client module information about request processing successful completion. + * + * @param request - Reference to the client- or service-provided request information about which should be logged. + * @param response - Reference to the PubNub service response. + */ + logRequestSuccess(request, response) { + if (this.isServiceRequest) + return; + this.client.logger.debug(() => { + const { status, headers, body } = response.response; + const fetchRequest = request.asFetchRequest; + // Copy Headers object content into plain Record. + Object.entries(headers).forEach(([key, value]) => (value)); + return { messageType: 'network-response', message: { status, url: fetchRequest.url, headers, body } }; + }); + } + /** + * Log to the core PubNub client module information about request processing error. + * + * @param request - Reference to the client- or service-provided request information about which should be logged. + * @param error - Request processing error information. + */ + logRequestError(request, error) { + if (this.isServiceRequest) + return; + if ((error.error ? error.error.message : 'Unknown').toLowerCase().includes('timeout')) { + this.client.logger.debug(() => ({ + messageType: 'network-request', + message: request.request, + details: 'Timeout', + canceled: true, + })); + } + else { + this.client.logger.warn(() => { + const { details, canceled } = this.errorDetailsFromSendingError(error); + let logDetails = details; + if (canceled) + logDetails = 'Aborted'; + else if (details.toLowerCase().includes('network')) + logDetails = 'Network error'; + return { + messageType: 'network-request', + message: request.request, + details: logDetails, + canceled: canceled, + failed: !canceled, + }; + }); + } + } + /** + * Retrieve error details from the error response object. + * + * @param error - Request fetch error object. + * @reruns Object with error details and whether it has been canceled or not. + */ + errorDetailsFromSendingError(error) { + const canceled = error.error ? error.error.type === 'TIMEOUT' || error.error.type === 'ABORTED' : false; + let details = error.error ? error.error.message : 'Unknown'; + if (error.response) { + const contentType = error.response.headers['content-type']; + if (error.response.body && + contentType && + (contentType.indexOf('javascript') !== -1 || contentType.indexOf('json') !== -1)) { + try { + const serviceResponse = JSON.parse(new TextDecoder().decode(error.response.body)); + if ('message' in serviceResponse) + details = serviceResponse.message; + else if ('error' in serviceResponse) { + if (typeof serviceResponse.error === 'string') + details = serviceResponse.error; + else if (typeof serviceResponse.error === 'object' && 'message' in serviceResponse.error) + details = serviceResponse.error.message; + } + } + catch (_) { } + } + if (details === 'Unknown') { + if (error.response.status >= 500) + details = 'Internal Server Error'; + else if (error.response.status == 400) + details = 'Bad request'; + else if (error.response.status == 403) + details = 'Access denied'; + else + details = `${error.response.status}`; + } + } + return { details, canceled }; + } + /** + * Percent-encode input string. + * + * **Note:** Encode content in accordance of the `PubNub` service requirements. + * + * @param input - Source string or number for encoding. + * @returns Percent-encoded string. + */ + encodeString(input) { + return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); + } + } + + class SubscribeRequest extends BasePubNubRequest { + // endregion + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + /** + * Create subscribe request from received _transparent_ transport request. + * + * @param request - Object with subscribe transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with read permissions on + * {@link SubscribeRequest.channels|channels} and {@link SubscribeRequest.channelGroups|channelGroups}. + * @returns Initialized and ready to use subscribe request. + */ + static fromTransportRequest(request, subscriptionKey, accessToken) { + return new SubscribeRequest(request, subscriptionKey, accessToken); + } + /** + * Create subscribe request from previously cached data. + * + * @param request - Object with subscribe transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [cachedChannelGroups] - Previously cached list of channel groups for subscription. + * @param [cachedChannels] - Previously cached list of channels for subscription. + * @param [cachedState] - Previously cached user's presence state for channels and groups. + * @param [accessToken] - Access token with read permissions on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + * @retusns Initialized and ready to use subscribe request. + */ + static fromCachedState(request, subscriptionKey, cachedChannelGroups, cachedChannels, cachedState, accessToken) { + return new SubscribeRequest(request, subscriptionKey, accessToken, cachedChannelGroups, cachedChannels, cachedState); + } + /** + * Create aggregated subscribe request. + * + * @param requests - List of subscribe requests for same the user. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link SubscribeRequest.channels|channels} and {@link SubscribeRequest.channelGroups|channelGroups}. + * @param timetokenOverride - Timetoken which should be used to patch timetoken in initial response. + * @param timetokenRegionOverride - Timetoken origin which should be used to patch timetoken origin in initial + * response. + * @returns Aggregated subscribe request which will be sent. + */ + static fromRequests(requests, accessToken, timetokenOverride, timetokenRegionOverride) { + const baseRequest = requests[Math.floor(Math.random() * requests.length)]; + const aggregatedRequest = Object.assign({}, baseRequest.request); + let state = {}; + const channelGroups = new Set(); + const channels = new Set(); + for (const request of requests) { + if (request.state) + state = Object.assign(Object.assign({}, state), request.state); + request.channelGroups.forEach(channelGroups.add, channelGroups); + request.channels.forEach(channels.add, channels); + } + // Update request channels list (if required). + if (channels.size || channelGroups.size) { + const pathComponents = aggregatedRequest.path.split('/'); + pathComponents[4] = channels.size ? [...channels].sort().join(',') : ','; + aggregatedRequest.path = pathComponents.join('/'); + } + // Update request channel groups list (if required). + if (channelGroups.size) + aggregatedRequest.queryParameters['channel-group'] = [...channelGroups].sort().join(','); + // Update request `state` (if required). + if (Object.keys(state).length) + aggregatedRequest.queryParameters.state = JSON.stringify(state); + else + delete aggregatedRequest.queryParameters.state; + if (accessToken) + aggregatedRequest.queryParameters.auth = accessToken.toString(); + aggregatedRequest.identifier = uuidGenerator.createUUID(); + // Create service request and link to its result other requests used in aggregation. + const request = new SubscribeRequest(aggregatedRequest, baseRequest.subscribeKey, accessToken); + for (const clientRequest of requests) + clientRequest.serviceRequest = request; + if (request.isInitialSubscribe && timetokenOverride && timetokenOverride !== '0') { + request.timetokenOverride = timetokenOverride; + if (timetokenRegionOverride) + request.timetokenRegionOverride = timetokenRegionOverride; + } + return request; + } + /** + * Create subscribe request from received _transparent_ transport request. + * + * @param request - Object with subscribe transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with read permissions on {@link SubscribeRequest.channels|channels} and + * {@link SubscribeRequest.channelGroups|channelGroups}. + * @param [cachedChannels] - Previously cached list of channels for subscription. + * @param [cachedChannelGroups] - Previously cached list of channel groups for subscription. + * @param [cachedState] - Previously cached user's presence state for channels and groups. + */ + constructor(request, subscriptionKey, accessToken, cachedChannelGroups, cachedChannels, cachedState) { + var _a; + // Retrieve information about request's origin (who initiated it). + const requireCachedStateReset = !!request.queryParameters && 'on-demand' in request.queryParameters; + delete request.queryParameters['on-demand']; + super(request, subscriptionKey, request.queryParameters.uuid, cachedChannels !== null && cachedChannels !== void 0 ? cachedChannels : SubscribeRequest.channelsFromRequest(request), cachedChannelGroups !== null && cachedChannelGroups !== void 0 ? cachedChannelGroups : SubscribeRequest.channelGroupsFromRequest(request), accessToken); + /** + * Request creation timestamp. + */ + this._creationDate = Date.now(); + /** + * Timetoken region which should be used to patch timetoken origin in initial response. + */ + this.timetokenRegionOverride = '0'; + // Shift on millisecond creation timestamp for two sequential requests. + if (this._creationDate <= SubscribeRequest.lastCreationDate) { + SubscribeRequest.lastCreationDate++; + this._creationDate = SubscribeRequest.lastCreationDate; + } + else + SubscribeRequest.lastCreationDate = this._creationDate; + this._requireCachedStateReset = requireCachedStateReset; + if (request.queryParameters['filter-expr']) + this.filterExpression = request.queryParameters['filter-expr']; + this._timetoken = ((_a = request.queryParameters.tt) !== null && _a !== void 0 ? _a : '0'); + if (request.queryParameters.tr) + this._region = request.queryParameters.tr; + if (cachedState) + this.state = cachedState; + // Clean up `state` from objects which is not used with request (if needed). + if (this.state || !request.queryParameters.state || request.queryParameters.state.length === 0) + return; + const state = JSON.parse(request.queryParameters.state); + for (const objectName of Object.keys(state)) + if (!this.channels.includes(objectName) && !this.channelGroups.includes(objectName)) + delete state[objectName]; + this.state = state; + } + // endregion + // -------------------------------------------------------- + // ---------------------- Properties ---------------------- + // -------------------------------------------------------- + // region Properties + /** + * Retrieve `subscribe` request creation timestamp. + * + * @returns `Subscribe` request creation timestamp. + */ + get creationDate() { + return this._creationDate; + } + /** + * Represent subscribe request as identifier. + * + * Generated identifier will be identical for requests created for the same user. + */ + get asIdentifier() { + const auth = this.accessToken ? this.accessToken.asIdentifier : undefined; + const id = `${this.userId}-${this.subscribeKey}${auth ? `-${auth}` : ''}`; + return this.filterExpression ? `${id}-${this.filterExpression}` : id; + } + /** + * Retrieve whether this is initial subscribe request or not. + * + * @returns `true` if subscribe REST API called with missing or `tt=0` query parameter. + */ + get isInitialSubscribe() { + return this._timetoken === '0'; + } + /** + * Retrieve subscription loop timetoken. + * + * @returns Subscription loop timetoken. + */ + get timetoken() { + return this._timetoken; + } + /** + * Update subscription loop timetoken. + * + * @param value - New timetoken that should be used in PubNub REST API calls. + */ + set timetoken(value) { + this._timetoken = value; + // Update value for transport request object. + this.request.queryParameters.tt = value; + } + /** + * Retrieve subscription loop timetoken's region. + * + * @returns Subscription loop timetoken's region. + */ + get region() { + return this._region; + } + /** + * Update subscription loop timetoken's region. + * + * @param value - New timetoken's region that should be used in PubNub REST API calls. + */ + set region(value) { + this._region = value; + // Update value for transport request object. + if (value) + this.request.queryParameters.tr = value; + else + delete this.request.queryParameters.tr; + } + /** + * Retrieve whether the request requires the client's cached subscription state reset or not. + * + * @returns `true` if a subscribe request has been created on user request (`subscribe()` call) or not. + */ + get requireCachedStateReset() { + return this._requireCachedStateReset; + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Check whether client's subscription state cache can be used for new request or not. + * + * @param request - Transport request from the core PubNub client module with request origin information. + * @returns `true` if request created not by user (subscription loop). + */ + static useCachedState(request) { + return !!request.queryParameters && !('on-demand' in request.queryParameters); + } + /** + * Reset the inner state of the `subscribe` request object to the one that `initial` requests. + */ + resetToInitialRequest() { + this._requireCachedStateReset = true; + this._timetoken = '0'; + this._region = undefined; + delete this.request.queryParameters.tt; + } + /** + * Check whether received is a subset of another `subscribe` request. + * + * If the receiver is a subset of another means: + * - list of channels of another `subscribe` request includes all channels from the receiver, + * - list of channel groups of another `subscribe` request includes all channel groups from the receiver, + * - receiver's timetoken equal to `0` or another request `timetoken`. + * + * @param request - Request that should be checked to be a superset of received. + * @retuns `true` in case if the receiver is a subset of another `subscribe` request. + */ + isSubsetOf(request) { + if (request.channelGroups.length && !this.includesStrings(request.channelGroups, this.channelGroups)) + return false; + if (request.channels.length && !this.includesStrings(request.channels, this.channels)) + return false; + return this.timetoken === '0' || this.timetoken === request.timetoken || request.timetoken === '0'; + } + /** + * Serialize request for easier representation in logs. + * + * @returns Stringified `subscribe` request. + */ + toString() { + return `SubscribeRequest { clientIdentifier: ${this.client ? this.client.identifier : 'service request'}, requestIdentifier: ${this.identifier}, serviceRequestIdentified: ${this.client ? (this.serviceRequest ? this.serviceRequest.identifier : "'not set'") : "'is service request"}, channels: [${this.channels.length ? this.channels.map((channel) => `'${channel}'`).join(', ') : ''}], channelGroups: [${this.channelGroups.length ? this.channelGroups.map((group) => `'${group}'`).join(', ') : ''}], timetoken: ${this.timetoken}, region: ${this.region}, reset: ${this._requireCachedStateReset ? "'reset'" : "'do not reset'"} }`; + } + /** + * Serialize request to "typed" JSON string. + * + * @returns "Typed" JSON string. + */ + toJSON() { + return this.toString(); + } + /** + * Extract list of channels for subscription from request URI path. + * + * @param request - Transport request from which should be extracted list of channels for presence announcement. + * + * @returns List of channel names (not percent-decoded) for which `subscribe` has been called. + */ + static channelsFromRequest(request) { + const channels = request.path.split('/')[4]; + return channels === ',' ? [] : channels.split(',').filter((name) => name.length > 0); + } + /** + * Extract list of channel groups for subscription from request query. + * + * @param request - Transport request from which should be extracted list of channel groups for presence announcement. + * + * @returns List of channel group names (not percent-decoded) for which `subscribe` has been called. + */ + static channelGroupsFromRequest(request) { + if (!request.queryParameters || !request.queryParameters['channel-group']) + return []; + const group = request.queryParameters['channel-group']; + return group.length === 0 ? [] : group.split(',').filter((name) => name.length > 0); + } + /** + * Check whether {@link main} array contains all entries from {@link sub} array. + * + * @param main - Main array with which `intersection` with {@link sub} should be checked. + * @param sub - Sub-array whose values should be checked in {@link main}. + * + * @returns `true` if all entries from {@link sub} is present in {@link main}. + */ + includesStrings(main, sub) { + const set = new Set(main); + return sub.every(set.has, set); + } + } + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information /** - * Per-subscription key map of client identifiers to the Shared Worker {@link MessagePort}. + * Global subscription request creation date tracking. * - * Shared Worker {@link MessagePort} represent specific PubNub client which connected to the Shared Worker. + * Tracking is required to handle about rapid requests receive and need to know which of them were earlier. */ - const sharedWorkerClients = {}; + SubscribeRequest.lastCreationDate = 0; + /** - * List of ongoing subscription requests. + * PubNub access token. * - * **Node:** Identifiers differ from request identifiers received in {@link SendRequestEvent} object. + * Object used to simplify manipulations with requests (aggregation) in the Shared Worker context. */ - const serviceRequests = {}; - // endregion - // -------------------------------------------------------- - // ------------------- Event Handlers --------------------- - // -------------------------------------------------------- - // region Event Handlers + class AccessToken { + /** + * Token comparison based on expiration date. + * + * The access token with the most distant expiration date (which should be used in requests) will be at the end of the + * sorted array. + * + * **Note:** `compare` used with {@link Array.sort|sort} function to identify token with more distant expiration date. + * + * @param lhToken - Left-hand access token which will be used in {@link Array.sort|sort} comparison. + * @param rhToken - Right-hand access token which will be used in {@link Array.sort|sort} comparison. + * @returns Comparison result. + */ + static compare(lhToken, rhToken) { + var _a, _b; + const lhTokenExpiration = (_a = lhToken.expiration) !== null && _a !== void 0 ? _a : 0; + const rhTokenExpiration = (_b = rhToken.expiration) !== null && _b !== void 0 ? _b : 0; + return lhTokenExpiration - rhTokenExpiration; + } + /** + * Create access token object for PubNub client. + * + * @param token - Authorization key or access token for `read` access to the channels and groups. + * @param [simplifiedToken] - Simplified access token based only on content of `resources`, `patterns`, and + * `authorized_uuid`. + * @param [expiration] - Access token expiration date. + */ + constructor(token, simplifiedToken, expiration) { + this.token = token; + this.simplifiedToken = simplifiedToken; + this.expiration = expiration; + } + /** + * Represent the access token as identifier. + * + * @returns String that lets us identify other access tokens that have similar configurations. + */ + get asIdentifier() { + var _a; + return (_a = this.simplifiedToken) !== null && _a !== void 0 ? _a : this.token; + } + /** + * Check whether two access token objects represent the same permissions or not. + * + * @param other - Other access token that should be used in comparison. + * @param checkExpiration - Whether the token expiration date also should be compared or not. + * @returns `true` if received and another access token object represents the same permissions (and `expiration` if + * has been requested). + */ + equalTo(other, checkExpiration = false) { + return this.asIdentifier === other.asIdentifier && (checkExpiration ? this.expiration === other.expiration : true); + } + /** + * Check whether the receiver is a newer auth token than another. + * + * @param other - Other access token that should be used in comparison. + * @returns `true` if received has a more distant expiration date than another token. + */ + isNewerThan(other) { + return this.simplifiedToken ? this.expiration > other.expiration : false; + } + /** + * Stringify object to actual access token / key value. + * + * @returns Actual access token / key value. + */ + toString() { + return this.token; + } + } + /** - * Handle new PubNub client 'connection'. + * Enum representing possible transport methods for HTTP requests. * - * Echo listeners to let `SharedWorker` users that it is ready. + * @enum {number} + */ + var TransportMethod; + (function (TransportMethod) { + /** + * Request will be sent using `GET` method. + */ + TransportMethod["GET"] = "GET"; + /** + * Request will be sent using `POST` method. + */ + TransportMethod["POST"] = "POST"; + /** + * Request will be sent using `PATCH` method. + */ + TransportMethod["PATCH"] = "PATCH"; + /** + * Request will be sent using `DELETE` method. + */ + TransportMethod["DELETE"] = "DELETE"; + /** + * Local request. + * + * Request won't be sent to the service and probably used to compute URL. + */ + TransportMethod["LOCAL"] = "LOCAL"; + })(TransportMethod || (TransportMethod = {})); + + class LeaveRequest extends BasePubNubRequest { + // endregion + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + /** + * Create `leave` request from received _transparent_ transport request. + * + * @param request - Object with heartbeat transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + * @returns Initialized and ready to use `leave` request. + */ + static fromTransportRequest(request, subscriptionKey, accessToken) { + return new LeaveRequest(request, subscriptionKey, accessToken); + } + /** + * Create `leave` request from received _transparent_ transport request. + * + * @param request - Object with heartbeat transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + */ + constructor(request, subscriptionKey, accessToken) { + const allChannelGroups = LeaveRequest.channelGroupsFromRequest(request); + const allChannels = LeaveRequest.channelsFromRequest(request); + const channelGroups = allChannelGroups.filter((group) => !group.endsWith('-pnpres')); + const channels = allChannels.filter((channel) => !channel.endsWith('-pnpres')); + super(request, subscriptionKey, request.queryParameters.uuid, channels, channelGroups, accessToken); + this.allChannelGroups = allChannelGroups; + this.allChannels = allChannels; + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Serialize request for easier representation in logs. + * + * @returns Stringified `leave` request. + */ + toString() { + return `LeaveRequest { channels: [${this.channels.length ? this.channels.map((channel) => `'${channel}'`).join(', ') : ''}], channelGroups: [${this.channelGroups.length ? this.channelGroups.map((group) => `'${group}'`).join(', ') : ''}] }`; + } + /** + * Serialize request to "typed" JSON string. + * + * @returns "Typed" JSON string. + */ + toJSON() { + return this.toString(); + } + /** + * Extract list of channels for presence announcement from request URI path. + * + * @param request - Transport request from which should be extracted list of channels for presence announcement. + * + * @returns List of channel names (not percent-decoded) for which `leave` has been called. + */ + static channelsFromRequest(request) { + const channels = request.path.split('/')[6]; + return channels === ',' ? [] : channels.split(',').filter((name) => name.length > 0); + } + /** + * Extract list of channel groups for presence announcement from request query. + * + * @param request - Transport request from which should be extracted list of channel groups for presence announcement. + * + * @returns List of channel group names (not percent-decoded) for which `leave` has been called. + */ + static channelGroupsFromRequest(request) { + if (!request.queryParameters || !request.queryParameters['channel-group']) + return []; + const group = request.queryParameters['channel-group']; + return group.length === 0 ? [] : group.split(',').filter((name) => name.length > 0); + } + } + + /** + * Create service `leave` request for a specific PubNub client with channels and groups for removal. * - * @param event - Remote `SharedWorker` client connection event. + * @param client - Reference to the PubNub client whose credentials should be used for new request. + * @param channels - List of channels that are not used by any other clients and can be left. + * @param channelGroups - List of channel groups that are not used by any other clients and can be left. + * @returns Service `leave` request. */ - self.onconnect = (event) => { - consoleLog('New PubNub Client connected to the Subscription Shared Worker.'); - event.ports.forEach((receiver) => { - receiver.start(); - receiver.onmessage = (event) => { - // Ignoring unknown event payloads. - if (!validateEventPayload(event)) - return; - const data = event.data; - if (data.type === 'client-register') { - // Appending information about messaging port for responses. - data.port = receiver; - registerClientIfRequired(data); - consoleLog(`Client '${data.clientIdentifier}' registered with '${sharedWorkerIdentifier}' shared worker`); - } - else if (data.type === 'client-update') - updateClientInformation(data); - else if (data.type === 'client-unregister') - unRegisterClient(data); - else if (data.type === 'client-pong') - handleClientPong(data); - else if (data.type === 'send-request') { - if (data.request.path.startsWith('/v2/subscribe')) { - const changedSubscription = updateClientSubscribeStateIfRequired(data); - const client = pubNubClients[data.clientIdentifier]; - if (client) { - // Check whether there are more clients which may schedule next subscription loop and they need to be - // aggregated or not. - const timerIdentifier = aggregateTimerId(client); - let enqueuedClients = []; - if (aggregationTimers.has(timerIdentifier)) - enqueuedClients = aggregationTimers.get(timerIdentifier)[0]; - enqueuedClients.push([client, data]); - // Clear existing aggregation timer if subscription list changed. - if (aggregationTimers.has(timerIdentifier) && changedSubscription) { - clearTimeout(aggregationTimers.get(timerIdentifier)[1]); - aggregationTimers.delete(timerIdentifier); - } - // Check whether we need to start new aggregation timer or not. - if (!aggregationTimers.has(timerIdentifier)) { - const aggregationTimer = setTimeout(() => { - handleSendSubscribeRequestEventForClients(enqueuedClients, data); - aggregationTimers.delete(timerIdentifier); - }, subscribeAggregationTimeout); - aggregationTimers.set(timerIdentifier, [enqueuedClients, aggregationTimer]); - } - } - } - else if (data.request.path.endsWith('/heartbeat')) { - updateClientHeartbeatState(data); - handleHeartbeatRequestEvent(data); - } - else - handleSendLeaveRequestEvent(data); - } - else if (data.type === 'cancel-request') - handleCancelRequestEvent(data); - }; - receiver.postMessage({ type: 'shared-worker-connected' }); - }); + const leaveRequest = (client, channels, channelGroups) => { + channels = channels + .filter((channel) => !channel.endsWith('-pnpres')) + .map((channel) => encodeString(channel)) + .sort(); + channelGroups = channelGroups + .filter((channelGroup) => !channelGroup.endsWith('-pnpres')) + .map((channelGroup) => encodeString(channelGroup)) + .sort(); + if (channels.length === 0 && channelGroups.length === 0) + return undefined; + const channelGroupsString = channelGroups.length > 0 ? channelGroups.join(',') : undefined; + const channelsString = channels.length === 0 ? ',' : channels.join(','); + const query = Object.assign(Object.assign({ instanceid: client.identifier, uuid: client.userId, requestid: uuidGenerator.createUUID() }, (client.accessToken ? { auth: client.accessToken.toString() } : {})), (channelGroupsString ? { 'channel-group': channelGroupsString } : {})); + const transportRequest = { + origin: client.origin, + path: `/v2/presence/sub-key/${client.subKey}/channel/${channelsString}/leave`, + queryParameters: query, + method: TransportMethod.GET, + headers: {}, + timeout: 10, + cancellable: false, + compressible: false, + identifier: query.requestid, + }; + return LeaveRequest.fromTransportRequest(transportRequest, client.subKey, client.accessToken); }; /** - * Handle aggregated clients request to send subscription request. + * Percent-encode input string. + * + * **Note:** Encode content in accordance of the `PubNub` service requirements. * - * @param clients - List of aggregated clients which would like to send subscription requests. - * @param event - Subscription event details. + * @param input - Source string or number for encoding. + * @returns Percent-encoded string. */ - const handleSendSubscribeRequestEventForClients = (clients, event) => { - const requestOrId = subscribeTransportRequestFromEvent(event); - const client = pubNubClients[event.clientIdentifier]; - if (!client) - return; - // Getting rest of aggregated clients. - clients = clients.filter((aggregatedClient) => aggregatedClient[0].clientIdentifier !== client.clientIdentifier); - handleSendSubscribeRequestForClient(client, event, requestOrId, true); - clients.forEach(([aggregatedClient, clientEvent]) => handleSendSubscribeRequestForClient(aggregatedClient, clientEvent, requestOrId, false)); + const encodeString = (input) => { + return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); }; + + class SubscriptionStateChange { + // endregion + // -------------------------------------------------------- + // --------------------- Constructor ---------------------- + // -------------------------------------------------------- + // region Constructor + /** + * Squash changes to exclude repetitive removal and addition of the same requests in a single change transaction. + * + * @param changes - List of changes that should be analyzed and squashed if possible. + * @returns List of changes that doesn't have self-excluding change requests. + */ + static squashedChanges(changes) { + if (!changes.length || changes.length === 1) + return changes; + // Sort changes in order in which they have been created (original `changes` is Set). + const sortedChanges = changes.sort((lhc, rhc) => lhc.timestamp - rhc.timestamp); + // Remove changes which first add and then remove same request (removes both addition and removal change entry). + const requestAddChange = sortedChanges.filter((change) => !change.remove); + requestAddChange.forEach((addChange) => { + for (let idx = 0; idx < requestAddChange.length; idx++) { + const change = requestAddChange[idx]; + if (!change.remove || change.request.identifier !== addChange.request.identifier) + continue; + sortedChanges.splice(idx, 1); + sortedChanges.splice(sortedChanges.indexOf(addChange), 1); + break; + } + }); + // Filter out old `add` change entries for the same client. + const addChangePerClient = {}; + requestAddChange.forEach((change) => { + if (addChangePerClient[change.clientIdentifier]) { + const changeIdx = sortedChanges.indexOf(change); + if (changeIdx >= 0) + sortedChanges.splice(changeIdx, 1); + } + addChangePerClient[change.clientIdentifier] = change; + }); + return sortedChanges; + } + /** + * Create subscription state batched change entry. + * + * @param clientIdentifier - Identifier of the {@link PubNubClient|PubNub} client that provided data for subscription + * state change. + * @param request - Request that should be used during batched subscription state modification. + * @param remove - Whether provided {@link request} should be removed from `subscription` state or not. + * @param sendLeave - Whether the {@link PubNubClient|client} should send a presence `leave` request for _free_ + * channels and groups or not. + * @param [clientInvalidate=false] - Whether the `subscription` state change was caused by the + * {@link PubNubClient|PubNub} client invalidation (unregister) or not. + */ + constructor(clientIdentifier, request, remove, sendLeave, clientInvalidate = false) { + this.clientIdentifier = clientIdentifier; + this.request = request; + this.remove = remove; + this.sendLeave = sendLeave; + this.clientInvalidate = clientInvalidate; + this._timestamp = this.timestampForChange(); + } + // endregion + // -------------------------------------------------------- + // --------------------- Properties ----------------------- + // -------------------------------------------------------- + // region Properties + /** + * Retrieve subscription change enqueue timestamp. + * + * @returns Subscription change enqueue timestamp. + */ + get timestamp() { + return this._timestamp; + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Serialize object for easier representation in logs. + * + * @returns Stringified `subscription` state object. + */ + toString() { + return `SubscriptionStateChange { timestamp: ${this.timestamp}, client: ${this.clientIdentifier}, request: ${this.request.toString()}, remove: ${this.remove ? "'remove'" : "'do not remove'"}, sendLeave: ${this.sendLeave ? "'send'" : "'do not send'"} }`; + } + /** + * Serialize the object to a "typed" JSON string. + * + * @returns "Typed" JSON string. + */ + toJSON() { + return this.toString(); + } + /** + * Retrieve timestamp when change has been added to the batch. + * + * Non-repetitive timestamp required for proper changes sorting and identification of requests which has been removed + * and added during single batch. + * + * @returns Non-repetitive timestamp even for burst changes. + */ + timestampForChange() { + const timestamp = Date.now(); + if (timestamp <= SubscriptionStateChange.previousChangeTimestamp) { + SubscriptionStateChange.previousChangeTimestamp++; + } + else + SubscriptionStateChange.previousChangeTimestamp = timestamp; + return SubscriptionStateChange.previousChangeTimestamp; + } + } + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information /** - * Handle subscribe request by single client. + * Timestamp when batched changes has been modified before. + */ + SubscriptionStateChange.previousChangeTimestamp = 0; + /** + * Aggregated subscription state. * - * @param client - Client which processes `request`. - * @param event - Subscription event details. - * @param requestOrId - New aggregated request object or its identifier (if already scheduled). - * @param requestOrigin - Whether `client` is the one who triggered subscribe request or not. + * State object responsible for keeping in sync and optimization of `client`-provided {@link SubscribeRequest|requests} + * by attaching them to already existing or new aggregated `service`-provided {@link SubscribeRequest|requests} to + * reduce number of concurrent connections. */ - const handleSendSubscribeRequestForClient = (client, event, requestOrId, requestOrigin) => { - var _a; - let isInitialSubscribe = false; - if (!requestOrigin && typeof requestOrId !== 'string') - requestOrId = requestOrId.identifier; - if (client.subscription) - isInitialSubscribe = client.subscription.timetoken === '0'; - if (typeof requestOrId === 'string') { - const scheduledRequest = serviceRequests[requestOrId]; - if (client) { - if (client.subscription) { - // Updating client timetoken information. - client.subscription.refreshTimestamp = Date.now(); - client.subscription.timetoken = scheduledRequest.timetoken; - client.subscription.region = scheduledRequest.region; - client.subscription.serviceRequestId = requestOrId; + class SubscriptionState extends EventTarget { + // endregion + // -------------------------------------------------------- + // --------------------- Constructor ---------------------- + // -------------------------------------------------------- + // region Constructor + /** + * Create subscription state management object. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + constructor(identifier) { + super(); + this.identifier = identifier; + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + /** + * Map of `client`-provided request identifiers to the subscription state listener abort controller. + */ + this.requestListenersAbort = {}; + /** + * Map of {@link PubNubClient|client} identifiers to their portion of data which affects subscription state. + * + * **Note:** This information is removed only with the {@link SubscriptionState.removeClient|removeClient} function + * call. + */ + this.clientsState = {}; + /** + * Map of {@link PubNubClient|client} to its {@link SubscribeRequest|request} that already received response/error + * or has been canceled. + */ + this.lastCompletedRequest = {}; + /** + * List of identifiers of the {@link PubNubClient|PubNub} clients that should be invalidated when it will be + * possible. + */ + this.clientsForInvalidation = []; + /** + * Map of {@link PubNubClient|client} to its {@link SubscribeRequest|request} which is pending for + * `service`-provided {@link SubscribeRequest|request} processing results. + */ + this.requests = {}; + /** + * Aggregated/modified {@link SubscribeRequest|subscribe} requests which is used to call PubNub REST API. + * + * **Note:** There could be multiple requests to handle the situation when similar {@link PubNubClient|PubNub} clients + * have subscriptions but with different timetokens (if requests have intersecting lists of channels and groups they + * can be merged in the future if a response on a similar channel will be received and the same `timetoken` will be + * used for continuation). + */ + this.serviceRequests = []; + /** + * Cached list of channel groups used with recent aggregation service requests. + * + * **Note:** Set required to have the ability to identify which channel groups have been added/removed with recent + * {@link SubscriptionStateChange|changes} list processing. + */ + this.channelGroups = new Set(); + /** + * Cached list of channels used with recent aggregation service requests. + * + * **Note:** Set required to have the ability to identify which channels have been added/removed with recent + * {@link SubscriptionStateChange|changes} list processing. + */ + this.channels = new Set(); + } + // endregion + // -------------------------------------------------------- + // ---------------------- Accessors ----------------------- + // -------------------------------------------------------- + // region Accessors + /** + * Check whether subscription state contain state for specific {@link PubNubClient|PubNub} client. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client for which state should be checked. + * @returns `true` if there is state related to the {@link PubNubClient|client}. + */ + hasStateForClient(client) { + return !!this.clientsState[client.identifier]; + } + /** + * Retrieve portion of subscription state which is unique for the {@link PubNubClient|client}. + * + * Function will return list of channels and groups which has been introduced by the client into the state (no other + * clients have them). + * + * @param client - Reference to the {@link PubNubClient|PubNub} client for which unique elements should be retrieved + * from the state. + * @param channels - List of client's channels from subscription state. + * @param channelGroups - List of client's channel groups from subscription state. + * @returns State with channels and channel groups unique for the {@link PubNubClient|client}. + */ + uniqueStateForClient(client, channels, channelGroups) { + let uniqueChannelGroups = [...channelGroups]; + let uniqueChannels = [...channels]; + Object.entries(this.clientsState).forEach(([identifier, state]) => { + if (identifier === client.identifier) + return; + uniqueChannelGroups = uniqueChannelGroups.filter((channelGroup) => !state.channelGroups.has(channelGroup)); + uniqueChannels = uniqueChannels.filter((channel) => !state.channels.has(channel)); + }); + return { channels: uniqueChannels, channelGroups: uniqueChannelGroups }; + } + /** + * Retrieve ongoing `client`-provided {@link SubscribeRequest|subscribe} request for the {@link PubNubClient|client}. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client for which requests should be retrieved. + * @param [invalidated=false] - Whether receiving request for invalidated (unregistered) {@link PubNubClient|PubNub} + * client. + * @returns A `client`-provided {@link SubscribeRequest|subscribe} request if it has been sent by + * {@link PubNubClient|client}. + */ + requestForClient(client, invalidated = false) { + var _a; + return (_a = this.requests[client.identifier]) !== null && _a !== void 0 ? _a : (invalidated ? this.lastCompletedRequest[client.identifier] : undefined); + } + // endregion + // -------------------------------------------------------- + // --------------------- Aggregation ---------------------- + // -------------------------------------------------------- + // region Aggregation + /** + * Update access token for the client which should be used with next subscribe request. + * + * @param accessToken - Access token for next subscribe REST API call. + */ + updateClientAccessToken(accessToken) { + if (!this.accessToken || accessToken.isNewerThan(this.accessToken)) + this.accessToken = accessToken; + } + /** + * Mark specific client as suitable for state invalidation when it will be appropriate. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which should be invalidated when will be + * possible. + */ + invalidateClient(client) { + if (this.clientsForInvalidation.includes(client.identifier)) + return; + this.clientsForInvalidation.push(client.identifier); + } + /** + * Process batched subscription state change. + * + * @param changes - List of {@link SubscriptionStateChange|changes} made from requests received from the core + * {@link PubNubClient|PubNub} client modules. + */ + processChanges(changes) { + if (changes.length) + changes = SubscriptionStateChange.squashedChanges(changes); + if (!changes.length) + return; + let stateRefreshRequired = this.channelGroups.size === 0 && this.channels.size === 0; + if (!stateRefreshRequired) + stateRefreshRequired = changes.some((change) => change.remove || change.request.requireCachedStateReset); + // Update list of PubNub client requests. + const appliedRequests = this.applyChanges(changes); + let stateChanges; + if (stateRefreshRequired) + stateChanges = this.refreshInternalState(); + // Identify and dispatch subscription state change event with service requests for cancellation and start. + this.handleSubscriptionStateChange(changes, stateChanges, appliedRequests.initial, appliedRequests.continuation, appliedRequests.removed); + // Check whether subscription state for all registered clients has been removed or not. + if (!Object.keys(this.clientsState).length) + this.dispatchEvent(new SubscriptionStateInvalidateEvent()); + } + /** + * Make changes to the internal state. + * + * Categorize changes by grouping requests (into `initial`, `continuation`, and `removed` groups) and update internal + * state to reflect those changes (add/remove `client`-provided requests). + * + * @param changes - Final subscription state changes list. + * @returns Subscribe request separated by different subscription loop stages. + */ + applyChanges(changes) { + const continuationRequests = []; + const initialRequests = []; + const removedRequests = []; + changes.forEach((change) => { + const { remove, request, clientIdentifier, clientInvalidate } = change; + if (!remove) { + if (request.isInitialSubscribe) + initialRequests.push(request); + else + continuationRequests.push(request); + this.requests[clientIdentifier] = request; + this.addListenersForRequestEvents(request); + } + if (remove && (!!this.requests[clientIdentifier] || !!this.lastCompletedRequest[clientIdentifier])) { + if (clientInvalidate) { + delete this.lastCompletedRequest[clientIdentifier]; + delete this.clientsState[clientIdentifier]; + } + delete this.requests[clientIdentifier]; + removedRequests.push(request); } - if (!isInitialSubscribe) + }); + return { initial: initialRequests, continuation: continuationRequests, removed: removedRequests }; + } + /** + * Process changes in subscription state. + * + * @param changes - Final subscription state changes list. + * @param stateChanges - Changes to the subscribed channels and groups in aggregated requests. + * @param initialRequests - List of `client`-provided handshake {@link SubscribeRequest|subscribe} requests. + * @param continuationRequests - List of `client`-provided subscription loop continuation + * {@link SubscribeRequest|subscribe} requests. + * @param removedRequests - List of `client`-provided {@link SubscribeRequest|subscribe} requests that should be + * removed from the state. + */ + handleSubscriptionStateChange(changes, stateChanges, initialRequests, continuationRequests, removedRequests) { + var _a, _b, _c, _d; + // Retrieve list of active (not completed or canceled) `service`-provided requests. + const serviceRequests = this.serviceRequests.filter((request) => !request.completed && !request.canceled); + const requestsWithInitialResponse = []; + const newContinuationServiceRequests = []; + const newInitialServiceRequests = []; + const cancelledServiceRequests = []; + let serviceLeaveRequest; + // Identify token override for initial requests. + let timetokenOverrideRefreshTimestamp; + let decidedTimetokenRegionOverride; + let decidedTimetokenOverride; + const cancelServiceRequest = (serviceRequest) => { + cancelledServiceRequests.push(serviceRequest); + const rest = serviceRequest + .dependentRequests() + .filter((dependantRequest) => !removedRequests.includes(dependantRequest)); + if (rest.length === 0) return; - const body = new TextEncoder().encode(`{"t":{"t":"${scheduledRequest.timetoken}","r":${(_a = scheduledRequest.region) !== null && _a !== void 0 ? _a : '0'}},"m":[]}`); - const headers = new Headers({ - 'Content-Type': 'text/javascript; charset="UTF-8"', - 'Content-Length': `${body.length}`, + rest.forEach((dependantRequest) => (dependantRequest.serviceRequest = undefined)); + (serviceRequest.isInitialSubscribe ? initialRequests : continuationRequests).push(...rest); + }; + // -------------------------------------------------- + // Identify ongoing `service`-provided requests which should be canceled because channels/channel groups has been + // added/removed. + // + if (stateChanges) { + if (stateChanges.channels.added || stateChanges.channelGroups.added) { + for (const serviceRequest of serviceRequests) + cancelServiceRequest(serviceRequest); + serviceRequests.length = 0; + } + else if (stateChanges.channels.removed || stateChanges.channelGroups.removed) { + const channelGroups = (_a = stateChanges.channelGroups.removed) !== null && _a !== void 0 ? _a : []; + const channels = (_b = stateChanges.channels.removed) !== null && _b !== void 0 ? _b : []; + for (let serviceRequestIdx = serviceRequests.length - 1; serviceRequestIdx >= 0; serviceRequestIdx--) { + const serviceRequest = serviceRequests[serviceRequestIdx]; + if (!serviceRequest.hasAnyChannelsOrGroups(channels, channelGroups)) + continue; + cancelServiceRequest(serviceRequest); + serviceRequests.splice(serviceRequestIdx, 1); + } + } + } + continuationRequests = this.squashSameClientRequests(continuationRequests); + initialRequests = this.squashSameClientRequests(initialRequests); + // -------------------------------------------------- + // Searching for optimal timetoken, which should be used for `service`-provided request (will override response with + // new timetoken to make it possible to aggregate on next subscription loop with already ongoing `service`-provided + // long-poll request). + // + (initialRequests.length ? continuationRequests : []).forEach((request) => { + let shouldSetPreviousTimetoken = !decidedTimetokenOverride; + if (!shouldSetPreviousTimetoken && request.timetoken !== '0') { + if (decidedTimetokenOverride === '0') + shouldSetPreviousTimetoken = true; + else if (request.timetoken < decidedTimetokenOverride) + shouldSetPreviousTimetoken = request.creationDate > timetokenOverrideRefreshTimestamp; + } + if (shouldSetPreviousTimetoken) { + timetokenOverrideRefreshTimestamp = request.creationDate; + decidedTimetokenOverride = request.timetoken; + decidedTimetokenRegionOverride = request.region; + } + }); + // -------------------------------------------------- + // Try to attach `initial` and `continuation` `client`-provided requests to ongoing `service`-provided requests. + // + // Separate continuation requests by next subscription loop timetoken. + // This prevents possibility that some subscribe requests will be aggregated into one with much newer timetoken and + // miss messages as result. + const continuationByTimetoken = {}; + continuationRequests.forEach((request) => { + if (!continuationByTimetoken[request.timetoken]) + continuationByTimetoken[request.timetoken] = [request]; + else + continuationByTimetoken[request.timetoken].push(request); + }); + this.attachToServiceRequest(serviceRequests, initialRequests); + for (let initialRequestIdx = initialRequests.length - 1; initialRequestIdx >= 0; initialRequestIdx--) { + const request = initialRequests[initialRequestIdx]; + serviceRequests.forEach((serviceRequest) => { + if (!request.isSubsetOf(serviceRequest) || serviceRequest.isInitialSubscribe) + return; + const { region, timetoken } = serviceRequest; + requestsWithInitialResponse.push({ request, timetoken, region: region }); + initialRequests.splice(initialRequestIdx, 1); }); - const response = new Response(body, { status: 200, headers }); - const result = requestProcessingSuccess([response, body]); - result.url = `${event.request.origin}${event.request.path}`; - result.clientIdentifier = event.clientIdentifier; - result.identifier = event.request.identifier; - publishClientEvent(client, result); } - return; - } - if (event.request.cancellable) - abortControllers.set(requestOrId.identifier, new AbortController()); - const scheduledRequest = serviceRequests[requestOrId.identifier]; - const { timetokenOverride, regionOverride } = scheduledRequest; - const expectingInitialSubscribeResponse = scheduledRequest.timetoken === '0'; - consoleLog(`'${Object.keys(serviceRequests).length}' subscription request currently active.`); - // Notify about request processing start. - for (const client of clientsForRequest(requestOrId.identifier)) - consoleLog({ messageType: 'network-request', message: requestOrId }, client); - sendRequest(requestOrId, () => clientsForRequest(requestOrId.identifier), (clients, fetchRequest, response) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, response, event.request); - // Clean up scheduled request and client references to it. - markRequestCompleted(clients, requestOrId.identifier); - }, (clients, fetchRequest, error) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, null, event.request, requestProcessingError(error)); - // Clean up scheduled request and client references to it. - markRequestCompleted(clients, requestOrId.identifier); - }, (response) => { - let serverResponse = response; - if (expectingInitialSubscribeResponse && timetokenOverride && timetokenOverride !== '0') - serverResponse = patchInitialSubscribeResponse(serverResponse, timetokenOverride, regionOverride); - return serverResponse; - }); - }; - const patchInitialSubscribeResponse = (serverResponse, timetoken, region) => { - if (timetoken === undefined || timetoken === '0' || serverResponse[0].status >= 400) { - return serverResponse; - } - let json; - const response = serverResponse[0]; - let decidedResponse = response; - let body = serverResponse[1]; - try { - json = JSON.parse(new TextDecoder().decode(body)); - } - catch (error) { - consoleLog(`Subscribe response parse error: ${error}`); - return serverResponse; - } - // Replace server-provided timetoken. - json.t.t = timetoken; - if (region) - json.t.r = parseInt(region, 10); - try { - body = new TextEncoder().encode(JSON.stringify(json)).buffer; - if (body.byteLength) { - const headers = new Headers(response.headers); - headers.set('Content-Length', `${body.byteLength}`); - // Create a new response with the original response options and modified headers - decidedResponse = new Response(body, { - status: response.status, - statusText: response.statusText, - headers: headers, + if (initialRequests.length) { + let aggregationRequests; + if (continuationRequests.length) { + decidedTimetokenOverride = Object.keys(continuationByTimetoken).sort().pop(); + const requests = continuationByTimetoken[decidedTimetokenOverride]; + decidedTimetokenRegionOverride = requests[0].region; + delete continuationByTimetoken[decidedTimetokenOverride]; + requests.forEach((request) => request.resetToInitialRequest()); + aggregationRequests = [...initialRequests, ...requests]; + } + else + aggregationRequests = initialRequests; + // Create handshake service request (if possible) + this.createAggregatedRequest(aggregationRequests, newInitialServiceRequests, decidedTimetokenOverride, decidedTimetokenRegionOverride); + } + // Handle case when `initial` requests are supersets of continuation requests. + Object.values(continuationByTimetoken).forEach((requestsByTimetoken) => { + // Set `initial` `service`-provided requests as service requests for those continuation `client`-provided requests + // that are a _subset_ of them. + this.attachToServiceRequest(newInitialServiceRequests, requestsByTimetoken); + // Set `ongoing` `service`-provided requests as service requests for those continuation `client`-provided requests + // that are a _subset_ of them (if any still available). + this.attachToServiceRequest(serviceRequests, requestsByTimetoken); + // Create continuation `service`-provided request (if possible). + this.createAggregatedRequest(requestsByTimetoken, newContinuationServiceRequests); + }); + // -------------------------------------------------- + // Identify channels and groups for which presence `leave` should be generated. + // + const channelGroupsForLeave = new Set(); + const channelsForLeave = new Set(); + if (stateChanges && + removedRequests.length && + (stateChanges.channels.removed || stateChanges.channelGroups.removed)) { + const channelGroups = (_c = stateChanges.channelGroups.removed) !== null && _c !== void 0 ? _c : []; + const channels = (_d = stateChanges.channels.removed) !== null && _d !== void 0 ? _d : []; + const client = removedRequests[0].client; + changes + .filter((change) => change.remove && change.sendLeave) + .forEach((change) => { + const { channels: requestChannels, channelGroups: requestChannelsGroups } = change.request; + channelGroups.forEach((group) => requestChannelsGroups.includes(group) && channelGroupsForLeave.add(group)); + channels.forEach((channel) => requestChannels.includes(channel) && channelsForLeave.add(channel)); }); + serviceLeaveRequest = leaveRequest(client, [...channelsForLeave], [...channelGroupsForLeave]); + } + if (requestsWithInitialResponse.length || + newInitialServiceRequests.length || + newContinuationServiceRequests.length || + cancelledServiceRequests.length || + serviceLeaveRequest) { + this.dispatchEvent(new SubscriptionStateChangeEvent(requestsWithInitialResponse, [...newInitialServiceRequests, ...newContinuationServiceRequests], cancelledServiceRequests, serviceLeaveRequest)); + } + } + /** + * Refresh the internal subscription's state. + */ + refreshInternalState() { + const channelGroups = new Set(); + const channels = new Set(); + // Aggregate channels and groups from active requests. + Object.entries(this.requests).forEach(([clientIdentifier, request]) => { + var _a; + var _b; + const clientState = ((_a = (_b = this.clientsState)[clientIdentifier]) !== null && _a !== void 0 ? _a : (_b[clientIdentifier] = { channels: new Set(), channelGroups: new Set() })); + request.channelGroups.forEach(clientState.channelGroups.add, clientState.channelGroups); + request.channels.forEach(clientState.channels.add, clientState.channels); + request.channelGroups.forEach(channelGroups.add, channelGroups); + request.channels.forEach(channels.add, channels); + }); + const changes = this.subscriptionStateChanges(channels, channelGroups); + // Update state information. + this.channelGroups = channelGroups; + this.channels = channels; + // Identify most suitable access token. + const sortedTokens = Object.values(this.requests) + .flat() + .filter((request) => !!request.accessToken) + .map((request) => request.accessToken) + .sort(AccessToken.compare); + if (sortedTokens && sortedTokens.length > 0) + this.accessToken = sortedTokens.pop(); + return changes; + } + // endregion + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + addListenersForRequestEvents(request) { + const abortController = (this.requestListenersAbort[request.identifier] = new AbortController()); + const cleanUpCallback = () => { + this.removeListenersFromRequestEvents(request); + if (!request.isServiceRequest) { + if (this.requests[request.client.identifier]) { + this.lastCompletedRequest[request.client.identifier] = request; + delete this.requests[request.client.identifier]; + const clientIdx = this.clientsForInvalidation.indexOf(request.client.identifier); + if (clientIdx > 0) { + this.clientsForInvalidation.splice(clientIdx, 1); + delete this.lastCompletedRequest[request.client.identifier]; + delete this.clientsState[request.client.identifier]; + // Check whether subscription state for all registered clients has been removed or not. + if (!Object.keys(this.clientsState).length) + this.dispatchEvent(new SubscriptionStateInvalidateEvent()); + } + } + return; + } + const requestIdx = this.serviceRequests.indexOf(request); + if (requestIdx >= 0) + this.serviceRequests.splice(requestIdx, 1); + }; + request.addEventListener(PubNubSharedWorkerRequestEvents.Success, cleanUpCallback, { + signal: abortController.signal, + once: true, + }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Error, cleanUpCallback, { + signal: abortController.signal, + once: true, + }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Canceled, cleanUpCallback, { + signal: abortController.signal, + once: true, + }); + } + removeListenersFromRequestEvents(request) { + if (!this.requestListenersAbort[request.request.identifier]) + return; + this.requestListenersAbort[request.request.identifier].abort(); + delete this.requestListenersAbort[request.request.identifier]; + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Identify changes to the channels and groups. + * + * @param channels - Set with channels which has been left after client requests list has been changed. + * @param channelGroups - Set with channel groups which has been left after client requests list has been changed. + * @returns Objects with names of channels and groups which has been added and removed from the current subscription + * state. + */ + subscriptionStateChanges(channels, channelGroups) { + const stateIsEmpty = this.channelGroups.size === 0 && this.channels.size === 0; + const changes = { channelGroups: {}, channels: {} }; + const removedChannelGroups = []; + const addedChannelGroups = []; + const removedChannels = []; + const addedChannels = []; + for (const group of channelGroups) + if (!this.channelGroups.has(group)) + addedChannelGroups.push(group); + for (const channel of channels) + if (!this.channels.has(channel)) + addedChannels.push(channel); + if (!stateIsEmpty) { + for (const group of this.channelGroups) + if (!channelGroups.has(group)) + removedChannelGroups.push(group); + for (const channel of this.channels) + if (!channels.has(channel)) + removedChannels.push(channel); } + if (addedChannels.length || removedChannels.length) { + changes.channels = Object.assign(Object.assign({}, (addedChannels.length ? { added: addedChannels } : {})), (removedChannels.length ? { removed: removedChannels } : {})); + } + if (addedChannelGroups.length || removedChannelGroups.length) { + changes.channelGroups = Object.assign(Object.assign({}, (addedChannelGroups.length ? { added: addedChannelGroups } : {})), (removedChannelGroups.length ? { removed: removedChannelGroups } : {})); + } + return Object.keys(changes.channelGroups).length === 0 && Object.keys(changes.channels).length === 0 + ? undefined + : changes; + } + /** + * Squash list of provided requests to represent latest request for each client. + * + * @param requests - List with potentially repetitive or multiple {@link SubscribeRequest|subscribe} requests for the + * same {@link PubNubClient|PubNub} client. + * @returns List of latest {@link SubscribeRequest|subscribe} requests for corresponding {@link PubNubClient|PubNub} + * clients. + */ + squashSameClientRequests(requests) { + if (!requests.length || requests.length === 1) + return requests; + // Sort requests in order in which they have been created. + const sortedRequests = requests.sort((lhr, rhr) => lhr.creationDate - rhr.creationDate); + return Object.values(sortedRequests.reduce((acc, value) => { + acc[value.client.identifier] = value; + return acc; + }, {})); + } + /** + * Attach `client`-provided requests to the compatible ongoing `service`-provided requests. + * + * @param serviceRequests - List of ongoing `service`-provided subscribe requests. + * @param requests - List of `client`-provided requests that should try to hook for service response using existing + * ongoing `service`-provided requests. + */ + attachToServiceRequest(serviceRequests, requests) { + if (!serviceRequests.length || !requests.length) + return; + [...requests].forEach((request) => { + for (const serviceRequest of serviceRequests) { + // Check whether continuation request is actually a subset of the `service`-provided request or not. + // Note: Second condition handled in the function which calls `attachToServiceRequest`. + if (!!request.serviceRequest || + !request.isSubsetOf(serviceRequest) || + (request.isInitialSubscribe && !serviceRequest.isInitialSubscribe)) + continue; + // Attach to the matching `service`-provided request. + request.serviceRequest = serviceRequest; + // There is no need to aggregate attached request. + const requestIdx = requests.indexOf(request); + requests.splice(requestIdx, 1); + break; + } + }); } - catch (error) { - consoleLog(`Subscribe serialization error: ${error}`); - return serverResponse; + /** + * Create aggregated `service`-provided {@link SubscribeRequest|subscribe} request. + * + * @param requests - List of `client`-provided {@link SubscribeRequest|subscribe} requests which should be sent with + * as single `service`-provided request. + * @param serviceRequests - List with created `service`-provided {@link SubscribeRequest|subscribe} requests. + * @param timetokenOverride - Timetoken that should replace the initial response timetoken. + * @param regionOverride - Timetoken region that should replace the initial response timetoken region. + */ + createAggregatedRequest(requests, serviceRequests, timetokenOverride, regionOverride) { + if (requests.length === 0) + return; + const serviceRequest = SubscribeRequest.fromRequests(requests, this.accessToken, timetokenOverride, regionOverride); + this.addListenersForRequestEvents(serviceRequest); + requests.forEach((request) => (request.serviceRequest = serviceRequest)); + this.serviceRequests.push(serviceRequest); + serviceRequests.push(serviceRequest); } - return body.byteLength > 0 ? [decidedResponse, body] : serverResponse; + } + + /****************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ + + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; + /** - * Handle client heartbeat request. + * SharedWorker's requests manager. * - * @param event - Heartbeat event details. - * @param [actualRequest] - Whether handling actual request from the core-part of the client and not backup heartbeat in - * the `SharedWorker`. - * @param [outOfOrder] - Whether handling request which is sent on irregular basis (setting update). + * Manager responsible for storing client-provided request for the time while enqueue / dequeue service request which + * is actually sent to the PubNub service. */ - const handleHeartbeatRequestEvent = (event, actualRequest = true, outOfOrder = false) => { - var _a; - const client = pubNubClients[event.clientIdentifier]; - const request = heartbeatTransportRequestFromEvent(event, actualRequest, outOfOrder); - if (!client) - return; - const heartbeatRequestKey = `${client.userId}_${(_a = clientAggregateAuthKey(client)) !== null && _a !== void 0 ? _a : ''}`; - const hbRequestsBySubscriptionKey = serviceHeartbeatRequests[client.subscriptionKey]; - const hbRequests = (hbRequestsBySubscriptionKey !== null && hbRequestsBySubscriptionKey !== void 0 ? hbRequestsBySubscriptionKey : {})[heartbeatRequestKey]; - if (!request) { - let message = `Previous heartbeat request has been sent less than ${client.heartbeatInterval} seconds ago. Skipping...`; - if (!client.heartbeat || (client.heartbeat.channels.length === 0 && client.heartbeat.channelGroups.length === 0)) - message = `${client.clientIdentifier} doesn't have subscriptions to non-presence channels. Skipping...`; - consoleLog(message, client); - let response; - let body; - // Pulling out previous response. - if (hbRequests && hbRequests.response) - [response, body] = hbRequests.response; - if (!response) { - body = new TextEncoder().encode('{ "status": 200, "message": "OK", "service": "Presence" }').buffer; - const headers = new Headers({ - 'Content-Type': 'text/javascript; charset="UTF-8"', - 'Content-Length': `${body.byteLength}`, + class RequestsManager extends EventTarget { + // -------------------------------------------------------- + // ------------------ Request processing ------------------ + // -------------------------------------------------------- + // region Request processing + /** + * Begin service request processing. + * + * @param request - Reference to the service request which should be sent. + * @param success - Request success completion handler. + * @param failure - Request failure handler. + * @param responsePreprocess - Raw response pre-processing function which is used before calling handling callbacks. + */ + sendRequest(request, success, failure, responsePreprocess) { + request.handleProcessingStarted(); + if (request.cancellable) + request.fetchAbortController = new AbortController(); + const fetchRequest = request.asFetchRequest; + (() => __awaiter(this, void 0, void 0, function* () { + Promise.race([ + fetch(fetchRequest, Object.assign(Object.assign({}, (request.fetchAbortController ? { signal: request.fetchAbortController.signal } : {})), { keepalive: true })), + request.requestTimeoutTimer(), + ]) + .then((response) => response.arrayBuffer().then((buffer) => [response, buffer])) + .then((response) => (responsePreprocess ? responsePreprocess(response) : response)) + .then((response) => { + if (response[0].status >= 400) + failure(fetchRequest, this.requestProcessingError(undefined, response)); + else + success(fetchRequest, this.requestProcessingSuccess(response)); + }) + .catch((error) => { + let fetchError = error; + if (typeof error === 'string') { + const errorMessage = error.toLowerCase(); + fetchError = new Error(error); + if (!errorMessage.includes('timeout') && errorMessage.includes('cancel')) + fetchError.name = 'AbortError'; + } + request.stopRequestTimeoutTimer(); + failure(fetchRequest, this.requestProcessingError(fetchError)); }); - response = new Response(body, { status: 200, headers }); + }))(); + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Create processing success event from service response. + * + * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each + * specific PubNub client state. + * + * @param res - Service response for used REST API endpoint along with response body. + * + * @returns Request processing success event object. + */ + requestProcessingSuccess(res) { + var _a; + const [response, body] = res; + const responseBody = body.byteLength > 0 ? body : undefined; + const contentLength = parseInt((_a = response.headers.get('Content-Length')) !== null && _a !== void 0 ? _a : '0', 10); + const contentType = response.headers.get('Content-Type'); + const headers = {}; + // Copy Headers object content into plain Record. + response.headers.forEach((value, key) => (headers[key.toLowerCase()] = value.toLowerCase())); + return { + type: 'request-process-success', + clientIdentifier: '', + identifier: '', + url: '', + response: { contentLength, contentType, headers, status: response.status, body: responseBody }, + }; + } + /** + * Create processing error event from service response. + * + * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each + * specific PubNub client state. + * + * @param [error] - Client-side request processing error (for example network issues). + * @param [response] - Service error response (for example permissions error or malformed + * payload) along with service body. + * @returns Request processing error event object. + */ + requestProcessingError(error, response) { + // Use service response as error information source. + if (response) + return Object.assign(Object.assign({}, this.requestProcessingSuccess(response)), { type: 'request-process-error' }); + let type = 'NETWORK_ISSUE'; + let message = 'Unknown error'; + let name = 'Error'; + if (error && error instanceof Error) { + message = error.message; + name = error.name; } - const result = requestProcessingSuccess([response, body]); - result.url = `${event.request.origin}${event.request.path}`; - result.clientIdentifier = event.clientIdentifier; - result.identifier = event.request.identifier; - publishClientEvent(client, result); - return; - } - consoleLog(`Started heartbeat request.`, client); - // Notify about request processing start. - for (const client of clientsForSendHeartbeatRequestEvent(event)) - consoleLog({ messageType: 'network-request', message: request }, client); - sendRequest(request, () => [client], (clients, fetchRequest, response) => { - if (hbRequests) - hbRequests.response = response; - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, response, event.request); - // Stop heartbeat timer on client error status codes. - if (response[0].status >= 400 && response[0].status < 500) - stopHeartbeatTimer(client); - }, (clients, fetchRequest, error) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, null, event.request, requestProcessingError(error)); - }); - // Start "backup" heartbeat timer. - if (!outOfOrder) - startHeartbeatTimer(client); - }; - /** - * Handle client request to leave request. - * - * @param data - Leave event details. - * @param [invalidatedClient] - Specific client to handle leave request. - * @param [invalidatedClientServiceRequestId] - Identifier of the service request ID for which the invalidated - * client waited for a subscribe response. - */ - const handleSendLeaveRequestEvent = (data, invalidatedClient, invalidatedClientServiceRequestId) => { - var _a, _b; - var _c; - const client = invalidatedClient !== null && invalidatedClient !== void 0 ? invalidatedClient : pubNubClients[data.clientIdentifier]; - const request = leaveTransportRequestFromEvent(data, invalidatedClient); - if (!client) - return; - // Clean up client subscription information if there is no more channels / groups to use. - const { subscription, heartbeat } = client; - const serviceRequestId = invalidatedClientServiceRequestId !== null && invalidatedClientServiceRequestId !== void 0 ? invalidatedClientServiceRequestId : subscription === null || subscription === void 0 ? void 0 : subscription.serviceRequestId; - if (subscription && subscription.channels.length === 0 && subscription.channelGroups.length === 0) { - subscription.channelGroupQuery = ''; - subscription.path = ''; - subscription.previousTimetoken = '0'; - subscription.refreshTimestamp = Date.now(); - subscription.timetoken = '0'; - delete subscription.region; - delete subscription.serviceRequestId; - delete subscription.request; - } - if (serviceHeartbeatRequests[client.subscriptionKey]) { - if (heartbeat && heartbeat.channels.length === 0 && heartbeat.channelGroups.length === 0) { - const hbRequestsBySubscriptionKey = ((_a = serviceHeartbeatRequests[_c = client.subscriptionKey]) !== null && _a !== void 0 ? _a : (serviceHeartbeatRequests[_c] = {})); - const heartbeatRequestKey = `${client.userId}_${(_b = clientAggregateAuthKey(client)) !== null && _b !== void 0 ? _b : ''}`; - if (hbRequestsBySubscriptionKey[heartbeatRequestKey] && - hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier === client.clientIdentifier) - delete hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier; - delete heartbeat.heartbeatEvent; - stopHeartbeatTimer(client); + const errorMessage = message.toLowerCase(); + if (errorMessage.includes('timeout')) + type = 'TIMEOUT'; + else if (name === 'AbortError' || errorMessage.includes('aborted') || errorMessage.includes('cancel')) { + message = 'Request aborted'; + type = 'ABORTED'; } + return { + type: 'request-process-error', + clientIdentifier: '', + identifier: '', + url: '', + error: { name, type, message }, + }; } - if (!request) { - const body = new TextEncoder().encode('{"status": 200, "action": "leave", "message": "OK", "service":"Presence"}'); - const headers = new Headers({ - 'Content-Type': 'text/javascript; charset="UTF-8"', - 'Content-Length': `${body.length}`, - }); - const response = new Response(body, { status: 200, headers }); - const result = requestProcessingSuccess([response, body]); - result.url = `${data.request.origin}${data.request.path}`; - result.clientIdentifier = data.clientIdentifier; - result.identifier = data.request.identifier; - publishClientEvent(client, result); - return; - } - consoleLog(`Started leave request.`, client); - // Notify about request processing start. - for (const client of clientsForSendLeaveRequestEvent(data, invalidatedClient)) - consoleLog({ messageType: 'network-request', message: request }, client); - sendRequest(request, () => [client], (clients, fetchRequest, response) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, response, data.request); - }, (clients, fetchRequest, error) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, null, data.request, requestProcessingError(error)); - }); - // Check whether there were active subscription with channels from this client or not. - if (serviceRequestId === undefined) - return; - // Update ongoing clients - const clients = clientsForRequest(serviceRequestId); - clients.forEach((client) => { - if (client && client.subscription) - delete client.subscription.serviceRequestId; - }); - cancelRequest(serviceRequestId); - restartSubscribeRequestForClients(clients); - }; + /** + * Percent-encode input string. + * + * **Note:** Encode content in accordance of the `PubNub` service requirements. + * + * @param input - Source string or number for encoding. + * @returns Percent-encoded string. + */ + encodeString(input) { + return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); + } + } + /** - * Handle cancel request event. - * - * Try cancel request if there is no other observers. + * Aggregation timer timeout. * - * @param event - Request cancellation event details. + * Timeout used by the timer to postpone enqueued `subscribe` requests processing and let other clients for the same + * subscribe key send next subscribe loop request (to make aggregation more efficient). */ - const handleCancelRequestEvent = (event) => { - const client = pubNubClients[event.clientIdentifier]; - if (!client || !client.subscription) - return; - const serviceRequestId = client.subscription.serviceRequestId; - if (!client || !serviceRequestId) - return; - // Unset awaited requests. - delete client.subscription.serviceRequestId; - if (client.subscription.request && client.subscription.request.identifier === event.identifier) { - delete client.subscription.request; - } - cancelRequest(serviceRequestId); - }; - // endregion - // -------------------------------------------------------- - // --------------------- Subscription --------------------- - // -------------------------------------------------------- - // region Subscription + const aggregationTimeout = 50; /** - * Try restart subscribe request for the list of clients. - * - * Subscribe restart will use previous timetoken information to schedule new subscription loop. + * Sent {@link SubscribeRequest|subscribe} requests manager. * - * **Note:** This function mimics behaviour when SharedWorker receives request from PubNub SDK. - * - * @param clients List of PubNub client states for which new aggregated request should be sent. + * Manager responsible for requests enqueue for batch processing and aggregated `service`-provided requests scheduling. */ - const restartSubscribeRequestForClients = (clients) => { - let clientWithRequest; - let request; - for (const client of clients) { - if (client.subscription && client.subscription.request) { - request = client.subscription.request; - clientWithRequest = client; - break; + class SubscribeRequestsManager extends RequestsManager { + // endregion + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + /** + * Create a {@link SubscribeRequest|subscribe} requests manager. + * + * @param clientsManager - Reference to the {@link PubNubClient|PubNub} clients manager as an events source for new + * clients for which {@link SubscribeRequest|subscribe} request sending events should be listened. + */ + constructor(clientsManager) { + super(); + this.clientsManager = clientsManager; + /** + * Map of change aggregation identifiers to the requests which should be processed at once. + * + * `requests` key contains a map of {@link PubNubClient|PubNub} client identifiers to requests created by it (usually + * there is only one at a time). + */ + this.requestsChangeAggregationQueue = {}; + /** + * Map of client identifiers to {@link AbortController} instances which is used to detach added listeners when + * {@link PubNubClient|PubNub} client unregisters. + */ + this.clientAbortControllers = {}; + /** + * Map of unique user identifier (composed from multiple request object properties) to the aggregated subscription + * {@link SubscriptionState|state}. + */ + this.subscriptionStates = {}; + this.addEventListenersForClientsManager(clientsManager); + } + // endregion + // -------------------------------------------------------- + // ----------------- Changes aggregation ------------------ + // -------------------------------------------------------- + // region Changes aggregation + /** + * Retrieve {@link SubscribeRequest|requests} changes aggregation queue for specific {@link PubNubClient|PubNub} + * client. + * + * @param client - Reference to {@link PubNubClient|PubNub} client for which {@link SubscribeRequest|subscribe} + * requests queue should be retrieved. + * @returns Tuple with aggregation key and aggregated changes of client's {@link SubscribeRequest|subscribe} requests + * that are enqueued for aggregation/removal. + */ + requestsChangeAggregationQueueForClient(client) { + for (const aggregationKey of Object.keys(this.requestsChangeAggregationQueue)) { + const { changes } = this.requestsChangeAggregationQueue[aggregationKey]; + if (Array.from(changes).some((change) => change.clientIdentifier === client.identifier)) + return [aggregationKey, changes]; } + return [undefined, new Set()]; } - if (!request || !clientWithRequest) - return; - const sendRequest = { - type: 'send-request', - clientIdentifier: clientWithRequest.clientIdentifier, - subscriptionKey: clientWithRequest.subscriptionKey, - request, - }; - handleSendSubscribeRequestEventForClients([[clientWithRequest, sendRequest]], sendRequest); - }; - // endregion - // -------------------------------------------------------- - // ------------------------ Common ------------------------ - // -------------------------------------------------------- - // region Common - /** - * Process transport request. - * - * @param request - Transport request with required information for {@link Request} creation. - * @param getClients - Request completion PubNub client observers getter. - * @param success - Request success completion handler. - * @param failure - Request failure handler. - * @param responsePreProcess - Raw response pre-processing function which is used before calling handling callbacks. - */ - const sendRequest = (request, getClients, success, failure, responsePreProcess) => { - (() => __awaiter(void 0, void 0, void 0, function* () { + /** + * Move {@link PubNubClient|PubNub} client to new subscription set. + * + * This function used when PubNub client changed its identity (`userId`) or auth (`access token`) and can't be + * aggregated with previous requests. + * + * **Note:** Previous `service`-provided `subscribe` request won't be canceled. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which should be moved to new state. + */ + moveClient(client) { + // Retrieve a list of client's requests that have been enqueued for further aggregation. + const [queueIdentifier, enqueuedChanges] = this.requestsChangeAggregationQueueForClient(client); + // Retrieve list of client's requests from active subscription state. + let state = this.subscriptionStateForClient(client); + const request = state === null || state === void 0 ? void 0 : state.requestForClient(client); + // Check whether PubNub client has any activity prior removal or not. + if (!state && !enqueuedChanges.size) + return; + // Make sure that client will be removed from its previous subscription state. + if (state) + state.invalidateClient(client); + // Requests aggregation identifier. + let identifier = request === null || request === void 0 ? void 0 : request.asIdentifier; + if (!identifier && enqueuedChanges.size) { + const [change] = enqueuedChanges; + identifier = change.request.asIdentifier; + } + if (!identifier) + return; + if (request) { + // Unset `service`-provided request because we can't receive a response with new `userId`. + request.serviceRequest = undefined; + state.processChanges([new SubscriptionStateChange(client.identifier, request, true, false, true)]); + state = this.subscriptionStateForIdentifier(identifier); + // Force state refresh (because we are putting into new subscription set). + request.resetToInitialRequest(); + state.processChanges([new SubscriptionStateChange(client.identifier, request, false, false)]); + } + // Check whether there is enqueued request changes which should be removed from previous queue and added to the new + // one. + if (!enqueuedChanges.size || !this.requestsChangeAggregationQueue[queueIdentifier]) + return; + // Start the changes aggregation timer if required (this also prepares the queue for `identifier`). + this.startAggregationTimer(identifier); + // Remove from previous aggregation queue. + const oldChangesQueue = this.requestsChangeAggregationQueue[queueIdentifier].changes; + SubscriptionStateChange.squashedChanges([...enqueuedChanges]) + .filter((change) => change.clientIdentifier !== client.identifier || change.remove) + .forEach(oldChangesQueue.delete, oldChangesQueue); + // Add previously scheduled for aggregation requests to the new subscription set target. + const { changes } = this.requestsChangeAggregationQueue[identifier]; + SubscriptionStateChange.squashedChanges([...enqueuedChanges]) + .filter((change) => change.clientIdentifier === client.identifier && + !change.request.completed && + change.request.canceled && + !change.remove) + .forEach(changes.add, changes); + } + /** + * Remove unregistered/disconnected {@link PubNubClient|PubNub} client from manager's {@link SubscriptionState|state}. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which should be removed from + * {@link SubscriptionState|state}. + * @param useChangeAggregation - Whether {@link PubNubClient|client} removal should be processed using an aggregation + * queue or change should be done on-the-fly by removing from both the aggregation queue and subscription state. + * @param sendLeave - Whether the {@link PubNubClient|client} should send a presence `leave` request for _free_ + * channels and groups or not. + * @param [invalidated=false] - Whether the {@link PubNubClient|PubNub} client and its request were removed as part of + * client invalidation (unregister) or not. + */ + removeClient(client, useChangeAggregation, sendLeave, invalidated = false) { var _a; - const fetchRequest = requestFromTransportRequest(request); - Promise.race([ - fetch(fetchRequest, { - signal: (_a = abortControllers.get(request.identifier)) === null || _a === void 0 ? void 0 : _a.signal, - keepalive: true, - }), - requestTimeoutTimer(request.identifier, request.timeout), - ]) - .then((response) => response.arrayBuffer().then((buffer) => [response, buffer])) - .then((response) => (responsePreProcess ? responsePreProcess(response) : response)) - .then((response) => { - const clients = getClients(); - if (clients.length === 0) - return; - success(clients, fetchRequest, response); - }) - .catch((error) => { - const clients = getClients(); - if (clients.length === 0) - return; - let fetchError = error; - if (typeof error === 'string') { - const errorMessage = error.toLowerCase(); - fetchError = new Error(error); - if (!errorMessage.includes('timeout') && errorMessage.includes('cancel')) - fetchError.name = 'AbortError'; + // Retrieve a list of client's requests that have been enqueued for further aggregation. + const [queueIdentifier, enqueuedChanges] = this.requestsChangeAggregationQueueForClient(client); + // Retrieve list of client's requests from active subscription state. + const state = this.subscriptionStateForClient(client); + const request = state === null || state === void 0 ? void 0 : state.requestForClient(client, invalidated); + // Check whether PubNub client has any activity prior removal or not. + if (!state && !enqueuedChanges.size) + return; + const identifier = (_a = (state && state.identifier)) !== null && _a !== void 0 ? _a : queueIdentifier; + // Remove the client's subscription requests from the active aggregation queue. + if (enqueuedChanges.size && this.requestsChangeAggregationQueue[identifier]) { + const { changes } = this.requestsChangeAggregationQueue[identifier]; + enqueuedChanges.forEach(changes.delete, changes); + this.stopAggregationTimerIfEmptyQueue(identifier); + } + if (!request) + return; + // Detach `client`-provided request to avoid unexpected response processing. + request.serviceRequest = undefined; + if (useChangeAggregation) { + // Start the changes aggregation timer if required (this also prepares the queue for `identifier`). + this.startAggregationTimer(identifier); + // Enqueue requests into the aggregated state change queue (delayed). + this.enqueueForAggregation(client, request, true, sendLeave, invalidated); + } + else if (state) + state.processChanges([new SubscriptionStateChange(client.identifier, request, true, sendLeave, invalidated)]); + } + /** + * Enqueue {@link SubscribeRequest|subscribe} requests for aggregation after small delay. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which created + * {@link SubscribeRequest|subscribe} request. + * @param enqueuedRequest - {@link SubscribeRequest|Subscribe} request which should be placed into the queue. + * @param removing - Whether requests enqueued for removal or not. + * @param sendLeave - Whether on remove it should leave "free" channels and groups or not. + * @param [clientInvalidate=false] - Whether the `subscription` state change was caused by the + * {@link PubNubClient|PubNub} client invalidation (unregister) or not. + */ + enqueueForAggregation(client, enqueuedRequest, removing, sendLeave, clientInvalidate = false) { + const identifier = enqueuedRequest.asIdentifier; + // Start the changes aggregation timer if required (this also prepares the queue for `identifier`). + this.startAggregationTimer(identifier); + // Enqueue requests into the aggregated state change queue. + const { changes } = this.requestsChangeAggregationQueue[identifier]; + changes.add(new SubscriptionStateChange(client.identifier, enqueuedRequest, removing, sendLeave, clientInvalidate)); + } + /** + * Start requests change aggregation timer. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + startAggregationTimer(identifier) { + if (this.requestsChangeAggregationQueue[identifier]) + return; + this.requestsChangeAggregationQueue[identifier] = { + timeout: setTimeout(() => this.handleDelayedAggregation(identifier), aggregationTimeout), + changes: new Set(), + }; + } + /** + * Stop request changes aggregation timer if there is no changes left in queue. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + stopAggregationTimerIfEmptyQueue(identifier) { + const queue = this.requestsChangeAggregationQueue[identifier]; + if (!queue) + return; + if (queue.changes.size === 0) { + if (queue.timeout) + clearTimeout(queue.timeout); + delete this.requestsChangeAggregationQueue[identifier]; + } + } + /** + * Handle delayed {@link SubscribeRequest|subscribe} requests aggregation. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + handleDelayedAggregation(identifier) { + if (!this.requestsChangeAggregationQueue[identifier]) + return; + const state = this.subscriptionStateForIdentifier(identifier); + // Squash self-excluding change entries. + const changes = [...this.requestsChangeAggregationQueue[identifier].changes]; + delete this.requestsChangeAggregationQueue[identifier]; + // Apply final changes to the subscription state. + state.processChanges(changes); + } + /** + * Retrieve existing or create new `subscription` {@link SubscriptionState|state} object for id. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + * @returns Existing or create new `subscription` {@link SubscriptionState|state} object for id. + */ + subscriptionStateForIdentifier(identifier) { + let state = this.subscriptionStates[identifier]; + if (!state) { + state = this.subscriptionStates[identifier] = new SubscriptionState(identifier); + // Make sure to receive updates from subscription state. + this.addListenerForSubscriptionStateEvents(state); + } + return state; + } + // endregion + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + /** + * Listen for {@link PubNubClient|PubNub} clients {@link PubNubClientsManager|manager} events that affect aggregated + * subscribe/heartbeat requests. + * + * @param clientsManager - Clients {@link PubNubClientsManager|manager} for which change in + * {@link PubNubClient|clients} should be tracked. + */ + addEventListenersForClientsManager(clientsManager) { + clientsManager.addEventListener(PubNubClientsManagerEvent.Registered, (evt) => { + const { client } = evt; + // Keep track of the client's listener abort controller. + const abortController = new AbortController(); + this.clientAbortControllers[client.identifier] = abortController; + client.addEventListener(PubNubClientEvent.IdentityChange, (event) => { + if (!(event instanceof PubNubClientIdentityChangeEvent)) + return; + // Make changes into state only if `userId` actually changed. + if (!!event.oldUserId !== !!event.newUserId || + (event.oldUserId && event.newUserId && event.newUserId !== event.oldUserId)) + this.moveClient(client); + }, { + signal: abortController.signal, + }); + client.addEventListener(PubNubClientEvent.AuthChange, (event) => { + var _a; + if (!(event instanceof PubNubClientAuthChangeEvent)) + return; + // Check whether the client should be moved to another state because of a permissions change or whether the + // same token with the same permissions should be used for the next requests. + if (!!event.oldAuth !== !!event.newAuth || + (event.oldAuth && event.newAuth && !event.oldAuth.equalTo(event.newAuth))) + this.moveClient(client); + else if (event.oldAuth && event.newAuth && event.oldAuth.equalTo(event.newAuth)) + (_a = this.subscriptionStateForClient(client)) === null || _a === void 0 ? void 0 : _a.updateClientAccessToken(event.newAuth); + }, { + signal: abortController.signal, + }); + client.addEventListener(PubNubClientEvent.SendSubscribeRequest, (event) => { + if (!(event instanceof PubNubClientSendSubscribeEvent)) + return; + this.enqueueForAggregation(event.client, event.request, false, false); + }, { signal: abortController.signal }); + client.addEventListener(PubNubClientEvent.CancelSubscribeRequest, (event) => { + if (!(event instanceof PubNubClientCancelSubscribeEvent)) + return; + this.enqueueForAggregation(event.client, event.request, true, false); + }, { signal: abortController.signal }); + client.addEventListener(PubNubClientEvent.SendLeaveRequest, (event) => { + if (!(event instanceof PubNubClientSendLeaveEvent)) + return; + const request = this.patchedLeaveRequest(event.request); + if (!request) + return; + this.sendRequest(request, (fetchRequest, response) => request.handleProcessingSuccess(fetchRequest, response), (fetchRequest, errorResponse) => request.handleProcessingError(fetchRequest, errorResponse)); + }, { signal: abortController.signal }); + }); + clientsManager.addEventListener(PubNubClientsManagerEvent.Unregistered, (event) => { + const { client, withLeave } = event; + // Remove all listeners added for the client. + const abortController = this.clientAbortControllers[client.identifier]; + delete this.clientAbortControllers[client.identifier]; + if (abortController) + abortController.abort(); + // Update manager's state. + this.removeClient(client, false, withLeave, true); + }); + } + /** + * Listen for subscription {@link SubscriptionState|state} events. + * + * @param state - Reference to the subscription object for which listeners should be added. + */ + addListenerForSubscriptionStateEvents(state) { + const abortController = new AbortController(); + state.addEventListener(SubscriptionStateEvent.Changed, (event) => { + const { requestsWithInitialResponse, canceledRequests, newRequests, leaveRequest } = event; + // Cancel outdated ongoing `service`-provided subscribe requests. + canceledRequests.forEach((request) => request.cancel('Cancel request')); + // Schedule new `service`-provided subscribe requests processing. + newRequests.forEach((request) => { + this.sendRequest(request, (fetchRequest, response) => request.handleProcessingSuccess(fetchRequest, response), (fetchRequest, error) => request.handleProcessingError(fetchRequest, error), request.isInitialSubscribe && request.timetokenOverride !== '0' + ? (response) => this.patchInitialSubscribeResponse(response, request.timetokenOverride, request.timetokenRegionOverride) + : undefined); + }); + requestsWithInitialResponse.forEach((response) => { + const { request, timetoken, region } = response; + request.handleProcessingStarted(); + this.makeResponseOnHandshakeRequest(request, timetoken, region); + }); + if (leaveRequest) { + this.sendRequest(leaveRequest, (fetchRequest, response) => leaveRequest.handleProcessingSuccess(fetchRequest, response), (fetchRequest, error) => leaveRequest.handleProcessingError(fetchRequest, error)); } - failure(clients, fetchRequest, fetchError); + }, { signal: abortController.signal }); + state.addEventListener(SubscriptionStateEvent.Invalidated, () => { + delete this.subscriptionStates[state.identifier]; + abortController.abort(); + }, { + signal: abortController.signal, + once: true, }); - }))(); - }; - /** - * Cancel (abort) service request by ID. - * - * @param requestId - Unique identifier of request which should be cancelled. - */ - const cancelRequest = (requestId) => { - if (clientsForRequest(requestId).length === 0) { - const controller = abortControllers.get(requestId); - abortControllers.delete(requestId); - // Clean up scheduled requests. - delete serviceRequests[requestId]; - // Abort request if possible. - if (controller) - controller.abort('Cancel request'); } - }; + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Retrieve subscription {@link SubscriptionState|state} with which specific client is working. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client for which subscription + * {@link SubscriptionState|state} should be found. + * @returns Reference to the subscription {@link SubscriptionState|state} if the client has ongoing + * {@link SubscribeRequest|requests}. + */ + subscriptionStateForClient(client) { + return Object.values(this.subscriptionStates).find((state) => state.hasStateForClient(client)); + } + /** + * Create `service`-provided `leave` request from a `client`-provided {@link LeaveRequest|request} with channels and + * groups for removal. + * + * @param request - Original `client`-provided `leave` {@link LeaveRequest|request}. + * @returns `service`-provided `leave` request. + */ + patchedLeaveRequest(request) { + const subscriptionState = this.subscriptionStateForClient(request.client); + // Something is wrong. Client doesn't have any active subscriptions. + if (!subscriptionState) { + request.cancel(); + return; + } + // Filter list from channels and groups which is still in use. + const clientStateForLeave = subscriptionState.uniqueStateForClient(request.client, request.channels, request.channelGroups); + const serviceRequest = leaveRequest(request.client, clientStateForLeave.channels, clientStateForLeave.channelGroups); + if (serviceRequest) + request.serviceRequest = serviceRequest; + return serviceRequest; + } + /** + * Return "response" from PubNub service with initial timetoken data. + * + * @param request - Client-provided handshake/initial request for which response should be provided. + * @param timetoken - Timetoken from currently active service request. + * @param region - Region from currently active service request. + */ + makeResponseOnHandshakeRequest(request, timetoken, region) { + const body = new TextEncoder().encode(`{"t":{"t":"${timetoken}","r":${region !== null && region !== void 0 ? region : '0'}},"m":[]}`); + request.handleProcessingSuccess(request.asFetchRequest, { + type: 'request-process-success', + clientIdentifier: '', + identifier: '', + url: '', + response: { + contentType: 'text/javascript; charset="UTF-8"', + contentLength: body.length, + headers: { 'content-type': 'text/javascript; charset="UTF-8"', 'content-length': `${body.length}` }, + status: 200, + body, + }, + }); + } + /** + * Patch `service`-provided subscribe response with new timetoken and region. + * + * @param serverResponse - Original service response for patching. + * @param timetoken - Original timetoken override value. + * @param region - Original timetoken region override value. + * @returns Patched subscribe REST API response. + */ + patchInitialSubscribeResponse(serverResponse, timetoken, region) { + if (timetoken === undefined || timetoken === '0' || serverResponse[0].status >= 400) + return serverResponse; + let json; + const response = serverResponse[0]; + let decidedResponse = response; + let body = serverResponse[1]; + try { + json = JSON.parse(SubscribeRequestsManager.textDecoder.decode(body)); + } + catch (error) { + console.error(`Subscribe response parse error: ${error}`); + return serverResponse; + } + // Replace server-provided timetoken. + json.t.t = timetoken; + if (region) + json.t.r = parseInt(region, 10); + try { + body = SubscribeRequestsManager.textEncoder.encode(JSON.stringify(json)).buffer; + if (body.byteLength) { + const headers = new Headers(response.headers); + headers.set('Content-Length', `${body.byteLength}`); + // Create a new response with the original response options and modified headers + decidedResponse = new Response(body, { + status: response.status, + statusText: response.statusText, + headers: headers, + }); + } + } + catch (error) { + console.error(`Subscribe serialization error: ${error}`); + return serverResponse; + } + return body.byteLength > 0 ? [decidedResponse, body] : serverResponse; + } + } + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information /** - * Create request timeout timer. - * - * **Note:** Native Fetch API doesn't support `timeout` out-of-box and {@link Promise} used to emulate it. - * - * @param requestId - Unique identifier of request which will time out after {@link requestTimeout} seconds. - * @param requestTimeout - Number of seconds after which request with specified identifier will time out. - * - * @returns Promise which rejects after time out will fire. + * Service response binary data decoder. */ - const requestTimeoutTimer = (requestId, requestTimeout) => new Promise((_, reject) => { - const timeoutId = setTimeout(() => { - // Clean up. - abortControllers.delete(requestId); - clearTimeout(timeoutId); - reject(new Error('Request timeout')); - }, requestTimeout * 1000); - }); + SubscribeRequestsManager.textDecoder = new TextDecoder(); /** - * Retrieve list of PubNub clients which is pending for service worker request completion. - * - * @param identifier - Identifier of the subscription request which has been scheduled by the Service Worker. - * - * @returns List of PubNub client state objects for Service Worker. + * Stringified to binary data encoder. */ - const clientsForRequest = (identifier) => { - return Object.values(pubNubClients).filter((client) => client !== undefined && client.subscription !== undefined && client.subscription.serviceRequestId === identifier); - }; + SubscribeRequestsManager.textEncoder = new TextEncoder(); + /** - * Clean up PubNub client states from ongoing request. - * - * Reset requested and scheduled request information to make PubNub client "free" for next requests. - * - * @param clients - List of PubNub clients which awaited for scheduled request completion. - * @param requestId - Unique subscribe request identifier for which {@link clients} has been provided. + * Type with events which is dispatched by heartbeat state in response to client-provided requests and PubNub + * client state change. */ - const markRequestCompleted = (clients, requestId) => { - delete serviceRequests[requestId]; - clients.forEach((client) => { - if (client.subscription) { - delete client.subscription.request; - delete client.subscription.serviceRequestId; - } - }); - }; + var HeartbeatStateEvent; + (function (HeartbeatStateEvent) { + /** + * Heartbeat state ready to send another heartbeat. + */ + HeartbeatStateEvent["Heartbeat"] = "heartbeat"; + /** + * Heartbeat state has been invalidated after all clients' state was removed from it. + */ + HeartbeatStateEvent["Invalidated"] = "invalidated"; + })(HeartbeatStateEvent || (HeartbeatStateEvent = {})); /** - * Creates a Request object from a given {@link TransportRequest} object. - * - * @param req - The {@link TransportRequest} object containing request information. - * - * @returns `Request` object generated from the {@link TransportRequest} object or `undefined` if no request - * should be sent. + * Dispatched by heartbeat state when new heartbeat can be sent. */ - const requestFromTransportRequest = (req) => { - let headers = undefined; - const queryParameters = req.queryParameters; - let path = req.path; - if (req.headers) { - headers = {}; - for (const [key, value] of Object.entries(req.headers)) - headers[key] = value; - } - if (queryParameters && Object.keys(queryParameters).length !== 0) - path = `${path}?${queryStringFromObject(queryParameters)}`; - return new Request(`${req.origin}${path}`, { - method: req.method, - headers, - redirect: 'follow', - }); - }; + class HeartbeatStateHeartbeatEvent extends CustomEvent { + /** + * Create heartbeat state heartbeat event. + * + * @param request - Aggregated heartbeat request which can be sent. + */ + constructor(request) { + super(HeartbeatStateEvent.Heartbeat, { detail: request }); + } + /** + * Retrieve aggregated heartbeat request which can be sent. + * + * @returns Aggregated heartbeat request which can be sent. + */ + get request() { + return this.detail; + } + /** + * Create clone of heartbeat event to make it possible to forward event upstream. + * + * @returns Client heartbeat event. + */ + clone() { + return new HeartbeatStateHeartbeatEvent(this.request); + } + } /** - * Construct transport request from send subscription request event. - * - * Update transport request to aggregate channels and groups if possible. - * - * @param event - Client's send subscription event request. - * - * @returns Final transport request or identifier from active request which will provide response to required - * channels and groups. + * Dispatched by heartbeat state when it has been invalidated. */ - const subscribeTransportRequestFromEvent = (event) => { - var _a, _b, _c, _d, _e; - const client = pubNubClients[event.clientIdentifier]; - const subscription = client.subscription; - const clients = clientsForSendSubscribeRequestEvent(subscription.timetoken, event); - const serviceRequestId = uuidGenerator.createUUID(); - const request = Object.assign({}, event.request); - let previousSubscribeTimetokenRefreshTimestamp; - let previousSubscribeTimetoken; - let previousSubscribeRegion; - if (clients.length > 1) { - const activeRequestId = activeSubscriptionForEvent(clients, event); - // Return identifier of the ongoing request. - if (activeRequestId) { - const scheduledRequest = serviceRequests[activeRequestId]; - const { channels, channelGroups } = (_a = client.subscription) !== null && _a !== void 0 ? _a : { channels: [], channelGroups: [] }; - if ((channels.length > 0 ? includesStrings(scheduledRequest.channels, channels) : true) && - (channelGroups.length > 0 ? includesStrings(scheduledRequest.channelGroups, channelGroups) : true)) { - return activeRequestId; - } - } - const state = ((_b = presenceState[client.subscriptionKey]) !== null && _b !== void 0 ? _b : {})[client.userId]; - const aggregatedState = {}; - const channelGroups = new Set(subscription.channelGroups); - const channels = new Set(subscription.channels); - if (state && subscription.objectsWithState.length) { - subscription.objectsWithState.forEach((name) => { - const objectState = state[name]; - if (objectState) - aggregatedState[name] = objectState; - }); - } - for (const _client of clients) { - const { subscription: _subscription } = _client; - // Skip clients which doesn't have active subscription request. - if (!_subscription) - continue; - // Keep track of timetoken from previous call to use it for catchup after initial subscribe. - if (_subscription.timetoken) { - let shouldSetPreviousTimetoken = !previousSubscribeTimetoken; - if (!shouldSetPreviousTimetoken && _subscription.timetoken !== '0') { - if (previousSubscribeTimetoken === '0') - shouldSetPreviousTimetoken = true; - else if (_subscription.timetoken < previousSubscribeTimetoken) - shouldSetPreviousTimetoken = _subscription.refreshTimestamp > previousSubscribeTimetokenRefreshTimestamp; - } - if (shouldSetPreviousTimetoken) { - previousSubscribeTimetokenRefreshTimestamp = _subscription.refreshTimestamp; - previousSubscribeTimetoken = _subscription.timetoken; - previousSubscribeRegion = _subscription.region; - } - } - _subscription.channelGroups.forEach(channelGroups.add, channelGroups); - _subscription.channels.forEach(channels.add, channels); - const activeServiceRequestId = _subscription.serviceRequestId; - _subscription.serviceRequestId = serviceRequestId; - // Set awaited service worker request identifier. - if (activeServiceRequestId && serviceRequests[activeServiceRequestId]) { - cancelRequest(activeServiceRequestId); - } - if (!state) - continue; - _subscription.objectsWithState.forEach((name) => { - const objectState = state[name]; - if (objectState && !aggregatedState[name]) - aggregatedState[name] = objectState; - }); - } - const serviceRequest = ((_c = serviceRequests[serviceRequestId]) !== null && _c !== void 0 ? _c : (serviceRequests[serviceRequestId] = { - requestId: serviceRequestId, - timetoken: (_d = request.queryParameters.tt) !== null && _d !== void 0 ? _d : '0', - channelGroups: [], - channels: [], - })); + class HeartbeatStateInvalidateEvent extends CustomEvent { + /** + * Create heartbeat state invalidation event. + */ + constructor() { + super(HeartbeatStateEvent.Invalidated); + } + /** + * Create clone of invalidate event to make it possible to forward event upstream. + * + * @returns Client invalidate event. + */ + clone() { + return new HeartbeatStateInvalidateEvent(); + } + } + + class HeartbeatRequest extends BasePubNubRequest { + // endregion + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + /** + * Create heartbeat request from received _transparent_ transport request. + * + * @param request - Object with heartbeat transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + * @returns Initialized and ready to use heartbeat request. + */ + static fromTransportRequest(request, subscriptionKey, accessToken) { + return new HeartbeatRequest(request, subscriptionKey, accessToken); + } + /** + * Create heartbeat request from previously cached data. + * + * @param request - Object with subscribe transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [aggregatedChannelGroups] - List of aggregated channel groups for the same user. + * @param [aggregatedChannels] - List of aggregated channels for the same user. + * @param [aggregatedState] - State aggregated for the same user. + * @param [accessToken] - Access token with read permissions on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + * @retusns Initialized and ready to use heartbeat request. + */ + static fromCachedState(request, subscriptionKey, aggregatedChannelGroups, aggregatedChannels, aggregatedState, accessToken) { // Update request channels list (if required). - if (channels.size) { - serviceRequest.channels = Array.from(channels).sort(); + if (aggregatedChannels.length || aggregatedChannelGroups.length) { const pathComponents = request.path.split('/'); - pathComponents[4] = serviceRequest.channels.join(','); + pathComponents[6] = aggregatedChannels.length ? [...aggregatedChannels].sort().join(',') : ','; request.path = pathComponents.join('/'); } // Update request channel groups list (if required). - if (channelGroups.size) { - serviceRequest.channelGroups = Array.from(channelGroups).sort(); - request.queryParameters['channel-group'] = serviceRequest.channelGroups.join(','); + if (aggregatedChannelGroups.length) + request.queryParameters['channel-group'] = [...aggregatedChannelGroups].sort().join(','); + // Update request `state` (if required). + if (aggregatedState && Object.keys(aggregatedState).length) + request.queryParameters.state = JSON.stringify(aggregatedState); + else + delete request.queryParameters.aggregatedState; + if (accessToken) + request.queryParameters.auth = accessToken.toString(); + request.identifier = uuidGenerator.createUUID(); + return new HeartbeatRequest(request, subscriptionKey, accessToken); + } + /** + * Create heartbeat request from received _transparent_ transport request. + * + * @param request - Object with heartbeat transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + */ + constructor(request, subscriptionKey, accessToken) { + const channelGroups = HeartbeatRequest.channelGroupsFromRequest(request).filter((group) => !group.endsWith('-pnpres')); + const channels = HeartbeatRequest.channelsFromRequest(request).filter((channel) => !channel.endsWith('-pnpres')); + super(request, subscriptionKey, request.queryParameters.uuid, channels, channelGroups, accessToken); + // Clean up `state` from objects which is not used with request (if needed). + if (!request.queryParameters.state || request.queryParameters.state.length === 0) + return; + const state = JSON.parse(request.queryParameters.state); + for (const objectName of Object.keys(state)) + if (!this.channels.includes(objectName) && !this.channelGroups.includes(objectName)) + delete state[objectName]; + this.state = state; + } + // endregion + // -------------------------------------------------------- + // ---------------------- Properties ---------------------- + // -------------------------------------------------------- + // region Properties + /** + * Represent heartbeat request as identifier. + * + * Generated identifier will be identical for requests created for the same user. + */ + get asIdentifier() { + const auth = this.accessToken ? this.accessToken.asIdentifier : undefined; + return `${this.userId}-${this.subscribeKey}${auth ? `-${auth}` : ''}`; + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Serialize request for easier representation in logs. + * + * @returns Stringified `heartbeat` request. + */ + toString() { + return `HeartbeatRequest { channels: [${this.channels.length ? this.channels.map((channel) => `'${channel}'`).join(', ') : ''}], channelGroups: [${this.channelGroups.length ? this.channelGroups.map((group) => `'${group}'`).join(', ') : ''}] }`; + } + /** + * Serialize request to "typed" JSON string. + * + * @returns "Typed" JSON string. + */ + toJSON() { + return this.toString(); + } + /** + * Extract list of channels for presence announcement from request URI path. + * + * @param request - Transport request from which should be extracted list of channels for presence announcement. + * + * @returns List of channel names (not percent-decoded) for which `heartbeat` has been called. + */ + static channelsFromRequest(request) { + const channels = request.path.split('/')[6]; + return channels === ',' ? [] : channels.split(',').filter((name) => name.length > 0); + } + /** + * Extract list of channel groups for presence announcement from request query. + * + * @param request - Transport request from which should be extracted list of channel groups for presence announcement. + * + * @returns List of channel group names (not percent-decoded) for which `heartbeat` has been called. + */ + static channelGroupsFromRequest(request) { + if (!request.queryParameters || !request.queryParameters['channel-group']) + return []; + const group = request.queryParameters['channel-group']; + return group.length === 0 ? [] : group.split(',').filter((name) => name.length > 0); + } + } + + class HeartbeatState extends EventTarget { + // endregion + // -------------------------------------------------------- + // --------------------- Constructor ---------------------- + // -------------------------------------------------------- + // region Constructor + /** + * Create heartbeat state management object. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + constructor(identifier) { + super(); + this.identifier = identifier; + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + /** + * Map of client identifiers to their portion of data which affects heartbeat state. + * + * **Note:** This information removed only with {@link HeartbeatState.removeClient|removeClient} function call. + */ + this.clientsState = {}; + /** + * Map of client to its requests which is pending for service request processing results. + */ + this.requests = {}; + /** + * Time when previous heartbeat request has been done. + */ + this.lastHeartbeatTimestamp = 0; + /** + * Stores whether automated _backup_ timer can fire or not. + */ + this.canSendBackupHeartbeat = true; + /** + * Whether previous call failed with `Access Denied` error or not. + */ + this.isAccessDeniedError = false; + /** + * Presence heartbeat interval. + * + * Value used to decide whether new request should be handled right away or should wait for _backup_ timer in state + * to send aggregated request. + */ + this._interval = 0; + } + // endregion + // -------------------------------------------------------- + // --------------------- Properties ----------------------- + // -------------------------------------------------------- + // region Properties + /** + * Update presence heartbeat interval. + * + * @param value - New heartbeat interval. + */ + set interval(value) { + const changed = this._interval !== value; + this._interval = value; + if (!changed) + return; + // Restart timer if required. + if (value === 0) + this.stopTimer(); + else + this.startTimer(); + } + /** + * Update access token which should be used for aggregated heartbeat requests. + * + * @param value - New access token for heartbeat requests. + */ + set accessToken(value) { + if (!value) { + this._accessToken = value; + return; } - // Update request `state` (if required). - if (Object.keys(aggregatedState).length) - request.queryParameters['state'] = JSON.stringify(aggregatedState); - // Update `auth` key (if required). - if (request.queryParameters && request.queryParameters.auth) { - const authKey = authKeyForAggregatedClientsRequest(clients); - if (authKey) - request.queryParameters.auth = authKey; + const accessTokens = Object.values(this.requests) + .filter((request) => !!request.accessToken) + .map((request) => request.accessToken); + accessTokens.push(value); + this._accessToken = accessTokens.sort(AccessToken.compare).pop(); + // Restart _backup_ heartbeat if previous call failed because of permissions error. + if (this.isAccessDeniedError) { + this.canSendBackupHeartbeat = true; + this.startTimer(this.presenceTimerTimeout()); } } - else { - serviceRequests[serviceRequestId] = { - requestId: serviceRequestId, - timetoken: (_e = request.queryParameters.tt) !== null && _e !== void 0 ? _e : '0', - channelGroups: subscription.channelGroups, - channels: subscription.channels, - }; + // endregion + // -------------------------------------------------------- + // ---------------------- Accessors ----------------------- + // -------------------------------------------------------- + // region Accessors + /** + * Retrieve portion of heartbeat state which is related to the specific client. + * + * @param client - Reference to the PubNub client for which state should be retrieved. + * @returns PubNub client's state in heartbeat. + */ + stateForClient(client) { + if (!this.clientsState[client.identifier]) + return undefined; + const clientState = this.clientsState[client.identifier]; + return clientState + ? { channels: [...clientState.channels], channelGroups: [...clientState.channelGroups], state: clientState.state } + : { channels: [], channelGroups: [] }; } - if (serviceRequests[serviceRequestId]) { - if (request.queryParameters && - request.queryParameters.tt !== undefined && - request.queryParameters.tr !== undefined) { - serviceRequests[serviceRequestId].region = request.queryParameters.tr; - } - if (!serviceRequests[serviceRequestId].timetokenOverride || - (serviceRequests[serviceRequestId].timetokenOverride !== '0' && - previousSubscribeTimetoken && - previousSubscribeTimetoken !== '0')) { - serviceRequests[serviceRequestId].timetokenOverride = previousSubscribeTimetoken; - serviceRequests[serviceRequestId].regionOverride = previousSubscribeRegion; + /** + * Retrieve recent heartbeat request for the client. + * + * @param client - Reference to the client for which request should be retrieved. + * @returns List of client's ongoing requests. + */ + requestForClient(client) { + return this.requests[client.identifier]; + } + // endregion + // -------------------------------------------------------- + // --------------------- Aggregation ---------------------- + // -------------------------------------------------------- + // region Aggregation + /** + * Add new client's request to the state. + * + * @param client - Reference to PubNub client which is adding new requests for processing. + * @param request - New client-provided heartbeat request for processing. + */ + addClientRequest(client, request) { + this.requests[client.identifier] = request; + this.clientsState[client.identifier] = { channels: request.channels, channelGroups: request.channelGroups }; + if (request.state) + this.clientsState[client.identifier].state = Object.assign({}, request.state); + // Update access token information (use the one which will provide permissions for longer period). + const sortedTokens = Object.values(this.requests) + .filter((request) => !!request.accessToken) + .map((request) => request.accessToken) + .sort(AccessToken.compare); + if (sortedTokens && sortedTokens.length > 0) + this._accessToken = sortedTokens.pop(); + this.sendAggregatedHeartbeat(request); + } + /** + * Remove client and requests associated with it from the state. + * + * @param client - Reference to the PubNub client which should be removed. + */ + removeClient(client) { + delete this.clientsState[client.identifier]; + delete this.requests[client.identifier]; + // Stop backup timer if there is no more channels and groups left. + if (!Object.keys(this.clientsState).length) { + this.stopTimer(); + this.dispatchEvent(new HeartbeatStateInvalidateEvent()); } } - subscription.serviceRequestId = serviceRequestId; - request.identifier = serviceRequestId; - const clientIds = clients - .reduce((identifiers, { clientIdentifier }) => { - identifiers.push(clientIdentifier); - return identifiers; - }, []) - .join(', '); - if (clientIds.length > 0) { - for (const _client of clients) - consoleDir(serviceRequests[serviceRequestId], `Started aggregated request for clients: ${clientIds}`, _client); - } - return request; - }; - /** - * Construct transport request from send heartbeat request event. - * - * Update transport request to aggregate channels and groups if possible. - * - * @param event - Client's send heartbeat event request. - * @param [actualRequest] - Whether handling actual request from the core-part of the client and not backup heartbeat in - * the `SharedWorker`. - * @param [outOfOrder] - Whether handling request which is sent on irregular basis (setting update). - * - * @returns Final transport request or identifier from active request which will provide response to required - * channels and groups. - */ - const heartbeatTransportRequestFromEvent = (event, actualRequest, outOfOrder) => { - var _a, _b, _c, _d; - var _e; - const client = pubNubClients[event.clientIdentifier]; - const clients = clientsForSendHeartbeatRequestEvent(event); - const request = Object.assign({}, event.request); - if (!client || !client.heartbeat) - return undefined; - const hbRequestsBySubscriptionKey = ((_a = serviceHeartbeatRequests[_e = client.subscriptionKey]) !== null && _a !== void 0 ? _a : (serviceHeartbeatRequests[_e] = {})); - const heartbeatRequestKey = `${client.userId}_${(_b = clientAggregateAuthKey(client)) !== null && _b !== void 0 ? _b : ''}`; - const channelGroupsForAnnouncement = [...client.heartbeat.channelGroups]; - const channelsForAnnouncement = [...client.heartbeat.channels]; - let aggregatedState; - let failedPreviousRequest = false; - let aggregated; - if (!hbRequestsBySubscriptionKey[heartbeatRequestKey]) { - hbRequestsBySubscriptionKey[heartbeatRequestKey] = { - createdByActualRequest: actualRequest, - channels: channelsForAnnouncement, - channelGroups: channelGroupsForAnnouncement, - clientIdentifier: client.clientIdentifier, - timestamp: Date.now(), - }; - aggregatedState = (_c = client.heartbeat.presenceState) !== null && _c !== void 0 ? _c : {}; - aggregated = false; - } - else { - const { createdByActualRequest, channels, channelGroups, response } = hbRequestsBySubscriptionKey[heartbeatRequestKey]; - // Allow out-of-order call from the client for heartbeat initiated by the `SharedWorker`. - if (!createdByActualRequest && actualRequest) { - hbRequestsBySubscriptionKey[heartbeatRequestKey].createdByActualRequest = true; - hbRequestsBySubscriptionKey[heartbeatRequestKey].timestamp = Date.now(); - outOfOrder = true; + removeFromClientState(client, channels, channelGroups) { + const clientState = this.clientsState[client.identifier]; + if (!clientState) + return; + clientState.channelGroups = clientState.channelGroups.filter((group) => !channelGroups.includes(group)); + clientState.channels = clientState.channels.filter((channel) => !channels.includes(channel)); + if (clientState.channels.length === 0 && clientState.channelGroups.length === 0) { + this.removeClient(client); + return; } - aggregatedState = (_d = client.heartbeat.presenceState) !== null && _d !== void 0 ? _d : {}; - aggregated = - includesStrings(channels, channelsForAnnouncement) && - includesStrings(channelGroups, channelGroupsForAnnouncement); - if (response) - failedPreviousRequest = response[0].status >= 400; - } - // Find minimum heartbeat interval which maybe required to use. - let minimumHeartbeatInterval = client.heartbeatInterval; - for (const client of clients) { - if (client.heartbeatInterval) - minimumHeartbeatInterval = Math.min(minimumHeartbeatInterval, client.heartbeatInterval); - } - // Check whether multiple instance aggregate heartbeat and there is previous sender known. - // `clientIdentifier` maybe empty in case if client which triggered heartbeats before has been invalidated and new - // should handle heartbeat unconditionally. - if (aggregated && hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier) { - const expectedTimestamp = hbRequestsBySubscriptionKey[heartbeatRequestKey].timestamp + minimumHeartbeatInterval * 1000; - const currentTimestamp = Date.now(); - // Request should be sent if a previous attempt failed. - if (!outOfOrder && !failedPreviousRequest && currentTimestamp < expectedTimestamp) { + // Clean up user's presence state from removed channels and groups. + if (!clientState.state) + return; + Object.keys(clientState.state).forEach((key) => { + if (!clientState.channels.includes(key) && !clientState.channelGroups.includes(key)) + delete clientState.state[key]; + }); + } + /** + * Start "backup" presence heartbeat timer. + * + * @param targetInterval - Interval after which heartbeat request should be sent. + */ + startTimer(targetInterval) { + this.stopTimer(); + if (Object.keys(this.clientsState).length === 0) + return; + this.timeout = setTimeout(() => this.handlePresenceTimer(), (targetInterval !== null && targetInterval !== void 0 ? targetInterval : this._interval) * 1000); + } + /** + * Stop "backup" presence heartbeat timer. + */ + stopTimer() { + if (this.timeout) + clearTimeout(this.timeout); + this.timeout = undefined; + } + /** + * Send aggregated heartbeat request (if possible). + * + * @param [request] - Client provided request which tried to announce presence. + */ + sendAggregatedHeartbeat(request) { + if (this.lastHeartbeatTimestamp !== 0) { // Check whether it is too soon to send request or not. - const leeway = minimumHeartbeatInterval * 0.05 * 1000; - // Leeway can't be applied if actual interval between heartbeat requests is smaller - // than 3 seconds which derived from the server's threshold. - if (minimumHeartbeatInterval - leeway <= 3 || expectedTimestamp - currentTimestamp > leeway) { - startHeartbeatTimer(client, true); - return undefined; + const expected = this.lastHeartbeatTimestamp + this._interval * 1000; + let leeway = this._interval * 0.05; + if (this._interval - leeway < 3) + leeway = 0; + const current = Date.now(); + if (expected - current > leeway * 1000) { + if (request && !!this.previousRequestResult) { + const fetchRequest = request.asFetchRequest; + const result = Object.assign(Object.assign({}, this.previousRequestResult), { clientIdentifier: request.client.identifier, identifier: request.identifier, url: fetchRequest.url }); + request.handleProcessingStarted(); + request.handleProcessingSuccess(fetchRequest, result); + return; + } + else if (!request) + return; } } + const requests = Object.values(this.requests); + const baseRequest = requests[Math.floor(Math.random() * requests.length)]; + const aggregatedRequest = Object.assign({}, baseRequest.request); + let state = {}; + const channelGroups = new Set(); + const channels = new Set(); + Object.values(this.clientsState).forEach((clientState) => { + if (clientState.state) + state = Object.assign(Object.assign({}, state), clientState.state); + clientState.channelGroups.forEach(channelGroups.add, channelGroups); + clientState.channels.forEach(channels.add, channels); + }); + this.lastHeartbeatTimestamp = Date.now(); + const serviceRequest = HeartbeatRequest.fromCachedState(aggregatedRequest, requests[0].subscribeKey, [...channelGroups], [...channels], Object.keys(state).length > 0 ? state : undefined, this._accessToken); + // Set service request for all client-provided requests without response. + Object.values(this.requests).forEach((request) => !request.serviceRequest && (request.serviceRequest = serviceRequest)); + this.addListenersForRequest(serviceRequest); + this.dispatchEvent(new HeartbeatStateHeartbeatEvent(serviceRequest)); + // Restart _backup_ timer after regular client-provided request triggered heartbeat. + if (request) + this.startTimer(); } - delete hbRequestsBySubscriptionKey[heartbeatRequestKey].response; - hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier = client.clientIdentifier; - // Aggregate channels for similar clients which is pending for heartbeat. - for (const _client of clients) { - const { heartbeat } = _client; - if (heartbeat === undefined || _client.clientIdentifier === event.clientIdentifier) - continue; - // Append presence state from the client (will override previously set value if already set). - if (heartbeat.presenceState) - aggregatedState = Object.assign(Object.assign({}, aggregatedState), heartbeat.presenceState); - channelGroupsForAnnouncement.push(...heartbeat.channelGroups.filter((channel) => !channelGroupsForAnnouncement.includes(channel))); - channelsForAnnouncement.push(...heartbeat.channels.filter((channel) => !channelsForAnnouncement.includes(channel))); - } - hbRequestsBySubscriptionKey[heartbeatRequestKey].channels = channelsForAnnouncement; - hbRequestsBySubscriptionKey[heartbeatRequestKey].channelGroups = channelGroupsForAnnouncement; - if (!outOfOrder) - hbRequestsBySubscriptionKey[heartbeatRequestKey].timestamp = Date.now(); - // Remove presence state for objects which is not part of heartbeat. - for (const objectName in Object.keys(aggregatedState)) { - if (!channelsForAnnouncement.includes(objectName) && !channelGroupsForAnnouncement.includes(objectName)) - delete aggregatedState[objectName]; - } - // No need to try send request with empty list of channels and groups. - if (channelsForAnnouncement.length === 0 && channelGroupsForAnnouncement.length === 0) - return undefined; - // Update request channels list (if required). - if (channelsForAnnouncement.length || channelGroupsForAnnouncement.length) { - const pathComponents = request.path.split('/'); - pathComponents[6] = channelsForAnnouncement.length ? channelsForAnnouncement.join(',') : ','; - request.path = pathComponents.join('/'); - } - // Update request channel groups list (if required). - if (channelGroupsForAnnouncement.length) - request.queryParameters['channel-group'] = channelGroupsForAnnouncement.join(','); - // Update request `state` (if required). - if (Object.keys(aggregatedState).length) - request.queryParameters['state'] = JSON.stringify(aggregatedState); - else - delete request.queryParameters['state']; - // Update `auth` key (if required). - if (clients.length > 1 && request.queryParameters && request.queryParameters.auth) { - const aggregatedAuthKey = authKeyForAggregatedClientsRequest(clients); - if (aggregatedAuthKey) - request.queryParameters.auth = aggregatedAuthKey; - } - return request; - }; - /** - * Construct transport request from send leave request event. - * - * Filter out channels and groups, which is still in use by other PubNub client instances from leave request. - * - * @param event - Client's sending leave event request. - * @param [invalidatedClient] - Invalidated PubNub client state. - * - * @returns Final transport request or `undefined` in case if there are no channels and groups for which request can be - * done. - */ - const leaveTransportRequestFromEvent = (event, invalidatedClient) => { - var _a; - const client = invalidatedClient !== null && invalidatedClient !== void 0 ? invalidatedClient : pubNubClients[event.clientIdentifier]; - const clients = clientsForSendLeaveRequestEvent(event, invalidatedClient); - let channelGroups = channelGroupsFromRequest(event.request); - let channels = channelsFromRequest(event.request); - const request = Object.assign({}, event.request); - // Remove channels / groups from active client's subscription. - if (client && client.subscription) { - const { subscription } = client; - if (channels.length) { - subscription.channels = subscription.channels.filter((channel) => !channels.includes(channel)); - // Modify cached request path. - const pathComponents = subscription.path.split('/'); - if (pathComponents[4] !== ',') { - const pathChannels = pathComponents[4].split(',').filter((channel) => !channels.includes(channel)); - pathComponents[4] = pathChannels.length ? pathChannels.join(',') : ','; - subscription.path = pathComponents.join('/'); + // endregion + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + /** + * Add listeners to the service request. + * + * Listeners used to capture last service success response and mark whether further _backup_ requests possible or not. + * + * @param request - Service `heartbeat` request for which events will be listened once. + */ + addListenersForRequest(request) { + const ac = new AbortController(); + const callback = (evt) => { + // Clean up service request listeners. + ac.abort(); + if (evt instanceof RequestSuccessEvent) { + const { response } = evt; + this.previousRequestResult = response; } - } - if (channelGroups.length) { - subscription.channelGroups = subscription.channelGroups.filter((group) => !channelGroups.includes(group)); - // Modify cached request path. - if (subscription.channelGroupQuery.length > 0) { - const queryChannelGroups = subscription.channelGroupQuery - .split(',') - .filter((group) => !channelGroups.includes(group)); - subscription.channelGroupQuery = queryChannelGroups.length ? queryChannelGroups.join(',') : ''; + else if (evt instanceof RequestErrorEvent) { + const { error } = evt; + this.canSendBackupHeartbeat = true; + this.isAccessDeniedError = false; + if (error.response && error.response.status >= 400 && error.response.status < 500) { + this.isAccessDeniedError = error.response.status === 403; + this.canSendBackupHeartbeat = false; + } } - } + }; + request.addEventListener(PubNubSharedWorkerRequestEvents.Success, callback, { signal: ac.signal, once: true }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Error, callback, { signal: ac.signal, once: true }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Canceled, callback, { signal: ac.signal, once: true }); } - // Remove channels / groups from client's presence heartbeat state. - if (client && client.heartbeat) { - const { heartbeat } = client; - if (channels.length) - heartbeat.channels = heartbeat.channels.filter((channel) => !channels.includes(channel)); - if (channelGroups.length) - heartbeat.channelGroups = heartbeat.channelGroups.filter((channel) => !channelGroups.includes(channel)); - } - // Filter out channels and groups which is still in use by the other PubNub client instances. - for (const client of clients) { - const subscription = client.subscription; - if (subscription === undefined) - continue; - if (client.clientIdentifier === event.clientIdentifier) - continue; - if (channels.length) - channels = channels.filter((channel) => !channel.endsWith('-pnpres') && !subscription.channels.includes(channel)); - if (channelGroups.length) - channelGroups = channelGroups.filter((group) => !group.endsWith('-pnpres') && !subscription.channelGroups.includes(group)); - } - // Clean up from presence channels and groups - const channelsAndGroupsCount = channels.length + channelGroups.length; - if (channels.length) - channels = channels.filter((channel) => !channel.endsWith('-pnpres')); - if (channelGroups.length) - channelGroups = channelGroups.filter((group) => !group.endsWith('-pnpres')); - if (channels.length === 0 && channelGroups.length === 0) { - if (client && client.workerLogVerbosity) { - const clientIds = clients - .reduce((identifiers, { clientIdentifier }) => { - identifiers.push(clientIdentifier); - return identifiers; - }, []) - .join(', '); - if (channelsAndGroupsCount > 0) { - consoleLog(`Leaving only presence channels which doesn't require presence leave. Ignoring leave request.`, client); - } - else { - consoleLog(`Specified channels and groups still in use by other clients: ${clientIds}. Ignoring leave request.`, client); - } - } - return undefined; + /** + * Handle periodic _backup_ heartbeat timer. + */ + handlePresenceTimer() { + if (Object.keys(this.clientsState).length === 0 || !this.canSendBackupHeartbeat) + return; + const targetInterval = this.presenceTimerTimeout(); + this.sendAggregatedHeartbeat(); + this.startTimer(targetInterval); } - // Update aggregated heartbeat state object. - if (client && serviceHeartbeatRequests[client.subscriptionKey] && (channels.length || channelGroups.length)) { - const hbRequestsBySubscriptionKey = serviceHeartbeatRequests[client.subscriptionKey]; - const heartbeatRequestKey = `${client.userId}_${(_a = clientAggregateAuthKey(client)) !== null && _a !== void 0 ? _a : ''}`; - if (hbRequestsBySubscriptionKey[heartbeatRequestKey]) { - let { channels: hbChannels, channelGroups: hbChannelGroups } = hbRequestsBySubscriptionKey[heartbeatRequestKey]; - if (channelGroups.length) - hbChannelGroups = hbChannelGroups.filter((group) => !channels.includes(group)); - if (channels.length) - hbChannels = hbChannels.filter((channel) => !channels.includes(channel)); - hbRequestsBySubscriptionKey[heartbeatRequestKey].channelGroups = hbChannelGroups; - hbRequestsBySubscriptionKey[heartbeatRequestKey].channels = hbChannels; - } + /** + * Compute timeout for _backup_ heartbeat timer. + * + * @returns Number of seconds after which new aggregated heartbeat request should be sent. + */ + presenceTimerTimeout() { + const timePassed = (Date.now() - this.lastHeartbeatTimestamp) / 1000; + let targetInterval = this._interval; + if (timePassed < targetInterval) + targetInterval -= timePassed; + if (targetInterval === this._interval) + targetInterval += 0.05; + targetInterval = Math.max(targetInterval, 3); + return targetInterval; } - // Update request channels list (if required). - if (channels.length) { - const pathComponents = request.path.split('/'); - pathComponents[6] = channels.join(','); - request.path = pathComponents.join('/'); - } - // Update request channel groups list (if required). - if (channelGroups.length) - request.queryParameters['channel-group'] = channelGroups.join(','); - // Update `auth` key (if required). - if (clients.length > 1 && request.queryParameters && request.queryParameters.auth) { - const aggregatedAuthKey = authKeyForAggregatedClientsRequest(clients); - if (aggregatedAuthKey) - request.queryParameters.auth = aggregatedAuthKey; - } - return request; - }; + } + /** - * Send event to the specific PubNub client. + * Heartbeat requests manager responsible for heartbeat aggregation and backup of throttled clients (background tabs). * - * @param client - State for the client which should receive {@link event}. - * @param event - Subscription worker event object. + * On each heartbeat request from core PubNub client module manager will try to identify whether it is time to send it + * and also will try to aggregate call for channels / groups for the same user. */ - const publishClientEvent = (client, event) => { - var _a; - const receiver = ((_a = sharedWorkerClients[client.subscriptionKey]) !== null && _a !== void 0 ? _a : {})[client.clientIdentifier]; - if (!receiver) - return false; - try { - receiver.postMessage(event); - return true; + class HeartbeatRequestsManager extends RequestsManager { + // endregion + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + /** + * Create heartbeat requests manager. + * + * @param clientsManager - Reference to the core PubNub clients manager to track their life-cycle and make + * corresponding state changes. + */ + constructor(clientsManager) { + super(); + this.clientsManager = clientsManager; + /** + * Map of unique user identifier (composed from multiple request object properties) to the aggregated heartbeat state. + * @private + */ + this.heartbeatStates = {}; + /** + * Map of client identifiers to `AbortController` instances which is used to detach added listeners when PubNub client + * unregister. + */ + this.clientAbortControllers = {}; + this.subscribeOnClientEvents(clientsManager); } - catch (error) { - if (client.workerLogVerbosity) - console.error(`[SharedWorker] Unable send message using message port: ${error}`); + // endregion + // -------------------------------------------------------- + // --------------------- Aggregation ---------------------- + // -------------------------------------------------------- + // region Aggregation + /** + * Retrieve heartbeat state with which specific client is working. + * + * @param client - Reference to the PubNub client for which heartbeat state should be found. + * @returns Reference to the heartbeat state if client has ongoing requests. + */ + heartbeatStateForClient(client) { + for (const heartbeatState of Object.values(this.heartbeatStates)) + if (!!heartbeatState.stateForClient(client)) + return heartbeatState; + return undefined; } - return false; - }; - /** - * Send request processing result event. - * - * @param clients - List of PubNub clients which should be notified about request result. - * @param fetchRequest - Actual request which has been used with `fetch` API. - * @param response - PubNub service response. - * @param request - Processed request information. - * @param [result] - Explicit request processing result which should be notified. - */ - const notifyRequestProcessingResult = (clients, fetchRequest, response, request, result) => { - var _a, _b; - if (clients.length === 0) - return; - if (!result && !response) - return; - const workerLogVerbosity = clients.some((client) => client && client.workerLogVerbosity); - const clientIds = (_a = sharedWorkerClients[clients[0].subscriptionKey]) !== null && _a !== void 0 ? _a : {}; - const isSubscribeRequest = request.path.startsWith('/v2/subscribe'); - if (!result && response) { - result = - response[0].status >= 400 - ? // Treat 4xx and 5xx status codes as errors. - requestProcessingError(undefined, response) - : requestProcessingSuccess(response); - } - const headers = {}; - let body; - let status = 200; - // Compose request response object. - if (response) { - body = response[1].byteLength > 0 ? response[1] : undefined; - const { headers: requestHeaders } = response[0]; - status = response[0].status; - // Copy Headers object content into plain Record. - requestHeaders.forEach((value, key) => (headers[key] = value.toLowerCase())); - } - const transportResponse = { status, url: fetchRequest.url, headers, body }; - // Notify about subscribe and leave requests completion. - if (workerLogVerbosity && request && !request.path.endsWith('/heartbeat')) { - const notifiedClientIds = clients - .reduce((identifiers, { clientIdentifier }) => { - identifiers.push(clientIdentifier); - return identifiers; - }, []) - .join(', '); - const endpoint = isSubscribeRequest ? 'subscribe' : 'leave'; - const message = `Notify clients about ${endpoint} request completion: ${notifiedClientIds}`; - for (const client of clients) - consoleLog(message, client); - } - for (const client of clients) { - if (isSubscribeRequest && !client.subscription) { - // Notifying about client with inactive subscription. - if (workerLogVerbosity) { - const message = `${client.clientIdentifier} doesn't have active subscription. Don't notify about completion.`; - for (const nClient of clients) - consoleLog(message, nClient); - } - continue; + /** + * Move client between heartbeat states. + * + * This function used when PubNub client changed its identity (`userId`) or auth (`access token`) and can't be + * aggregated with previous requests. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which should be moved to new state. + */ + moveClient(client) { + const state = this.heartbeatStateForClient(client); + const request = state ? state.requestForClient(client) : undefined; + if (!state || !request) + return; + this.removeClient(client); + this.addClient(client, request); + } + /** + * Add client-provided heartbeat request into heartbeat state for aggregation. + * + * @param client - Reference to the client which provided heartbeat request. + * @param request - Reference to the heartbeat request which should be used in aggregation. + */ + addClient(client, request) { + var _a; + const identifier = request.asIdentifier; + let state = this.heartbeatStates[identifier]; + if (!state) { + state = this.heartbeatStates[identifier] = new HeartbeatState(identifier); + state.interval = (_a = client.heartbeatInterval) !== null && _a !== void 0 ? _a : 0; + // Make sure to receive updates from heartbeat state. + this.addListenerForHeartbeatStateEvents(state); } - const serviceWorkerClientId = clientIds[client.clientIdentifier]; - const { request: clientRequest } = (_b = client.subscription) !== null && _b !== void 0 ? _b : {}; - let decidedRequest = clientRequest !== null && clientRequest !== void 0 ? clientRequest : request; - if (!isSubscribeRequest) - decidedRequest = request; - if (serviceWorkerClientId && decidedRequest) { - const payload = Object.assign(Object.assign({}, result), { clientIdentifier: client.clientIdentifier, identifier: decidedRequest.identifier, url: `${decidedRequest.origin}${decidedRequest.path}` }); - if (result.type === 'request-process-success' && client.workerLogVerbosity) - consoleLog({ messageType: 'network-response', message: transportResponse }, client); - else if (result.type === 'request-process-error' && client.workerLogVerbosity) { - const canceled = result.error ? result.error.type === 'TIMEOUT' || result.error.type === 'ABORTED' : false; - let details = result.error ? result.error.message : 'Unknown'; - if (payload.response) { - const contentType = payload.response.headers['content-type']; - if (payload.response.body && - contentType && - (contentType.indexOf('javascript') !== -1 || contentType.indexOf('json') !== -1)) { - try { - const serviceResponse = JSON.parse(new TextDecoder().decode(payload.response.body)); - if ('message' in serviceResponse) - details = serviceResponse.message; - else if ('error' in serviceResponse) { - if (typeof serviceResponse.error === 'string') - details = serviceResponse.error; - else if (typeof serviceResponse.error === 'object' && 'message' in serviceResponse.error) - details = serviceResponse.error.message; - } - } - catch (_) { } - } - if (details === 'Unknown') { - if (payload.response.status >= 500) - details = 'Internal Server Error'; - else if (payload.response.status == 400) - details = 'Bad request'; - else if (payload.response.status == 403) - details = 'Access denied'; - else - details = `${payload.response.status}`; - } + else if (client.heartbeatInterval && + state.interval > 0 && + client.heartbeatInterval > 0 && + client.heartbeatInterval < state.interval) + state.interval = client.heartbeatInterval; + state.addClientRequest(client, request); + } + /** + * Remove client and its requests from further aggregated heartbeat calls. + * + * @param client - Reference to the PubNub client which should be removed from heartbeat state. + */ + removeClient(client) { + const state = this.heartbeatStateForClient(client); + if (!state) + return; + state.removeClient(client); + } + // endregion + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + /** + * Listen for PubNub clients manager events which affects aggregated subscribe / heartbeat requests. + * + * @param clientsManager - Clients manager for which change in clients should be tracked. + */ + subscribeOnClientEvents(clientsManager) { + // Listen for new core PubNub client registrations. + clientsManager.addEventListener(PubNubClientsManagerEvent.Registered, (evt) => { + const { client } = evt; + // Keep track of the client's listener abort controller. + const abortController = new AbortController(); + this.clientAbortControllers[client.identifier] = abortController; + client.addEventListener(PubNubClientEvent.Disconnect, () => this.removeClient(client), { + signal: abortController.signal, + }); + client.addEventListener(PubNubClientEvent.IdentityChange, (event) => { + if (!(event instanceof PubNubClientIdentityChangeEvent)) + return; + // Make changes into state only if `userId` actually changed. + if (!!event.oldUserId !== !!event.newUserId || + (event.oldUserId && event.newUserId && event.newUserId !== event.oldUserId)) { + const state = this.heartbeatStateForClient(client); + const request = state ? state.requestForClient(client) : undefined; + if (request) + request.userId = event.newUserId; + this.moveClient(client); } - consoleLog({ - messageType: 'network-request', - message: request, - details, - canceled, - failed: !canceled, - }, client); - } - publishClientEvent(client, payload); - } - else if (!serviceWorkerClientId && workerLogVerbosity) { - // Notifying about client without Shared Worker's communication channel. - const message = `${client.clientIdentifier} doesn't have Shared Worker's communication channel. Don't notify about completion.`; - for (const nClient of clients) { - if (nClient.clientIdentifier !== client.clientIdentifier) - consoleLog(message, nClient); - } - } + }, { + signal: abortController.signal, + }); + client.addEventListener(PubNubClientEvent.AuthChange, (event) => { + if (!(event instanceof PubNubClientAuthChangeEvent)) + return; + const state = this.heartbeatStateForClient(client); + const request = state ? state.requestForClient(client) : undefined; + if (request) + request.accessToken = event.newAuth; + // Check whether the client should be moved to another state because of a permissions change or whether the + // same token with the same permissions should be used for the next requests. + if (!!event.oldAuth !== !!event.newAuth || + (event.oldAuth && event.newAuth && !event.newAuth.equalTo(event.oldAuth))) + this.moveClient(client); + }, { + signal: abortController.signal, + }); + client.addEventListener(PubNubClientEvent.HeartbeatIntervalChange, (evt) => { + var _a; + const event = evt; + const state = this.heartbeatStateForClient(client); + if (state) + state.interval = (_a = event.newInterval) !== null && _a !== void 0 ? _a : 0; + }, { signal: abortController.signal }); + client.addEventListener(PubNubClientEvent.SendHeartbeatRequest, (evt) => this.addClient(client, evt.request), { signal: abortController.signal }); + client.addEventListener(PubNubClientEvent.SendLeaveRequest, (evt) => { + const { request } = evt; + const state = this.heartbeatStateForClient(client); + if (!state) + return; + state.removeFromClientState(client, request.channels, request.channelGroups); + }, { signal: abortController.signal }); + }); + // Listen for core PubNub client module disappearance. + clientsManager.addEventListener(PubNubClientsManagerEvent.Unregistered, (evt) => { + const { client } = evt; + // Remove all listeners added for the client. + const abortController = this.clientAbortControllers[client.identifier]; + delete this.clientAbortControllers[client.identifier]; + if (abortController) + abortController.abort(); + this.removeClient(client); + }); } - }; - /** - * Create processing success event from service response. - * - * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each - * specific PubNub client state. - * - * @param res - Service response for used REST API endpoint along with response body. - * - * @returns Request processing success event object. - */ - const requestProcessingSuccess = (res) => { - var _a; - const [response, body] = res; - const responseBody = body.byteLength > 0 ? body : undefined; - const contentLength = parseInt((_a = response.headers.get('Content-Length')) !== null && _a !== void 0 ? _a : '0', 10); - const contentType = response.headers.get('Content-Type'); - const headers = {}; - // Copy Headers object content into plain Record. - response.headers.forEach((value, key) => (headers[key] = value.toLowerCase())); - return { - type: 'request-process-success', - clientIdentifier: '', - identifier: '', - url: '', - response: { - contentLength, - contentType, - headers, - status: response.status, - body: responseBody, - }, - }; - }; - /** - * Create processing error event from service response. - * - * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each - * specific PubNub client state. - * - * @param [error] - Client-side request processing error (for example network issues). - * @param [res] - Service error response (for example permissions error or malformed - * payload) along with service body. - * - * @returns Request processing error event object. - */ - const requestProcessingError = (error, res) => { - // Use service response as error information source. - if (res) { - return Object.assign(Object.assign({}, requestProcessingSuccess(res)), { type: 'request-process-error' }); - } - let type = 'NETWORK_ISSUE'; - let message = 'Unknown error'; - let name = 'Error'; - if (error && error instanceof Error) { - message = error.message; - name = error.name; - } - const errorMessage = message.toLowerCase(); - if (errorMessage.includes('timeout')) - type = 'TIMEOUT'; - else if (name === 'AbortError' || errorMessage.includes('aborted') || errorMessage.includes('cancel')) { - message = 'Request aborted'; - type = 'ABORTED'; - } - return { - type: 'request-process-error', - clientIdentifier: '', - identifier: '', - url: '', - error: { name, type, message }, - }; - }; - // endregion + /** + * Listen for heartbeat state events. + * + * @param state - Reference to the subscription object for which listeners should be added. + */ + addListenerForHeartbeatStateEvents(state) { + const abortController = new AbortController(); + state.addEventListener(HeartbeatStateEvent.Heartbeat, (evt) => { + const { request } = evt; + this.sendRequest(request, (fetchRequest, response) => request.handleProcessingSuccess(fetchRequest, response), (fetchRequest, error) => request.handleProcessingError(fetchRequest, error)); + }, { signal: abortController.signal }); + state.addEventListener(HeartbeatStateEvent.Invalidated, () => { + delete this.heartbeatStates[state.identifier]; + abortController.abort(); + }, { signal: abortController.signal, once: true }); + } + } // -------------------------------------------------------- - // ----------------------- Helpers ------------------------ + // ---------------------- Information --------------------- // -------------------------------------------------------- - // region Helpers + // region Information /** - * Register client if it didn't use Service Worker before. - * - * The registration process updates the Service Worker state with information about channels and groups in which - * particular PubNub clients are interested, and uses this information when another subscribe request is made to build - * shared requests. - * - * @param event - Base information about PubNub client instance and Service Worker {@link Client}. + * Service response binary data decoder. */ - const registerClientIfRequired = (event) => { - var _a, _b, _c; - var _d, _e; - const { clientIdentifier } = event; - if (pubNubClients[clientIdentifier]) - return; - const client = (pubNubClients[clientIdentifier] = { - clientIdentifier, - subscriptionKey: event.subscriptionKey, - userId: event.userId, - heartbeatInterval: event.heartbeatInterval, - newlyRegistered: true, - offlineClientsCheckInterval: event.workerOfflineClientsCheckInterval, - unsubscribeOfflineClients: event.workerUnsubscribeOfflineClients, - workerLogVerbosity: event.workerLogVerbosity, - }); - // Map registered PubNub client to its subscription key. - const clientsBySubscriptionKey = ((_a = pubNubClientsBySubscriptionKey[_d = event.subscriptionKey]) !== null && _a !== void 0 ? _a : (pubNubClientsBySubscriptionKey[_d] = [])); - if (clientsBySubscriptionKey.every((entry) => entry.clientIdentifier !== clientIdentifier)) - clientsBySubscriptionKey.push(client); - // Binding PubNub client to the MessagePort (receiver). - ((_b = sharedWorkerClients[_e = event.subscriptionKey]) !== null && _b !== void 0 ? _b : (sharedWorkerClients[_e] = {}))[clientIdentifier] = event.port; - const message = `Registered PubNub client with '${clientIdentifier}' identifier. ` + - `'${clientsBySubscriptionKey.length}' clients currently active.`; - for (const _client of clientsBySubscriptionKey) - consoleLog(message, _client); - if (!pingTimeouts[event.subscriptionKey] && - ((_c = pubNubClientsBySubscriptionKey[event.subscriptionKey]) !== null && _c !== void 0 ? _c : []).length > 0) { - const { subscriptionKey } = event; - const interval = event.workerOfflineClientsCheckInterval; - for (const _client of clientsBySubscriptionKey) - consoleLog(`Setup PubNub client ping event ${interval} seconds`, _client); - pingTimeouts[subscriptionKey] = setTimeout(() => pingClients(subscriptionKey), interval * 500 - 1); - } - }; + HeartbeatRequestsManager.textDecoder = new TextDecoder(); + /** - * Update configuration of previously registered PubNub client. - * - * @param event - Object with up-to-date client settings, which should be reflected in SharedWorker's state for the - * registered client. + * Enum with available log levels. */ - const updateClientInformation = (event) => { - var _a, _b, _c; - const { clientIdentifier, userId, heartbeatInterval, accessToken: authKey, preProcessedToken: token } = event; - const client = pubNubClients[clientIdentifier]; - // This should never happen. - if (!client) - return; - consoleDir({ userId, heartbeatInterval, authKey, token }, `Update client configuration:`, client); - // Check whether identity changed as part of configuration update or not. - if (userId !== client.userId || (authKey && authKey !== ((_a = client.authKey) !== null && _a !== void 0 ? _a : ''))) { - const _heartbeatRequests = (_b = serviceHeartbeatRequests[client.subscriptionKey]) !== null && _b !== void 0 ? _b : {}; - const heartbeatRequestKey = `${userId}_${(_c = clientAggregateAuthKey(client)) !== null && _c !== void 0 ? _c : ''}`; - // Clean up previous heartbeat aggregation data. - if (_heartbeatRequests[heartbeatRequestKey] !== undefined) - delete _heartbeatRequests[heartbeatRequestKey]; - } - const intervalChanged = client.heartbeatInterval !== heartbeatInterval; - // Updating client configuration. - client.userId = userId; - client.heartbeatInterval = heartbeatInterval; - if (authKey) - client.authKey = authKey; - if (token) - client.accessToken = token; - if (intervalChanged) - startHeartbeatTimer(client, true); - updateCachedRequestAuthKeys(client); - // Make immediate heartbeat call (if possible). - if (!client.heartbeat || !client.heartbeat.heartbeatEvent) - return; - handleHeartbeatRequestEvent(client.heartbeat.heartbeatEvent, false, true); - }; + var LogLevel; + (function (LogLevel) { + /** + * Used to notify about every last detail: + * - function calls, + * - full payloads, + * - internal variables, + * - state-machine hops. + */ + LogLevel[LogLevel["Trace"] = 0] = "Trace"; + /** + * Used to notify about broad strokes of your SDK’s logic: + * - inputs/outputs to public methods, + * - network request + * - network response + * - decision branches. + */ + LogLevel[LogLevel["Debug"] = 1] = "Debug"; + /** + * Used to notify summary of what the SDK is doing under the hood: + * - initialized, + * - connected, + * - entity created. + */ + LogLevel[LogLevel["Info"] = 2] = "Info"; + /** + * Used to notify about non-fatal events: + * - deprecations, + * - request retries. + */ + LogLevel[LogLevel["Warn"] = 3] = "Warn"; + /** + * Used to notify about: + * - exceptions, + * - HTTP failures, + * - invalid states. + */ + LogLevel[LogLevel["Error"] = 4] = "Error"; + /** + * Logging disabled. + */ + LogLevel[LogLevel["None"] = 5] = "None"; + })(LogLevel || (LogLevel = {})); + /** - * Unregister client if it uses Service Worker before. - * - * During registration removal client information will be removed from the Shared Worker and - * long-poll request will be cancelled if possible. - * - * @param event - Base information about PubNub client instance and Service Worker {@link Client}. + * Custom {@link Logger} implementation to send logs to the core PubNub client module from the shared worker context. */ - const unRegisterClient = (event) => { - invalidateClient(event.subscriptionKey, event.clientIdentifier); - }; + class ClientLogger { + /** + * Create logger for specific PubNub client representation object. + * + * @param minLogLevel - Minimum messages log level to be logged. + * @param port - Message port for two-way communication with core PunNub client module. + */ + constructor(minLogLevel, port) { + this.minLogLevel = minLogLevel; + this.port = port; + } + /** + * Process a `debug` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + debug(message) { + this.log(message, LogLevel.Debug); + } + /** + * Process a `error` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + error(message) { + this.log(message, LogLevel.Error); + } + /** + * Process an `info` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + info(message) { + this.log(message, LogLevel.Info); + } + /** + * Process a `trace` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + trace(message) { + this.log(message, LogLevel.Trace); + } + /** + * Process an `warn` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + warn(message) { + this.log(message, LogLevel.Warn); + } + /** + * Send log entry to the core PubNub client module. + * + * @param message - Object which should be sent to the core PubNub client module. + * @param level - Log entry level (will be handled by if core PunNub client module minimum log level matches). + */ + log(message, level) { + // Discard logged message if logger not enabled. + if (level < this.minLogLevel) + return; + let entry; + if (typeof message === 'string') + entry = { messageType: 'text', message }; + else if (typeof message === 'function') + entry = message(); + else + entry = message; + entry.level = level; + try { + this.port.postMessage({ type: 'shared-worker-console-log', message: entry }); + } + catch (error) { + if (this.minLogLevel !== LogLevel.None) + console.error(`[SharedWorker] Unable send message using message port: ${error}`); + } + } + } + /** - * Update information about previously registered client. - * - * Use information from request to populate list of channels and other useful information. - * - * @param event - Send request. - * @returns `true` if channels / groups list has been changed. May return `undefined` because `client` is missing. + * PubNub client representation in Shared Worker context. */ - const updateClientSubscribeStateIfRequired = (event) => { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; - var _m, _o, _p, _q, _r, _s, _t, _u, _v; - const query = event.request.queryParameters; - const { clientIdentifier } = event; - const client = pubNubClients[clientIdentifier]; - let changed = false; - // This should never happen. - if (!client) - return; - const channelGroupQuery = ((_a = query['channel-group']) !== null && _a !== void 0 ? _a : ''); - const state = ((_b = query.state) !== null && _b !== void 0 ? _b : ''); - let subscription = client.subscription; - if (!subscription) { - changed = true; - subscription = { - refreshTimestamp: 0, - path: '', - channelGroupQuery: '', - channels: [], - channelGroups: [], - previousTimetoken: '0', - timetoken: '0', - objectsWithState: [], - }; - if (state.length > 0) { - const parsedState = JSON.parse(state); - const userState = ((_d = (_o = ((_c = presenceState[_m = client.subscriptionKey]) !== null && _c !== void 0 ? _c : (presenceState[_m] = {})))[_p = client.userId]) !== null && _d !== void 0 ? _d : (_o[_p] = {})); - Object.entries(parsedState).forEach(([objectName, value]) => (userState[objectName] = value)); - subscription.objectsWithState = Object.keys(parsedState); - } - client.subscription = subscription; - } - else { - if (state.length > 0) { - const parsedState = JSON.parse(state); - const userState = ((_f = (_r = ((_e = presenceState[_q = client.subscriptionKey]) !== null && _e !== void 0 ? _e : (presenceState[_q] = {})))[_s = client.userId]) !== null && _f !== void 0 ? _f : (_r[_s] = {})); - Object.entries(parsedState).forEach(([objectName, value]) => (userState[objectName] = value)); - // Clean up state for objects where presence state has been reset. - for (const objectName of subscription.objectsWithState) - if (!parsedState[objectName]) - delete userState[objectName]; - subscription.objectsWithState = Object.keys(parsedState); + class PubNubClient extends EventTarget { + // endregion + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + /** + * Create PubNub client. + * + * @param identifier - Unique PubNub client identifier. + * @param subKey - Subscribe REST API access key. + * @param userId - Unique identifier of the user currently configured for the PubNub client. + * @param port - Message port for two-way communication with core PubNub client module. + * @param logLevel - Minimum messages log level which should be passed to the `Subscription` worker logger. + * @param [heartbeatInterval] - Interval that is used to announce a user's presence on channels/groups. + */ + constructor(identifier, subKey, userId, port, logLevel, heartbeatInterval) { + super(); + this.identifier = identifier; + this.subKey = subKey; + this.userId = userId; + this.port = port; + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + /** + * Map of ongoing PubNub client requests. + * + * Unique request identifiers mapped to the requests requested by the core PubNub client module. + */ + this.requests = {}; + /** + * Controller, which is used on PubNub client unregister event to clean up listeners. + */ + this.listenerAbortController = new AbortController(); + /** + * List of subscription channel groups after previous subscribe request. + * + * **Note:** Keep a local cache to reduce the amount of parsing with each received subscribe send request. + */ + this.cachedSubscriptionChannelGroups = []; + /** + * List of subscription channels after previous subscribe request. + * + * **Note:** Keep a local cache to reduce the amount of parsing with each received subscribe send request. + */ + this.cachedSubscriptionChannels = []; + /** + * Whether {@link PubNubClient|PubNub} client has been invalidated (unregistered) or not. + */ + this._invalidated = false; + this.logger = new ClientLogger(logLevel, this.port); + this._heartbeatInterval = heartbeatInterval; + this.subscribeOnEvents(); + } + /** + * Clean up resources used by this PubNub client. + */ + invalidate(dispatchEvent = false) { + // Remove the client's listeners. + this.listenerAbortController.abort(); + this._invalidated = true; + this.cancelRequests(); + } + // endregion + // -------------------------------------------------------- + // ---------------------- Properties ---------------------- + // -------------------------------------------------------- + // region Properties + /** + * Retrieve origin which is used to access PubNub REST API. + * + * @returns Origin which is used to access PubNub REST API. + */ + get origin() { + var _a; + return (_a = this._origin) !== null && _a !== void 0 ? _a : ''; + } + /** + * Retrieve heartbeat interval, which is used to announce a user's presence on channels/groups. + * + * @returns Heartbeat interval, which is used to announce a user's presence on channels/groups. + */ + get heartbeatInterval() { + return this._heartbeatInterval; + } + /** + * Retrieve an access token to have `read` access to resources used by this client. + * + * @returns Access token to have `read` access to resources used by this client. + */ + get accessToken() { + return this._accessToken; + } + /** + * Retrieve whether the {@link PubNubClient|PubNub} client has been invalidated (unregistered) or not. + * + * @returns `true` if the client has been invalidated during unregistration. + */ + get isInvalidated() { + return this._invalidated; + } + /** + * Retrieve the last time, the core PubNub client module responded with the `PONG` event. + * + * @returns Last time, the core PubNub client module responded with the `PONG` event. + */ + get lastPongEvent() { + return this._lastPongEvent; + } + // endregion + // -------------------------------------------------------- + // --------------------- Communication -------------------- + // -------------------------------------------------------- + // region Communication + /** + * Post event to the core PubNub client module. + * + * @param event - Subscription worker event payload. + * @returns `true` if the event has been sent without any issues. + */ + postEvent(event) { + try { + this.port.postMessage(event); + return true; } - // Handle potential presence state reset. - else if (subscription.objectsWithState.length) { - const userState = ((_h = (_u = ((_g = presenceState[_t = client.subscriptionKey]) !== null && _g !== void 0 ? _g : (presenceState[_t] = {})))[_v = client.userId]) !== null && _h !== void 0 ? _h : (_u[_v] = {})); - for (const objectName of subscription.objectsWithState) - delete userState[objectName]; - subscription.objectsWithState = []; + catch (error) { + this.logger.error(`Unable send message using message port: ${error}`); } + return false; } - if (subscription.path !== event.request.path) { - subscription.path = event.request.path; - const _channelsFromRequest = channelsFromRequest(event.request); - if (!changed) - changed = !includesStrings(subscription.channels, _channelsFromRequest); - subscription.channels = _channelsFromRequest; + // endregion + // -------------------------------------------------------- + // -------------------- Event handlers -------------------- + // -------------------------------------------------------- + // region Event handlers + /** + * Subscribe to client-specific signals from the core PubNub client module. + */ + subscribeOnEvents() { + this.port.addEventListener('message', (event) => { + if (event.data.type === 'client-unregister') + this.handleUnregisterEvent(); + else if (event.data.type === 'client-update') + this.handleConfigurationUpdateEvent(event.data); + else if (event.data.type === 'send-request') + this.handleSendRequestEvent(event.data); + else if (event.data.type === 'cancel-request') + this.handleCancelRequestEvent(event.data); + else if (event.data.type === 'client-disconnect') + this.handleDisconnectEvent(); + else if (event.data.type === 'client-pong') + this.handlePongEvent(); + }, { signal: this.listenerAbortController.signal }); } - if (subscription.channelGroupQuery !== channelGroupQuery) { - subscription.channelGroupQuery = channelGroupQuery; - const _channelGroupsFromRequest = channelGroupsFromRequest(event.request); - if (!changed) - changed = !includesStrings(subscription.channelGroups, _channelGroupsFromRequest); - subscription.channelGroups = _channelGroupsFromRequest; - } - let { authKey } = client; - subscription.refreshTimestamp = Date.now(); - subscription.request = event.request; - subscription.filterExpression = ((_j = query['filter-expr']) !== null && _j !== void 0 ? _j : ''); - subscription.timetoken = ((_k = query.tt) !== null && _k !== void 0 ? _k : '0'); - if (query.tr !== undefined) - subscription.region = query.tr; - client.authKey = ((_l = query.auth) !== null && _l !== void 0 ? _l : ''); - client.origin = event.request.origin; - client.userId = query.uuid; - client.pnsdk = query.pnsdk; - client.accessToken = event.preProcessedToken; - if (client.newlyRegistered && !authKey && client.authKey) - authKey = client.authKey; - client.newlyRegistered = false; - return changed; - }; - /** - * Update presence heartbeat information for previously registered client. - * - * Use information from request to populate list of channels / groups and presence state information. - * - * @param event - Send heartbeat request event. - */ - const updateClientHeartbeatState = (event) => { - var _a, _b, _c; - const { clientIdentifier } = event; - const client = pubNubClients[clientIdentifier]; - const { request } = event; - const query = (_a = request.queryParameters) !== null && _a !== void 0 ? _a : {}; - // This should never happen. - if (!client) - return; - const _clientHeartbeat = ((_b = client.heartbeat) !== null && _b !== void 0 ? _b : (client.heartbeat = { - channels: [], - channelGroups: [], - })); - _clientHeartbeat.heartbeatEvent = Object.assign({}, event); - // Update presence heartbeat information about client. - _clientHeartbeat.channelGroups = channelGroupsFromRequest(request).filter((group) => !group.endsWith('-pnpres')); - _clientHeartbeat.channels = channelsFromRequest(request).filter((channel) => !channel.endsWith('-pnpres')); - const state = ((_c = query.state) !== null && _c !== void 0 ? _c : ''); - if (state.length > 0) { - const userPresenceState = JSON.parse(state); - for (const objectName of Object.keys(userPresenceState)) - if (!_clientHeartbeat.channels.includes(objectName) && !_clientHeartbeat.channelGroups.includes(objectName)) - delete userPresenceState[objectName]; - _clientHeartbeat.presenceState = userPresenceState; - } - client.accessToken = event.preProcessedToken; - }; - /** - * Handle PubNub client response on PING request. - * - * @param event - Information about client which responded on PING request. - */ - const handleClientPong = (event) => { - const client = pubNubClients[event.clientIdentifier]; - if (!client) - return; - client.lastPongEvent = new Date().getTime() / 1000; - }; - /** - * Clean up resources used by registered PubNub client instance. - * - * @param subscriptionKey - Subscription key which has been used by the - * invalidated instance. - * @param clientId - Unique PubNub client identifier. - */ - const invalidateClient = (subscriptionKey, clientId) => { - var _a, _b, _c; - const invalidatedClient = pubNubClients[clientId]; - delete pubNubClients[clientId]; - let clients = pubNubClientsBySubscriptionKey[subscriptionKey]; - let serviceRequestId; - // Unsubscribe invalidated PubNub client. - if (invalidatedClient) { - // Cancel long-poll request if possible. - if (invalidatedClient.subscription) { - serviceRequestId = invalidatedClient.subscription.serviceRequestId; - delete invalidatedClient.subscription.serviceRequestId; - if (serviceRequestId) - cancelRequest(serviceRequestId); + /** + * Handle PubNub client unregister event. + * + * During unregister handling, the following changes will happen: + * - remove from the clients hash map ({@link PubNubClientsManager|clients manager}) + * - reset long-poll request (remove channels/groups that have been used only by this client) + * - stop backup heartbeat timer + */ + handleUnregisterEvent() { + this.invalidate(); + this.dispatchEvent(new PubNubClientUnregisterEvent(this)); + } + /** + * Update client's configuration. + * + * During configuration update handling, the following changes may happen (depending on the changed data): + * - reset long-poll request (remove channels/groups that have been used only by this client from active request) on + * `userID` change. + * - heartbeat will be sent immediately on `userID` change (to announce new user presence). **Note:** proper flow will + * be `unsubscribeAll` and then, with changed `userID` subscribe back, but the code will handle hard reset as well. + * - _backup_ heartbeat timer reschedule in on `heartbeatInterval` change. + * + * @param event - Object with up-to-date client settings, which should be reflected in SharedWorker's state for the + * registered client. + */ + handleConfigurationUpdateEvent(event) { + const { userId, accessToken: authKey, preProcessedToken: token, heartbeatInterval, workerLogLevel } = event; + this.logger.minLogLevel = workerLogLevel; + this.logger.debug(() => ({ + messageType: 'object', + message: { userId, authKey, token, heartbeatInterval, workerLogLevel }, + details: 'Update client configuration with parameters:', + })); + // Check whether authentication information has been changed or not. + // Important: If changed, this should be notified before a potential identity change event. + if (!!authKey || !!this.accessToken) { + const accessToken = authKey ? new AccessToken(authKey, (token !== null && token !== void 0 ? token : {}).token, (token !== null && token !== void 0 ? token : {}).expiration) : undefined; + // Check whether the access token really changed or not. + if (!!accessToken !== !!this.accessToken || + (!!accessToken && this.accessToken && !accessToken.equalTo(this.accessToken, true))) { + const oldValue = this._accessToken; + this._accessToken = accessToken; + // Make sure that all ongoing subscribe (usually should be only one at a time) requests use proper + // `accessToken`. + Object.values(this.requests) + .filter((request) => (!request.completed && request instanceof SubscribeRequest) || request instanceof HeartbeatRequest) + .forEach((request) => (request.accessToken = accessToken)); + this.dispatchEvent(new PubNubClientAuthChangeEvent(this, accessToken, oldValue)); + } } - // Make sure to stop heartbeat timer. - stopHeartbeatTimer(invalidatedClient); - if (serviceHeartbeatRequests[subscriptionKey]) { - const hbRequestsBySubscriptionKey = ((_a = serviceHeartbeatRequests[subscriptionKey]) !== null && _a !== void 0 ? _a : (serviceHeartbeatRequests[subscriptionKey] = {})); - const heartbeatRequestKey = `${invalidatedClient.userId}_${(_b = clientAggregateAuthKey(invalidatedClient)) !== null && _b !== void 0 ? _b : ''}`; - if (hbRequestsBySubscriptionKey[heartbeatRequestKey] && - hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier === invalidatedClient.clientIdentifier) - delete hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier; + // Check whether PubNub client identity has been changed or not. + if (this.userId !== userId) { + const oldValue = this.userId; + this.userId = userId; + // Make sure that all ongoing subscribe (usually should be only one at a time) requests use proper `userId`. + // **Note:** Core PubNub client module docs have a warning saying that `userId` should be changed only after + // unsubscribe/disconnect to properly update the user's presence. + Object.values(this.requests) + .filter((request) => (!request.completed && request instanceof SubscribeRequest) || request instanceof HeartbeatRequest) + .forEach((request) => (request.userId = userId)); + this.dispatchEvent(new PubNubClientIdentityChangeEvent(this, oldValue, userId)); } - // Leave subscribed channels / groups properly. - if (invalidatedClient.unsubscribeOfflineClients) - unsubscribeClient(invalidatedClient, serviceRequestId); - } - if (clients) { - // Clean up linkage between client and subscription key. - clients = clients.filter((client) => client.clientIdentifier !== clientId); - if (clients.length > 0) - pubNubClientsBySubscriptionKey[subscriptionKey] = clients; - else { - delete pubNubClientsBySubscriptionKey[subscriptionKey]; - delete serviceHeartbeatRequests[subscriptionKey]; + if (this._heartbeatInterval !== heartbeatInterval) { + const oldValue = this._heartbeatInterval; + this._heartbeatInterval = heartbeatInterval; + this.dispatchEvent(new PubNubClientHeartbeatIntervalChangeEvent(this, heartbeatInterval, oldValue)); + } + } + /** + * Handle requests send request from the core PubNub client module. + * + * @param data - Object with received request details. + */ + handleSendRequestEvent(data) { + var _a; + let request; + // Setup client's authentication token from request (if it hasn't been set yet) + if (!this._accessToken && !!((_a = data.request.queryParameters) === null || _a === void 0 ? void 0 : _a.auth) && !!data.preProcessedToken) { + const auth = data.request.queryParameters.auth; + this._accessToken = new AccessToken(auth, data.preProcessedToken.token, data.preProcessedToken.expiration); } - // Clean up presence state information if not in use anymore. - if (clients.length === 0) - delete presenceState[subscriptionKey]; - // Clean up service workers client linkage to PubNub clients. - if (clients.length > 0) { - const workerClients = sharedWorkerClients[subscriptionKey]; - if (workerClients) { - delete workerClients[clientId]; - if (Object.keys(workerClients).length === 0) - delete sharedWorkerClients[subscriptionKey]; + if (data.request.path.startsWith('/v2/subscribe')) { + if (SubscribeRequest.useCachedState(data.request) && + (this.cachedSubscriptionChannelGroups.length || this.cachedSubscriptionChannels.length)) { + request = SubscribeRequest.fromCachedState(data.request, this.subKey, this.cachedSubscriptionChannelGroups, this.cachedSubscriptionChannels, this.cachedSubscriptionState, this.accessToken); + } + else { + request = SubscribeRequest.fromTransportRequest(data.request, this.subKey, this.accessToken); + // Update the cached client's subscription state. + this.cachedSubscriptionChannelGroups = [...request.channelGroups]; + this.cachedSubscriptionChannels = [...request.channels]; + if (request.state) + this.cachedSubscriptionState = Object.assign({}, request.state); + else + this.cachedSubscriptionState = undefined; } } + else if (data.request.path.endsWith('/heartbeat')) + request = HeartbeatRequest.fromTransportRequest(data.request, this.subKey, this.accessToken); else - delete sharedWorkerClients[subscriptionKey]; - } - const message = `Invalidate '${clientId}' client. '${((_c = pubNubClientsBySubscriptionKey[subscriptionKey]) !== null && _c !== void 0 ? _c : []).length}' clients currently active.`; - if (!clients) - consoleLog(message); - else - for (const _client of clients) - consoleLog(message, _client); - }; - /** - * Unsubscribe offline / invalidated PubNub client. - * - * @param client - Invalidated PubNub client state object. - * @param [invalidatedClientServiceRequestId] - Identifier of the service request ID for which the invalidated - * client waited for a subscribe response. - */ - const unsubscribeClient = (client, invalidatedClientServiceRequestId) => { - if (!client.subscription) - return; - const { channels, channelGroups } = client.subscription; - const encodedChannelGroups = (channelGroups !== null && channelGroups !== void 0 ? channelGroups : []) - .filter((name) => !name.endsWith('-pnpres')) - .map((name) => encodeString(name)) - .sort(); - const encodedChannels = (channels !== null && channels !== void 0 ? channels : []) - .filter((name) => !name.endsWith('-pnpres')) - .map((name) => encodeString(name)) - .sort(); - if (encodedChannels.length === 0 && encodedChannelGroups.length === 0) - return; - const channelGroupsString = encodedChannelGroups.length > 0 ? encodedChannelGroups.join(',') : undefined; - const channelsString = encodedChannels.length === 0 ? ',' : encodedChannels.join(','); - const query = Object.assign(Object.assign({ instanceid: client.clientIdentifier, uuid: client.userId, requestid: uuidGenerator.createUUID() }, (client.authKey ? { auth: client.authKey } : {})), (channelGroupsString ? { 'channel-group': channelGroupsString } : {})); - const request = { - type: 'send-request', - clientIdentifier: client.clientIdentifier, - subscriptionKey: client.subscriptionKey, - request: { - origin: client.origin, - path: `/v2/presence/sub-key/${client.subscriptionKey}/channel/${channelsString}/leave`, - queryParameters: query, - method: TransportMethod.GET, - headers: {}, - timeout: 10, - cancellable: false, - compressible: false, - identifier: query.requestid, - }, - }; - handleSendLeaveRequestEvent(request, client, invalidatedClientServiceRequestId); - }; - /** - * Start presence heartbeat timer for periodic `heartbeat` API calls. - * - * @param client - Client state with information for heartbeat. - * @param [adjust] - Whether timer fire timer should be re-adjusted or not. - */ - const startHeartbeatTimer = (client, adjust = false) => { - const { heartbeat, heartbeatInterval } = client; - // Check whether there is a need to run "backup" heartbeat timer or not. - const shouldStart = heartbeatInterval && - heartbeatInterval > 0 && - heartbeat !== undefined && - heartbeat.heartbeatEvent && - (heartbeat.channels.length > 0 || heartbeat.channelGroups.length > 0); - if (!shouldStart) { - stopHeartbeatTimer(client); - return; - } - // Check whether there is active timer which should be re-adjusted or not. - if (adjust && !heartbeat.loop) - return; - let targetInterval = heartbeatInterval; - if (adjust && heartbeat.loop) { - const activeTime = (Date.now() - heartbeat.loop.startTimestamp) / 1000; - if (activeTime < targetInterval) - targetInterval -= activeTime; - if (targetInterval === heartbeat.loop.heartbeatInterval) - targetInterval += 0.05; + request = LeaveRequest.fromTransportRequest(data.request, this.subKey, this.accessToken); + request.client = this; + this.requests[request.request.identifier] = request; + if (!this._origin) + this._origin = request.origin; + // Set client state cleanup on request processing completion (with any outcome). + this.listenRequestCompletion(request); + // Notify request managers about new client-provided request. + this.dispatchEvent(this.eventWithRequest(request)); } - stopHeartbeatTimer(client); - if (targetInterval <= 0) - return; - heartbeat.loop = { - timer: setTimeout(() => { - stopHeartbeatTimer(client); - if (!client.heartbeat || !client.heartbeat.heartbeatEvent) - return; - // Generate new request ID - const { request } = client.heartbeat.heartbeatEvent; - request.identifier = uuidGenerator.createUUID(); - request.queryParameters.requestid = request.identifier; - handleHeartbeatRequestEvent(client.heartbeat.heartbeatEvent, false); - }, targetInterval * 1000), - heartbeatInterval, - startTimestamp: Date.now(), - }; - }; - /** - * Stop presence heartbeat timer before it will fire. - * - * @param client - Client state for which presence heartbeat timer should be stopped. - */ - const stopHeartbeatTimer = (client) => { - const { heartbeat } = client; - if (heartbeat === undefined || !heartbeat.loop) - return; - clearTimeout(heartbeat.loop.timer); - delete heartbeat.loop; - }; - /** - * Refresh authentication key stored in cached `subscribe` and `heartbeat` requests. - * - * @param client - Client state for which cached requests should be updated. - */ - const updateCachedRequestAuthKeys = (client) => { - var _a, _b; - var _c; - const { subscription, heartbeat } = client; - // Update `auth` query for cached subscribe request (if required). - if (subscription && subscription.request && subscription.request.queryParameters) { - const query = subscription.request.queryParameters; - if (client.authKey && client.authKey.length > 0) - query.auth = client.authKey; - else if (query.auth) - delete query.auth; - } - // Update `auth` query for cached heartbeat request (if required). - if ((heartbeat === null || heartbeat === void 0 ? void 0 : heartbeat.heartbeatEvent) && heartbeat.heartbeatEvent.request) { - if (client.accessToken) - heartbeat.heartbeatEvent.preProcessedToken = client.accessToken; - const hbRequestsBySubscriptionKey = ((_a = serviceHeartbeatRequests[_c = client.subscriptionKey]) !== null && _a !== void 0 ? _a : (serviceHeartbeatRequests[_c] = {})); - const heartbeatRequestKey = `${client.userId}_${(_b = clientAggregateAuthKey(client)) !== null && _b !== void 0 ? _b : ''}`; - if (hbRequestsBySubscriptionKey[heartbeatRequestKey] && hbRequestsBySubscriptionKey[heartbeatRequestKey].response) - delete hbRequestsBySubscriptionKey[heartbeatRequestKey].response; - // Generate new request ID - heartbeat.heartbeatEvent.request.identifier = uuidGenerator.createUUID(); - const query = heartbeat.heartbeatEvent.request.queryParameters; - query.requestid = heartbeat.heartbeatEvent.request.identifier; - if (client.authKey && client.authKey.length > 0) - query.auth = client.authKey; - else if (query.auth) - delete query.auth; + /** + * Handle on-demand request cancellation. + * + * **Note:** Cancellation will dispatch the event handled in `listenRequestCompletion` and remove target request from + * the PubNub client requests' list. + * + * @param data - Object with canceled request information. + */ + handleCancelRequestEvent(data) { + if (!this.requests[data.identifier]) + return; + const request = this.requests[data.identifier]; + request.cancel('Cancel request'); } - }; - /** - * Validate received event payload. - */ - const validateEventPayload = (event) => { - const { clientIdentifier, subscriptionKey } = event.data; - if (!clientIdentifier || typeof clientIdentifier !== 'string') - return false; - return !(!subscriptionKey || typeof subscriptionKey !== 'string'); - }; + /** + * Handle PubNub client disconnect event. + * + * **Note:** On disconnect, the core {@link PubNubClient|PubNub} client module will terminate `client`-provided + * subscribe requests ({@link handleCancelRequestEvent} will be called). + * + * During disconnection handling, the following changes will happen: + * - reset subscription state ({@link SubscribeRequestsManager|subscription requests manager}) + * - stop backup heartbeat timer + * - reset heartbeat state ({@link HeartbeatRequestsManager|heartbeat requests manager}) + */ + handleDisconnectEvent() { + this.dispatchEvent(new PubNubClientDisconnectEvent(this)); + } + /** + * Handle ping-pong response from the core PubNub client module. + */ + handlePongEvent() { + this._lastPongEvent = Date.now() / 1000; + } + /** + * Listen for any request outcome to clean + * + * @param request - Request for which processing outcome should be observed. + */ + listenRequestCompletion(request) { + const ac = new AbortController(); + const callback = (evt) => { + delete this.requests[request.identifier]; + ac.abort(); + if (evt instanceof RequestSuccessEvent) + this.postEvent(evt.response); + else if (evt instanceof RequestErrorEvent) + this.postEvent(evt.error); + else if (evt instanceof RequestCancelEvent) { + this.postEvent(this.requestCancelError(request)); + // Notify specifically about the `subscribe` request cancellation. + if (!this._invalidated && request instanceof SubscribeRequest) + this.dispatchEvent(new PubNubClientCancelSubscribeEvent(request.client, request)); + } + }; + request.addEventListener(PubNubSharedWorkerRequestEvents.Success, callback, { signal: ac.signal, once: true }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Error, callback, { signal: ac.signal, once: true }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Canceled, callback, { signal: ac.signal, once: true }); + } + // endregion + // -------------------------------------------------------- + // ----------------------- Requests ----------------------- + // -------------------------------------------------------- + // region Requests + /** + * Cancel any active `client`-provided requests. + * + * **Note:** Cancellation will dispatch the event handled in `listenRequestCompletion` and remove `request` from the + * PubNub client requests' list. + */ + cancelRequests() { + Object.values(this.requests).forEach((request) => request.cancel()); + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Wrap `request` into corresponding event for dispatching. + * + * @param request - Request which should be used to identify event type and stored in it. + */ + eventWithRequest(request) { + let event; + if (request instanceof SubscribeRequest) + event = new PubNubClientSendSubscribeEvent(this, request); + else if (request instanceof HeartbeatRequest) + event = new PubNubClientSendHeartbeatEvent(this, request); + else + event = new PubNubClientSendLeaveEvent(this, request); + return event; + } + /** + * Create request cancellation response. + * + * @param request - Reference on client-provided request for which payload should be prepared. + * @returns Object which will be treated as cancel response on core PubNub client module side. + */ + requestCancelError(request) { + return { + type: 'request-process-error', + clientIdentifier: this.identifier, + identifier: request.request.identifier, + url: request.asFetchRequest.url, + error: { name: 'AbortError', type: 'ABORTED', message: 'Request aborted' }, + }; + } + } + /** - * Search for active subscription for one of the passed {@link sharedWorkerClients}. + * Registered {@link PubNubClient|PubNub} client instances manager. * - * @param activeClients - List of suitable registered PubNub clients. - * @param event - Send Subscriber Request event data. - * - * @returns Unique identifier of the active request which will receive real-time updates for channels and groups - * requested in received subscription request or `undefined` if none of active (or not scheduled) request can be used. + * Manager responsible for keeping track and interaction with registered {@link PubNubClient|PubNub}. */ - const activeSubscriptionForEvent = (activeClients, event) => { - var _a; - const query = event.request.queryParameters; - const channelGroupQuery = ((_a = query['channel-group']) !== null && _a !== void 0 ? _a : ''); - const requestPath = event.request.path; - let channelGroups; - let channels; - for (const client of activeClients) { - const { subscription } = client; - // Skip PubNub clients which doesn't await for subscription response. - if (!subscription || !subscription.serviceRequestId) - continue; - const sourceClient = pubNubClients[event.clientIdentifier]; - const requestId = subscription.serviceRequestId; - if (subscription.path === requestPath && subscription.channelGroupQuery === channelGroupQuery) { - consoleLog(`Found identical request started by '${client.clientIdentifier}' client. -Waiting for existing '${requestId}' request completion.`, sourceClient); - return subscription.serviceRequestId; - } - else { - const scheduledRequest = serviceRequests[subscription.serviceRequestId]; - if (!channelGroups) - channelGroups = channelGroupsFromRequest(event.request); - if (!channels) - channels = channelsFromRequest(event.request); - // Checking whether all required channels and groups are handled already by active request or not. - if (channels.length && !includesStrings(scheduledRequest.channels, channels)) - continue; - if (channelGroups.length && !includesStrings(scheduledRequest.channelGroups, channelGroups)) - continue; - consoleDir(scheduledRequest, `'${event.request.identifier}' request channels and groups are subset of ongoing '${requestId}' request -which has started by '${client.clientIdentifier}' client. Waiting for existing '${requestId}' request completion.`, sourceClient); - return subscription.serviceRequestId; + class PubNubClientsManager extends EventTarget { + // endregion + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + /** + * Create {@link PubNubClient|PubNub} clients manager. + * + * @param sharedWorkerIdentifier - Unique `Subscription` worker identifier that will work with clients. + */ + constructor(sharedWorkerIdentifier) { + super(); + this.sharedWorkerIdentifier = sharedWorkerIdentifier; + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + /** + * Map of started `PING` timeouts per subscription key. + */ + this.timeouts = {}; + /** + * Map of previously created {@link PubNubClient|PubNub} clients. + */ + this.clients = {}; + /** + * Map of previously created {@link PubNubClient|PubNub} clients to the corresponding subscription key. + */ + this.clientBySubscribeKey = {}; + } + // endregion + // -------------------------------------------------------- + // ----------------- Client registration ------------------ + // -------------------------------------------------------- + // region Client registration + /** + * Create {@link PubNubClient|PubNub} client. + * + * Function called in response to the `client-register` from the core {@link PubNubClient|PubNub} client module. + * + * @param event - Registration event with base {@link PubNubClient|PubNub} client information. + * @param port - Message port for two-way communication with core {@link PubNubClient|PubNub} client module. + * @returns New {@link PubNubClient|PubNub} client or existing one from the cache. + */ + createClient(event, port) { + var _a; + if (this.clients[event.clientIdentifier]) + return this.clients[event.clientIdentifier]; + const client = new PubNubClient(event.clientIdentifier, event.subscriptionKey, event.userId, port, event.workerLogLevel, event.heartbeatInterval); + this.registerClient(client); + // Start offline PubNub clients checks (ping-pong). + if (event.workerOfflineClientsCheckInterval) { + this.startClientTimeoutCheck(event.subscriptionKey, event.workerOfflineClientsCheckInterval, (_a = event.workerUnsubscribeOfflineClients) !== null && _a !== void 0 ? _a : false); } + return client; } - return undefined; - }; - /** - * Find PubNub client states with configuration compatible with the one in request. - * - * Method allow to find information about all PubNub client instances which use same: - * - subscription key - * - `userId` - * - `auth` key - * - `filter expression` - * - `timetoken` (compare should be done against previous timetoken of the client which requested new subscribe). - * - * @param timetoken - Previous timetoken used by the PubNub client which requested to send new subscription request - * (it will be the same as 'current' timetoken of the other PubNub clients). - * @param event - Send subscribe request event information. - * - * @returns List of PubNub client states which works from other pages for the same user. - */ - const clientsForSendSubscribeRequestEvent = (timetoken, event) => { - var _a, _b; - const reqClient = pubNubClients[event.clientIdentifier]; - if (!reqClient) - return []; - const query = event.request.queryParameters; - const authKey = clientAggregateAuthKey(reqClient); - const filterExpression = ((_a = query['filter-expr']) !== null && _a !== void 0 ? _a : ''); - const userId = query.uuid; - return ((_b = pubNubClientsBySubscriptionKey[event.subscriptionKey]) !== null && _b !== void 0 ? _b : []).filter((client) => client.userId === userId && - clientAggregateAuthKey(client) === authKey && - client.subscription && - // Only clients with active subscription can be used. - (client.subscription.channels.length !== 0 || client.subscription.channelGroups.length !== 0) && - client.subscription.filterExpression === filterExpression && - (timetoken === '0' || client.subscription.timetoken === '0' || client.subscription.timetoken === timetoken)); - }; - /** - * Find PubNub client state with configuration compatible with toe one in request. - * - * Method allow to find information about all PubNub client instances which use same: - * - subscription key - * - `userId` - * - `auth` key - * - * @param event - Send heartbeat request event information. - * - * @returns List of PubNub client states which works from other pages for the same user. - */ - const clientsForSendHeartbeatRequestEvent = (event) => { - return clientsForSendLeaveRequestEvent(event); - }; - /** - * Find PubNub client states with configuration compatible with the one in request. - * - * Method allow to find information about all PubNub client instances which use same: - * - subscription key - * - `userId` - * - `auth` key - * - * @param event - Send leave request event information. - * @param [invalidatedClient] - Invalidated PubNub client state. - * - * @returns List of PubNub client states which works from other pages for the same user. - */ - const clientsForSendLeaveRequestEvent = (event, invalidatedClient) => { - var _a; - const reqClient = invalidatedClient !== null && invalidatedClient !== void 0 ? invalidatedClient : pubNubClients[event.clientIdentifier]; - if (!reqClient) - return []; - const query = event.request.queryParameters; - const authKey = clientAggregateAuthKey(reqClient); - const userId = query.uuid; - return ((_a = pubNubClientsBySubscriptionKey[event.subscriptionKey]) !== null && _a !== void 0 ? _a : []).filter((client) => client.userId === userId && clientAggregateAuthKey(client) === authKey); - }; - /** - * Extract list of channels from request URI path. - * - * @param request - Transport request which should provide `path` for parsing. - * - * @returns List of channel names (not percent-decoded) for which `subscribe` or `leave` has been called. - */ - const channelsFromRequest = (request) => { - const channels = request.path.split('/')[request.path.startsWith('/v2/subscribe/') ? 4 : 6]; - return channels === ',' ? [] : channels.split(',').filter((name) => name.length > 0); - }; - /** - * Extract list of channel groups from request query. - * - * @param request - Transport request which should provide `query` for parsing. - * - * @returns List of channel group names (not percent-decoded) for which `subscribe` or `leave` has been called. - */ - const channelGroupsFromRequest = (request) => { - var _a; - const group = ((_a = request.queryParameters['channel-group']) !== null && _a !== void 0 ? _a : ''); - return group.length === 0 ? [] : group.split(',').filter((name) => name.length > 0); - }; - /** - * Check whether {@link main} array contains all entries from {@link sub} array. - * - * @param main - Main array with which `intersection` with {@link sub} should be checked. - * @param sub - Sub-array whose values should be checked in {@link main}. - * - * @returns `true` if all entries from {@link sub} is present in {@link main}. - */ - const includesStrings = (main, sub) => { - const set = new Set(main); - return sub.every(set.has, set); - }; - /** - * Send PubNub client PING request to identify disconnected instances. - * - * @param subscriptionKey - Subscribe key for which offline PubNub client should be checked. - */ - const pingClients = (subscriptionKey) => { - const payload = { type: 'shared-worker-ping' }; - const _pubNubClients = Object.values(pubNubClients).filter((client) => client && client.subscriptionKey === subscriptionKey); - _pubNubClients.forEach((client) => { - let clientInvalidated = false; - if (client && client.lastPingRequest) { - const interval = client.offlineClientsCheckInterval; - // Check whether client never respond or last response was too long time ago. - if (!client.lastPongEvent || Math.abs(client.lastPongEvent - client.lastPingRequest) > interval * 0.5) { - clientInvalidated = true; - for (const _client of _pubNubClients) - consoleLog(`'${client.clientIdentifier}' client is inactive. Invalidating...`, _client); - invalidateClient(client.subscriptionKey, client.clientIdentifier); + /** + * Store {@link PubNubClient|PubNub} client in manager's internal state. + * + * @param client - Freshly created {@link PubNubClient|PubNub} client which should be registered. + */ + registerClient(client) { + this.clients[client.identifier] = { client, abortController: new AbortController() }; + // Associate client with subscription key. + if (!this.clientBySubscribeKey[client.subKey]) + this.clientBySubscribeKey[client.subKey] = [client]; + else + this.clientBySubscribeKey[client.subKey].push(client); + this.forEachClient(client.subKey, (subKeyClient) => subKeyClient.logger.debug(`'${client.identifier}' client registered with '${this.sharedWorkerIdentifier}' shared worker (${this.clientBySubscribeKey[client.subKey].length} active clients).`)); + this.subscribeOnClientEvents(client); + this.dispatchEvent(new PubNubClientManagerRegisterEvent(client)); + } + /** + * Remove {@link PubNubClient|PubNub} client from manager's internal state. + * + * @param client - Previously created {@link PubNubClient|PubNub} client which should be removed. + * @param [withLeave=false] - Whether `leave` request should be sent or not. + * @param [onClientInvalidation=false] - Whether client removal caused by its invalidation (event from the + * {@link PubNubClient|PubNub} client) or as result of timeout check. + */ + unregisterClient(client, withLeave = false, onClientInvalidation = false) { + if (!this.clients[client.identifier]) + return; + // Make sure to detach all listeners for this `client`. + if (this.clients[client.identifier].abortController) + this.clients[client.identifier].abortController.abort(); + delete this.clients[client.identifier]; + const clientsBySubscribeKey = this.clientBySubscribeKey[client.subKey]; + if (clientsBySubscribeKey) { + const clientIdx = clientsBySubscribeKey.indexOf(client); + clientsBySubscribeKey.splice(clientIdx, 1); + if (clientsBySubscribeKey.length === 0) { + delete this.clientBySubscribeKey[client.subKey]; + this.stopClientTimeoutCheck(client); } } - if (client && !clientInvalidated) { - client.lastPingRequest = new Date().getTime() / 1000; - publishClientEvent(client, payload); - } - }); - // Restart ping timer if there is still active PubNub clients for subscription key. - if (_pubNubClients && _pubNubClients.length > 0 && _pubNubClients[0]) { - const interval = _pubNubClients[0].offlineClientsCheckInterval; - pingTimeouts[subscriptionKey] = setTimeout(() => pingClients(subscriptionKey), interval * 500 - 1); + this.forEachClient(client.subKey, (subKeyClient) => subKeyClient.logger.debug(`'${this.sharedWorkerIdentifier}' shared worker unregistered '${client.identifier}' client (${this.clientBySubscribeKey[client.subKey].length} active clients).`)); + if (!onClientInvalidation) + client.invalidate(); + this.dispatchEvent(new PubNubClientManagerUnregisterEvent(client, withLeave)); } - }; - /** - * Retrieve auth key which is suitable for common clients request aggregation. - * - * @param client - Client for which auth key for aggregation should be retrieved. - * - * @returns Client aggregation auth key. - */ - const clientAggregateAuthKey = (client) => { - var _a; - return client.accessToken ? ((_a = client.accessToken.token) !== null && _a !== void 0 ? _a : client.authKey) : client.authKey; - }; + // endregion + // -------------------------------------------------------- + // ----------------- Availability check ------------------- + // -------------------------------------------------------- + // region Availability check + /** + * Start timer for _timeout_ {@link PubNubClient|PubNub} client checks. + * + * @param subKey - Subscription key to get list of {@link PubNubClient|PubNub} clients that should be checked. + * @param interval - Interval at which _timeout_ check should be done. + * @param unsubscribeOffline - Whether _timeout_ (or _offline_) {@link PubNubClient|PubNub} clients should send + * `leave` request before invalidation or not. + */ + startClientTimeoutCheck(subKey, interval, unsubscribeOffline) { + if (this.timeouts[subKey]) + return; + this.forEachClient(subKey, (client) => client.logger.debug(`Setup PubNub client ping for every ${interval} seconds.`)); + this.timeouts[subKey] = { + interval, + unsubscribeOffline, + timeout: setTimeout(() => this.handleTimeoutCheck(subKey), interval * 500 - 1), + }; + } + /** + * Stop _timeout_ (or _offline_) {@link PubNubClient|PubNub} clients pinging. + * + * **Note:** This method is used only when all clients for a specific subscription key have been unregistered. + * + * @param client - {@link PubNubClient|PubNub} client with which the last client related by subscription key has been + * removed. + */ + stopClientTimeoutCheck(client) { + if (!this.timeouts[client.subKey]) + return; + if (this.timeouts[client.subKey].timeout) + clearTimeout(this.timeouts[client.subKey].timeout); + delete this.timeouts[client.subKey]; + } + /** + * Handle periodic {@link PubNubClient|PubNub} client timeout checks. + * + * @param subKey - Subscription key to get list of {@link PubNubClient|PubNub} clients that should be checked. + */ + handleTimeoutCheck(subKey) { + if (!this.timeouts[subKey]) + return; + const interval = this.timeouts[subKey].interval; + [...this.clientBySubscribeKey[subKey]].forEach((client) => { + // Handle potential SharedWorker timers throttling and early eviction of the PubNub core client. + // If timer fired later than specified interval - it has been throttled and shouldn't unregister client. + if (client.lastPingRequest && Date.now() / 1000 - client.lastPingRequest - 0.2 > interval * 0.5) { + client.logger.warn('PubNub clients timeout timer fired after throttling past due time.'); + client.lastPingRequest = undefined; + } + if (client.lastPingRequest && + (!client.lastPongEvent || Math.abs(client.lastPongEvent - client.lastPingRequest) > interval)) { + this.unregisterClient(client, this.timeouts[subKey].unsubscribeOffline); + // Notify other clients with same subscription key that one of them became inactive. + this.forEachClient(subKey, (subKeyClient) => { + if (subKeyClient.identifier !== client.identifier) + subKeyClient.logger.debug(`'${client.identifier}' client is inactive. Invalidating...`); + }); + } + if (this.clients[client.identifier]) { + client.lastPingRequest = Date.now() / 1000; + client.postEvent({ type: 'shared-worker-ping' }); + } + }); + // Restart PubNub clients timeout check timer. + if (this.timeouts[subKey]) + this.timeouts[subKey].timeout = setTimeout(() => this.handleTimeoutCheck(subKey), interval * 500); + } + // endregion + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + /** + * Listen for {@link PubNubClient|PubNub} client events that affect aggregated subscribe/heartbeat requests. + * + * @param client - {@link PubNubClient|PubNub} client for which event should be listened. + */ + subscribeOnClientEvents(client) { + client.addEventListener(PubNubClientEvent.Unregister, () => this.unregisterClient(client, this.timeouts[client.subKey] ? this.timeouts[client.subKey].unsubscribeOffline : false, true), { signal: this.clients[client.identifier].abortController.signal, once: true }); + } + // endregion + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + /** + * Call callback function for all {@link PubNubClient|PubNub} clients that have similar `subscribeKey`. + * + * @param subKey - Subscription key for which list of clients should be retrieved. + * @param callback - Function that will be called for each client list entry. + */ + forEachClient(subKey, callback) { + if (!this.clientBySubscribeKey[subKey]) + return; + this.clientBySubscribeKey[subKey].forEach(callback); + } + } + + /// /** - * Pick auth key for clients with latest expiration date. + * Subscription Service Worker Transport provider. * - * @param clients - List of clients for which latest auth key should be retrieved. + * Service worker provides support for PubNub subscription feature to give better user experience across + * multiple opened pages. * - * @returns Access token which can be used to confirm `userId` permissions for aggregated request. + * @internal */ - const authKeyForAggregatedClientsRequest = (clients) => { - const latestClient = clients - .filter((client) => !!client.accessToken) - .sort((a, b) => a.accessToken.expiration - b.accessToken.expiration) - .pop(); - return latestClient ? latestClient.authKey : undefined; - }; /** - * Compose clients' aggregation key. - * - * Aggregation key includes key parameters which differentiate clients between each other. - * - * @param client - Client for which identifier should be composed. - * - * @returns Aggregation timeout identifier string. + * Unique shared worker instance identifier. */ - const aggregateTimerId = (client) => { - const authKey = clientAggregateAuthKey(client); - let id = `${client.userId}-${client.subscriptionKey}${authKey ? `-${authKey}` : ''}`; - if (client.subscription && client.subscription.filterExpression) - id += `-${client.subscription.filterExpression}`; - return id; - }; + const sharedWorkerIdentifier = uuidGenerator.createUUID(); + const clientsManager = new PubNubClientsManager(sharedWorkerIdentifier); + new SubscribeRequestsManager(clientsManager); + new HeartbeatRequestsManager(clientsManager); + // endregion + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event Handlers /** - * Print message on the worker's clients console. + * Handle new PubNub client 'connection'. * - * @param message - Message which should be printed. - * @param [client] - Target client to which log message should be sent. - */ - const consoleLog = (message, client) => { - const clients = (client ? [client] : Object.values(pubNubClients)).filter((client) => client && client.workerLogVerbosity); - const payload = { - type: 'shared-worker-console-log', - message, - }; - clients.forEach((client) => { - if (client) - publishClientEvent(client, payload); - }); - }; - /** - * Print message on the worker's clients console. + * Echo listeners to let `SharedWorker` users that it is ready. * - * @param data - Data which should be printed into the console. - * @param [message] - Message which should be printed before {@link data}. - * @param [client] - Target client to which log message should be sent. + * @param event - Remote `SharedWorker` client connection event. */ - const consoleDir = (data, message, client) => { - const clients = (client ? [client] : Object.values(pubNubClients)).filter((client) => client && client.workerLogVerbosity); - const payload = { - type: 'shared-worker-console-dir', - message, - data, - }; - clients.forEach((client) => { - if (client) - publishClientEvent(client, payload); + self.onconnect = (event) => { + event.ports.forEach((receiver) => { + receiver.start(); + receiver.onmessage = (event) => { + const data = event.data; + if (data.type === 'client-register') + clientsManager.createClient(data, receiver); + }; + receiver.postMessage({ type: 'shared-worker-connected' }); }); }; - /** - * Stringify request query key / value pairs. - * - * @param query - Request query object. - * - * @returns Stringified query object. - */ - const queryStringFromObject = (query) => { - return Object.keys(query) - .map((key) => { - const queryValue = query[key]; - if (!Array.isArray(queryValue)) - return `${key}=${encodeString(queryValue)}`; - return queryValue.map((value) => `${key}=${encodeString(value)}`).join('&'); - }) - .join('&'); - }; - /** - * Percent-encode input string. - * - * **Note:** Encode content in accordance of the `PubNub` service requirements. - * - * @param input - Source string or number for encoding. - * - * @returns Percent-encoded string. - */ - const encodeString = (input) => { - return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); - }; - // endregion // endregion })); diff --git a/dist/web/pubnub.worker.min.js b/dist/web/pubnub.worker.min.js index 75c5a2b51..8bcd60169 100644 --- a/dist/web/pubnub.worker.min.js +++ b/dist/web/pubnub.worker.min.js @@ -1,2 +1,2 @@ -!function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";function e(e,t,n,r){return new(n||(n=Promise))((function(s,i){function o(e){try{c(r.next(e))}catch(e){i(e)}}function a(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){var t;e.done?s(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}c((r=r.apply(e,t||[])).next())}))}var t;"function"==typeof SuppressedError&&SuppressedError,function(e){e.GET="GET",e.POST="POST",e.PATCH="PATCH",e.DELETE="DELETE",e.LOCAL="LOCAL"}(t||(t={}));"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function n(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var r,s,i={exports:{}}; -/*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */r=i,function(e){var t="0.1.0",n={3:/^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,4:/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,5:/^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,all:/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i};function r(){var e,t,n="";for(e=0;e<32;e++)t=16*Math.random()|0,8!==e&&12!==e&&16!==e&&20!==e||(n+="-"),n+=(12===e?4:16===e?3&t|8:t).toString(16);return n}function s(e,t){var r=n[t||"all"];return r&&r.test(e)||!1}r.isUUID=s,r.VERSION=t,e.uuid=r,e.isUUID=s}(s=i.exports),null!==r&&(r.exports=s.uuid);var o=n(i.exports),a={createUUID:()=>o.uuid?o.uuid():o()};const c=new Map,l={},u=a.createUUID(),d=new Map,h={},f={},p={},b={},g={},v={};self.onconnect=e=>{ae("New PubNub Client connected to the Subscription Shared Worker."),e.ports.forEach((e=>{e.start(),e.onmessage=t=>{if(!H(t))return;const n=t.data;if("client-register"===n.type)n.port=e,x(n),ae(`Client '${n.clientIdentifier}' registered with '${u}' shared worker`);else if("client-update"===n.type)W(n);else if("client-unregister"===n.type)D(n);else if("client-pong"===n.type)M(n);else if("send-request"===n.type)if(n.request.path.startsWith("/v2/subscribe")){const e=F(n),t=h[n.clientIdentifier];if(t){const r=oe(t);let s=[];if(c.has(r)&&(s=c.get(r)[0]),s.push([t,n]),c.has(r)&&e&&(clearTimeout(c.get(r)[1]),c.delete(r)),!c.has(r)){const e=setTimeout((()=>{y(s,n),c.delete(r)}),50);c.set(r,[s,e])}}}else n.request.path.endsWith("/heartbeat")?(N(n),I(n)):k(n);else"cancel-request"===n.type&&w(n)},e.postMessage({type:"shared-worker-connected"})}))};const y=(e,t)=>{const n=S(t),r=h[t.clientIdentifier];r&&(e=e.filter((e=>e[0].clientIdentifier!==r.clientIdentifier)),q(r,t,n,!0),e.forEach((([e,t])=>q(e,t,n,!1))))},q=(e,t,n,r)=>{var s;let i=!1;if(r||"string"==typeof n||(n=n.identifier),e.subscription&&(i="0"===e.subscription.timetoken),"string"==typeof n){const r=v[n];if(e){if(e.subscription&&(e.subscription.refreshTimestamp=Date.now(),e.subscription.timetoken=r.timetoken,e.subscription.region=r.region,e.subscription.serviceRequestId=n),!i)return;const o=(new TextEncoder).encode(`{"t":{"t":"${r.timetoken}","r":${null!==(s=r.region)&&void 0!==s?s:"0"}},"m":[]}`),a=new Headers({"Content-Type":'text/javascript; charset="UTF-8"',"Content-Length":`${o.length}`}),c=new Response(o,{status:200,headers:a}),l=L([c,o]);l.url=`${t.request.origin}${t.request.path}`,l.clientIdentifier=t.clientIdentifier,l.identifier=t.request.identifier,R(e,l)}return}t.request.cancellable&&d.set(n.identifier,new AbortController);const o=v[n.identifier],{timetokenOverride:a,regionOverride:c}=o,l="0"===o.timetoken;ae(`'${Object.keys(v).length}' subscription request currently active.`);for(const e of O(n.identifier))ae({messageType:"network-request",message:n},e);T(n,(()=>O(n.identifier)),((e,r,s)=>{C(e,r,s,t.request),P(e,n.identifier)}),((e,r,s)=>{C(e,r,null,t.request,U(s)),P(e,n.identifier)}),(e=>{let t=e;return l&&a&&"0"!==a&&(t=m(t,a,c)),t}))},m=(e,t,n)=>{if(void 0===t||"0"===t||e[0].status>=400)return e;let r;const s=e[0];let i=s,o=e[1];try{r=JSON.parse((new TextDecoder).decode(o))}catch(t){return ae(`Subscribe response parse error: ${t}`),e}r.t.t=t,n&&(r.t.r=parseInt(n,10));try{if(o=(new TextEncoder).encode(JSON.stringify(r)).buffer,o.byteLength){const e=new Headers(s.headers);e.set("Content-Length",`${o.byteLength}`),i=new Response(o,{status:s.status,statusText:s.statusText,headers:e})}}catch(t){return ae(`Subscribe serialization error: ${t}`),e}return o.byteLength>0?[i,o]:e},I=(e,t=!0,n=!1)=>{var r;const s=h[e.clientIdentifier],i=G(e,t,n);if(!s)return;const o=`${s.userId}_${null!==(r=se(s))&&void 0!==r?r:""}`,a=p[s.subscriptionKey],c=(null!=a?a:{})[o];if(!i){let t,n,r=`Previous heartbeat request has been sent less than ${s.heartbeatInterval} seconds ago. Skipping...`;if((!s.heartbeat||0===s.heartbeat.channels.length&&0===s.heartbeat.channelGroups.length)&&(r=`${s.clientIdentifier} doesn't have subscriptions to non-presence channels. Skipping...`),ae(r,s),c&&c.response&&([t,n]=c.response),!t){n=(new TextEncoder).encode('{ "status": 200, "message": "OK", "service": "Presence" }').buffer;const e=new Headers({"Content-Type":'text/javascript; charset="UTF-8"',"Content-Length":`${n.byteLength}`});t=new Response(n,{status:200,headers:e})}const i=L([t,n]);return i.url=`${e.request.origin}${e.request.path}`,i.clientIdentifier=e.clientIdentifier,i.identifier=e.request.identifier,void R(s,i)}ae("Started heartbeat request.",s);for(const t of Y(e))ae({messageType:"network-request",message:i},t);T(i,(()=>[s]),((t,n,r)=>{c&&(c.response=r),C(t,n,r,e.request),r[0].status>=400&&r[0].status<500&&J(s)}),((t,n,r)=>{C(t,n,null,e.request,U(r))})),n||B(s)},k=(e,t,n)=>{var r,s,i;const o=null!=t?t:h[e.clientIdentifier],a=A(e,t);if(!o)return;const{subscription:c,heartbeat:l}=o,u=null!=n?n:null==c?void 0:c.serviceRequestId;if(c&&0===c.channels.length&&0===c.channelGroups.length&&(c.channelGroupQuery="",c.path="",c.previousTimetoken="0",c.refreshTimestamp=Date.now(),c.timetoken="0",delete c.region,delete c.serviceRequestId,delete c.request),p[o.subscriptionKey]&&l&&0===l.channels.length&&0===l.channelGroups.length){const e=null!==(r=p[i=o.subscriptionKey])&&void 0!==r?r:p[i]={},t=`${o.userId}_${null!==(s=se(o))&&void 0!==s?s:""}`;e[t]&&e[t].clientIdentifier===o.clientIdentifier&&delete e[t].clientIdentifier,delete l.heartbeatEvent,J(o)}if(!a){const t=(new TextEncoder).encode('{"status": 200, "action": "leave", "message": "OK", "service":"Presence"}'),n=new Headers({"Content-Type":'text/javascript; charset="UTF-8"',"Content-Length":`${t.length}`}),r=new Response(t,{status:200,headers:n}),s=L([r,t]);return s.url=`${e.request.origin}${e.request.path}`,s.clientIdentifier=e.clientIdentifier,s.identifier=e.request.identifier,void R(o,s)}ae("Started leave request.",o);for(const n of Z(e,t))ae({messageType:"network-request",message:a},n);if(T(a,(()=>[o]),((t,n,r)=>{C(t,n,r,e.request)}),((t,n,r)=>{C(t,n,null,e.request,U(r))})),void 0===u)return;const d=O(u);d.forEach((e=>{e&&e.subscription&&delete e.subscription.serviceRequestId})),j(u),$(d)},w=e=>{const t=h[e.clientIdentifier];if(!t||!t.subscription)return;const n=t.subscription.serviceRequestId;t&&n&&(delete t.subscription.serviceRequestId,t.subscription.request&&t.subscription.request.identifier===e.identifier&&delete t.subscription.request,j(n))},$=e=>{let t,n;for(const r of e)if(r.subscription&&r.subscription.request){n=r.subscription.request,t=r;break}if(!n||!t)return;const r={type:"send-request",clientIdentifier:t.clientIdentifier,subscriptionKey:t.subscriptionKey,request:n};y([[t,r]],r)},T=(t,n,r,s,i)=>{e(void 0,void 0,void 0,(function*(){var e;const o=K(t);Promise.race([fetch(o,{signal:null===(e=d.get(t.identifier))||void 0===e?void 0:e.signal,keepalive:!0}),E(t.identifier,t.timeout)]).then((e=>e.arrayBuffer().then((t=>[e,t])))).then((e=>i?i(e):e)).then((e=>{const t=n();0!==t.length&&r(t,o,e)})).catch((e=>{const t=n();if(0===t.length)return;let r=e;if("string"==typeof e){const t=e.toLowerCase();r=new Error(e),!t.includes("timeout")&&t.includes("cancel")&&(r.name="AbortError")}s(t,o,r)}))}))},j=e=>{if(0===O(e).length){const t=d.get(e);d.delete(e),delete v[e],t&&t.abort("Cancel request")}},E=(e,t)=>new Promise(((n,r)=>{const s=setTimeout((()=>{d.delete(e),clearTimeout(s),r(new Error("Request timeout"))}),1e3*t)})),O=e=>Object.values(h).filter((t=>void 0!==t&&void 0!==t.subscription&&t.subscription.serviceRequestId===e)),P=(e,t)=>{delete v[t],e.forEach((e=>{e.subscription&&(delete e.subscription.request,delete e.subscription.serviceRequestId)}))},K=e=>{let t;const n=e.queryParameters;let r=e.path;if(e.headers){t={};for(const[n,r]of Object.entries(e.headers))t[n]=r}return n&&0!==Object.keys(n).length&&(r=`${r}?${le(n)}`),new Request(`${e.origin}${r}`,{method:e.method,headers:t,redirect:"follow"})},S=e=>{var t,n,r,s,i;const o=h[e.clientIdentifier],c=o.subscription,l=X(c.timetoken,e),u=a.createUUID(),d=Object.assign({},e.request);let f,p,g;if(l.length>1){const i=z(l,e);if(i){const e=v[i],{channels:n,channelGroups:r}=null!==(t=o.subscription)&&void 0!==t?t:{channels:[],channelGroups:[]};if((!(n.length>0)||ne(e.channels,n))&&(!(r.length>0)||ne(e.channelGroups,r)))return i}const a=(null!==(n=b[o.subscriptionKey])&&void 0!==n?n:{})[o.userId],h={},y=new Set(c.channelGroups),q=new Set(c.channels);a&&c.objectsWithState.length&&c.objectsWithState.forEach((e=>{const t=a[e];t&&(h[e]=t)}));for(const e of l){const{subscription:t}=e;if(!t)continue;if(t.timetoken){let e=!p;e||"0"===t.timetoken||("0"===p?e=!0:t.timetokenf)),e&&(f=t.refreshTimestamp,p=t.timetoken,g=t.region)}t.channelGroups.forEach(y.add,y),t.channels.forEach(q.add,q);const n=t.serviceRequestId;t.serviceRequestId=u,n&&v[n]&&j(n),a&&t.objectsWithState.forEach((e=>{const t=a[e];t&&!h[e]&&(h[e]=t)}))}const m=null!==(r=v[u])&&void 0!==r?r:v[u]={requestId:u,timetoken:null!==(s=d.queryParameters.tt)&&void 0!==s?s:"0",channelGroups:[],channels:[]};if(q.size){m.channels=Array.from(q).sort();const e=d.path.split("/");e[4]=m.channels.join(","),d.path=e.join("/")}if(y.size&&(m.channelGroups=Array.from(y).sort(),d.queryParameters["channel-group"]=m.channelGroups.join(",")),Object.keys(h).length&&(d.queryParameters.state=JSON.stringify(h)),d.queryParameters&&d.queryParameters.auth){const e=ie(l);e&&(d.queryParameters.auth=e)}}else v[u]={requestId:u,timetoken:null!==(i=d.queryParameters.tt)&&void 0!==i?i:"0",channelGroups:c.channelGroups,channels:c.channels};v[u]&&(d.queryParameters&&void 0!==d.queryParameters.tt&&void 0!==d.queryParameters.tr&&(v[u].region=d.queryParameters.tr),(!v[u].timetokenOverride||"0"!==v[u].timetokenOverride&&p&&"0"!==p)&&(v[u].timetokenOverride=p,v[u].regionOverride=g)),c.serviceRequestId=u,d.identifier=u;const y=l.reduce(((e,{clientIdentifier:t})=>(e.push(t),e)),[]).join(", ");if(y.length>0)for(const e of l)ce(v[u],`Started aggregated request for clients: ${y}`,e);return d},G=(e,t,n)=>{var r,s,i,o,a;const c=h[e.clientIdentifier],l=Y(e),u=Object.assign({},e.request);if(!c||!c.heartbeat)return;const d=null!==(r=p[a=c.subscriptionKey])&&void 0!==r?r:p[a]={},f=`${c.userId}_${null!==(s=se(c))&&void 0!==s?s:""}`,b=[...c.heartbeat.channelGroups],g=[...c.heartbeat.channels];let v,y,q=!1;if(d[f]){const{createdByActualRequest:e,channels:r,channelGroups:s,response:i}=d[f];!e&&t&&(d[f].createdByActualRequest=!0,d[f].timestamp=Date.now(),n=!0),v=null!==(o=c.heartbeat.presenceState)&&void 0!==o?o:{},y=ne(r,g)&&ne(s,b),i&&(q=i[0].status>=400)}else d[f]={createdByActualRequest:t,channels:g,channelGroups:b,clientIdentifier:c.clientIdentifier,timestamp:Date.now()},v=null!==(i=c.heartbeat.presenceState)&&void 0!==i?i:{},y=!1;let m=c.heartbeatInterval;for(const e of l)e.heartbeatInterval&&(m=Math.min(m,e.heartbeatInterval));if(y&&d[f].clientIdentifier){const e=d[f].timestamp+1e3*m,t=Date.now();if(!n&&!q&&tn)return void B(c,!0)}}delete d[f].response,d[f].clientIdentifier=c.clientIdentifier;for(const t of l){const{heartbeat:n}=t;void 0!==n&&t.clientIdentifier!==e.clientIdentifier&&(n.presenceState&&(v=Object.assign(Object.assign({},v),n.presenceState)),b.push(...n.channelGroups.filter((e=>!b.includes(e)))),g.push(...n.channels.filter((e=>!g.includes(e)))))}d[f].channels=g,d[f].channelGroups=b,n||(d[f].timestamp=Date.now());for(const e in Object.keys(v))g.includes(e)||b.includes(e)||delete v[e];if(0!==g.length||0!==b.length){if(g.length||b.length){const e=u.path.split("/");e[6]=g.length?g.join(","):",",u.path=e.join("/")}if(b.length&&(u.queryParameters["channel-group"]=b.join(",")),Object.keys(v).length?u.queryParameters.state=JSON.stringify(v):delete u.queryParameters.state,l.length>1&&u.queryParameters&&u.queryParameters.auth){const e=ie(l);e&&(u.queryParameters.auth=e)}return u}},A=(e,t)=>{var n;const r=null!=t?t:h[e.clientIdentifier],s=Z(e,t);let i=te(e.request),o=ee(e.request);const a=Object.assign({},e.request);if(r&&r.subscription){const{subscription:e}=r;if(o.length){e.channels=e.channels.filter((e=>!o.includes(e)));const t=e.path.split("/");if(","!==t[4]){const n=t[4].split(",").filter((e=>!o.includes(e)));t[4]=n.length?n.join(","):",",e.path=t.join("/")}}if(i.length&&(e.channelGroups=e.channelGroups.filter((e=>!i.includes(e))),e.channelGroupQuery.length>0)){const t=e.channelGroupQuery.split(",").filter((e=>!i.includes(e)));e.channelGroupQuery=t.length?t.join(","):""}}if(r&&r.heartbeat){const{heartbeat:e}=r;o.length&&(e.channels=e.channels.filter((e=>!o.includes(e)))),i.length&&(e.channelGroups=e.channelGroups.filter((e=>!i.includes(e))))}for(const t of s){const n=t.subscription;void 0!==n&&(t.clientIdentifier!==e.clientIdentifier&&(o.length&&(o=o.filter((e=>!e.endsWith("-pnpres")&&!n.channels.includes(e)))),i.length&&(i=i.filter((e=>!e.endsWith("-pnpres")&&!n.channelGroups.includes(e))))))}const c=o.length+i.length;if(o.length&&(o=o.filter((e=>!e.endsWith("-pnpres")))),i.length&&(i=i.filter((e=>!e.endsWith("-pnpres")))),0!==o.length||0!==i.length){if(r&&p[r.subscriptionKey]&&(o.length||i.length)){const e=p[r.subscriptionKey],t=`${r.userId}_${null!==(n=se(r))&&void 0!==n?n:""}`;if(e[t]){let{channels:n,channelGroups:r}=e[t];i.length&&(r=r.filter((e=>!o.includes(e)))),o.length&&(n=n.filter((e=>!o.includes(e)))),e[t].channelGroups=r,e[t].channels=n}}if(o.length){const e=a.path.split("/");e[6]=o.join(","),a.path=e.join("/")}if(i.length&&(a.queryParameters["channel-group"]=i.join(",")),s.length>1&&a.queryParameters&&a.queryParameters.auth){const e=ie(s);e&&(a.queryParameters.auth=e)}return a}if(r&&r.workerLogVerbosity){const e=s.reduce(((e,{clientIdentifier:t})=>(e.push(t),e)),[]).join(", ");ae(c>0?"Leaving only presence channels which doesn't require presence leave. Ignoring leave request.":`Specified channels and groups still in use by other clients: ${e}. Ignoring leave request.`,r)}},R=(e,t)=>{var n;const r=(null!==(n=g[e.subscriptionKey])&&void 0!==n?n:{})[e.clientIdentifier];if(!r)return!1;try{return r.postMessage(t),!0}catch(t){e.workerLogVerbosity&&console.error(`[SharedWorker] Unable send message using message port: ${t}`)}return!1},C=(e,t,n,r,s)=>{var i,o;if(0===e.length)return;if(!s&&!n)return;const a=e.some((e=>e&&e.workerLogVerbosity)),c=null!==(i=g[e[0].subscriptionKey])&&void 0!==i?i:{},l=r.path.startsWith("/v2/subscribe");!s&&n&&(s=n[0].status>=400?U(void 0,n):L(n));const u={};let d,h=200;if(n){d=n[1].byteLength>0?n[1]:void 0;const{headers:e}=n[0];h=n[0].status,e.forEach(((e,t)=>u[t]=e.toLowerCase()))}const f={status:h,url:t.url,headers:u,body:d};if(a&&r&&!r.path.endsWith("/heartbeat")){const t=`Notify clients about ${l?"subscribe":"leave"} request completion: ${e.reduce(((e,{clientIdentifier:t})=>(e.push(t),e)),[]).join(", ")}`;for(const n of e)ae(t,n)}for(const t of e){if(l&&!t.subscription){if(a){const n=`${t.clientIdentifier} doesn't have active subscription. Don't notify about completion.`;for(const t of e)ae(n,t)}continue}const n=c[t.clientIdentifier],{request:i}=null!==(o=t.subscription)&&void 0!==o?o:{};let u=null!=i?i:r;if(l||(u=r),n&&u){const e=Object.assign(Object.assign({},s),{clientIdentifier:t.clientIdentifier,identifier:u.identifier,url:`${u.origin}${u.path}`});if("request-process-success"===s.type&&t.workerLogVerbosity)ae({messageType:"network-response",message:f},t);else if("request-process-error"===s.type&&t.workerLogVerbosity){const n=!!s.error&&("TIMEOUT"===s.error.type||"ABORTED"===s.error.type);let i=s.error?s.error.message:"Unknown";if(e.response){const t=e.response.headers["content-type"];if(e.response.body&&t&&(-1!==t.indexOf("javascript")||-1!==t.indexOf("json")))try{const t=JSON.parse((new TextDecoder).decode(e.response.body));"message"in t?i=t.message:"error"in t&&("string"==typeof t.error?i=t.error:"object"==typeof t.error&&"message"in t.error&&(i=t.error.message))}catch(e){}"Unknown"===i&&(i=e.response.status>=500?"Internal Server Error":400==e.response.status?"Bad request":403==e.response.status?"Access denied":`${e.response.status}`)}ae({messageType:"network-request",message:r,details:i,canceled:n,failed:!n},t)}R(t,e)}else if(!n&&a){const n=`${t.clientIdentifier} doesn't have Shared Worker's communication channel. Don't notify about completion.`;for(const r of e)r.clientIdentifier!==t.clientIdentifier&&ae(n,r)}}},L=e=>{var t;const[n,r]=e,s=r.byteLength>0?r:void 0,i=parseInt(null!==(t=n.headers.get("Content-Length"))&&void 0!==t?t:"0",10),o=n.headers.get("Content-Type"),a={};return n.headers.forEach(((e,t)=>a[t]=e.toLowerCase())),{type:"request-process-success",clientIdentifier:"",identifier:"",url:"",response:{contentLength:i,contentType:o,headers:a,status:n.status,body:s}}},U=(e,t)=>{if(t)return Object.assign(Object.assign({},L(t)),{type:"request-process-error"});let n="NETWORK_ISSUE",r="Unknown error",s="Error";e&&e instanceof Error&&(r=e.message,s=e.name);const i=r.toLowerCase();return i.includes("timeout")?n="TIMEOUT":("AbortError"===s||i.includes("aborted")||i.includes("cancel"))&&(r="Request aborted",n="ABORTED"),{type:"request-process-error",clientIdentifier:"",identifier:"",url:"",error:{name:s,type:n,message:r}}},x=e=>{var t,n,r,s,i;const{clientIdentifier:o}=e;if(h[o])return;const a=h[o]={clientIdentifier:o,subscriptionKey:e.subscriptionKey,userId:e.userId,heartbeatInterval:e.heartbeatInterval,newlyRegistered:!0,offlineClientsCheckInterval:e.workerOfflineClientsCheckInterval,unsubscribeOfflineClients:e.workerUnsubscribeOfflineClients,workerLogVerbosity:e.workerLogVerbosity},c=null!==(t=f[s=e.subscriptionKey])&&void 0!==t?t:f[s]=[];c.every((e=>e.clientIdentifier!==o))&&c.push(a),(null!==(n=g[i=e.subscriptionKey])&&void 0!==n?n:g[i]={})[o]=e.port;const u=`Registered PubNub client with '${o}' identifier. '${c.length}' clients currently active.`;for(const e of c)ae(u,e);if(!l[e.subscriptionKey]&&(null!==(r=f[e.subscriptionKey])&&void 0!==r?r:[]).length>0){const{subscriptionKey:t}=e,n=e.workerOfflineClientsCheckInterval;for(const e of c)ae(`Setup PubNub client ping event ${n} seconds`,e);l[t]=setTimeout((()=>re(t)),500*n-1)}},W=e=>{var t,n,r;const{clientIdentifier:s,userId:i,heartbeatInterval:o,accessToken:a,preProcessedToken:c}=e,l=h[s];if(!l)return;if(ce({userId:i,heartbeatInterval:o,authKey:a,token:c},"Update client configuration:",l),i!==l.userId||a&&a!==(null!==(t=l.authKey)&&void 0!==t?t:"")){const e=null!==(n=p[l.subscriptionKey])&&void 0!==n?n:{},t=`${i}_${null!==(r=se(l))&&void 0!==r?r:""}`;void 0!==e[t]&&delete e[t]}const u=l.heartbeatInterval!==o;l.userId=i,l.heartbeatInterval=o,a&&(l.authKey=a),c&&(l.accessToken=c),u&&B(l,!0),Q(l),l.heartbeat&&l.heartbeat.heartbeatEvent&&I(l.heartbeat.heartbeatEvent,!1,!0)},D=e=>{V(e.subscriptionKey,e.clientIdentifier)},F=e=>{var t,n,r,s,i,o,a,c,l,u,d,f,p,g,v,y,q,m,I,k;const w=e.request.queryParameters,{clientIdentifier:$}=e,T=h[$];let j=!1;if(!T)return;const E=null!==(t=w["channel-group"])&&void 0!==t?t:"",O=null!==(n=w.state)&&void 0!==n?n:"";let P=T.subscription;if(P){if(O.length>0){const e=JSON.parse(O),t=null!==(o=(y=null!==(i=b[v=T.subscriptionKey])&&void 0!==i?i:b[v]={})[q=T.userId])&&void 0!==o?o:y[q]={};Object.entries(e).forEach((([e,n])=>t[e]=n));for(const n of P.objectsWithState)e[n]||delete t[n];P.objectsWithState=Object.keys(e)}else if(P.objectsWithState.length){const e=null!==(c=(I=null!==(a=b[m=T.subscriptionKey])&&void 0!==a?a:b[m]={})[k=T.userId])&&void 0!==c?c:I[k]={};for(const t of P.objectsWithState)delete e[t];P.objectsWithState=[]}}else{if(j=!0,P={refreshTimestamp:0,path:"",channelGroupQuery:"",channels:[],channelGroups:[],previousTimetoken:"0",timetoken:"0",objectsWithState:[]},O.length>0){const e=JSON.parse(O),t=null!==(s=(p=null!==(r=b[f=T.subscriptionKey])&&void 0!==r?r:b[f]={})[g=T.userId])&&void 0!==s?s:p[g]={};Object.entries(e).forEach((([e,n])=>t[e]=n)),P.objectsWithState=Object.keys(e)}T.subscription=P}if(P.path!==e.request.path){P.path=e.request.path;const t=ee(e.request);j||(j=!ne(P.channels,t)),P.channels=t}if(P.channelGroupQuery!==E){P.channelGroupQuery=E;const t=te(e.request);j||(j=!ne(P.channelGroups,t)),P.channelGroups=t}let{authKey:K}=T;return P.refreshTimestamp=Date.now(),P.request=e.request,P.filterExpression=null!==(l=w["filter-expr"])&&void 0!==l?l:"",P.timetoken=null!==(u=w.tt)&&void 0!==u?u:"0",void 0!==w.tr&&(P.region=w.tr),T.authKey=null!==(d=w.auth)&&void 0!==d?d:"",T.origin=e.request.origin,T.userId=w.uuid,T.pnsdk=w.pnsdk,T.accessToken=e.preProcessedToken,T.newlyRegistered&&!K&&T.authKey&&(K=T.authKey),T.newlyRegistered=!1,j},N=e=>{var t,n,r;const{clientIdentifier:s}=e,i=h[s],{request:o}=e,a=null!==(t=o.queryParameters)&&void 0!==t?t:{};if(!i)return;const c=null!==(n=i.heartbeat)&&void 0!==n?n:i.heartbeat={channels:[],channelGroups:[]};c.heartbeatEvent=Object.assign({},e),c.channelGroups=te(o).filter((e=>!e.endsWith("-pnpres"))),c.channels=ee(o).filter((e=>!e.endsWith("-pnpres")));const l=null!==(r=a.state)&&void 0!==r?r:"";if(l.length>0){const e=JSON.parse(l);for(const t of Object.keys(e))c.channels.includes(t)||c.channelGroups.includes(t)||delete e[t];c.presenceState=e}i.accessToken=e.preProcessedToken},M=e=>{const t=h[e.clientIdentifier];t&&(t.lastPongEvent=(new Date).getTime()/1e3)},V=(e,t)=>{var n,r,s;const i=h[t];delete h[t];let o,a=f[e];if(i){if(i.subscription&&(o=i.subscription.serviceRequestId,delete i.subscription.serviceRequestId,o&&j(o)),J(i),p[e]){const t=null!==(n=p[e])&&void 0!==n?n:p[e]={},s=`${i.userId}_${null!==(r=se(i))&&void 0!==r?r:""}`;t[s]&&t[s].clientIdentifier===i.clientIdentifier&&delete t[s].clientIdentifier}i.unsubscribeOfflineClients&&_(i,o)}if(a)if(a=a.filter((e=>e.clientIdentifier!==t)),a.length>0?f[e]=a:(delete f[e],delete p[e]),0===a.length&&delete b[e],a.length>0){const n=g[e];n&&(delete n[t],0===Object.keys(n).length&&delete g[e])}else delete g[e];const c=`Invalidate '${t}' client. '${(null!==(s=f[e])&&void 0!==s?s:[]).length}' clients currently active.`;if(a)for(const e of a)ae(c,e);else ae(c)},_=(e,n)=>{if(!e.subscription)return;const{channels:r,channelGroups:s}=e.subscription,i=(null!=s?s:[]).filter((e=>!e.endsWith("-pnpres"))).map((e=>ue(e))).sort(),o=(null!=r?r:[]).filter((e=>!e.endsWith("-pnpres"))).map((e=>ue(e))).sort();if(0===o.length&&0===i.length)return;const c=i.length>0?i.join(","):void 0,l=0===o.length?",":o.join(","),u=Object.assign(Object.assign({instanceid:e.clientIdentifier,uuid:e.userId,requestid:a.createUUID()},e.authKey?{auth:e.authKey}:{}),c?{"channel-group":c}:{}),d={type:"send-request",clientIdentifier:e.clientIdentifier,subscriptionKey:e.subscriptionKey,request:{origin:e.origin,path:`/v2/presence/sub-key/${e.subscriptionKey}/channel/${l}/leave`,queryParameters:u,method:t.GET,headers:{},timeout:10,cancellable:!1,compressible:!1,identifier:u.requestid}};k(d,e,n)},B=(e,t=!1)=>{const{heartbeat:n,heartbeatInterval:r}=e;if(!(r&&r>0&&void 0!==n&&n.heartbeatEvent&&(n.channels.length>0||n.channelGroups.length>0)))return void J(e);if(t&&!n.loop)return;let s=r;if(t&&n.loop){const e=(Date.now()-n.loop.startTimestamp)/1e3;e{if(J(e),!e.heartbeat||!e.heartbeat.heartbeatEvent)return;const{request:t}=e.heartbeat.heartbeatEvent;t.identifier=a.createUUID(),t.queryParameters.requestid=t.identifier,I(e.heartbeat.heartbeatEvent,!1)}),1e3*s),heartbeatInterval:r,startTimestamp:Date.now()})},J=e=>{const{heartbeat:t}=e;void 0!==t&&t.loop&&(clearTimeout(t.loop.timer),delete t.loop)},Q=e=>{var t,n,r;const{subscription:s,heartbeat:i}=e;if(s&&s.request&&s.request.queryParameters){const t=s.request.queryParameters;e.authKey&&e.authKey.length>0?t.auth=e.authKey:t.auth&&delete t.auth}if((null==i?void 0:i.heartbeatEvent)&&i.heartbeatEvent.request){e.accessToken&&(i.heartbeatEvent.preProcessedToken=e.accessToken);const s=null!==(t=p[r=e.subscriptionKey])&&void 0!==t?t:p[r]={},o=`${e.userId}_${null!==(n=se(e))&&void 0!==n?n:""}`;s[o]&&s[o].response&&delete s[o].response,i.heartbeatEvent.request.identifier=a.createUUID();const c=i.heartbeatEvent.request.queryParameters;c.requestid=i.heartbeatEvent.request.identifier,e.authKey&&e.authKey.length>0?c.auth=e.authKey:c.auth&&delete c.auth}},H=e=>{const{clientIdentifier:t,subscriptionKey:n}=e.data;return!(!t||"string"!=typeof t)&&!(!n||"string"!=typeof n)},z=(e,t)=>{var n;const r=null!==(n=t.request.queryParameters["channel-group"])&&void 0!==n?n:"",s=t.request.path;let i,o;for(const n of e){const{subscription:e}=n;if(!e||!e.serviceRequestId)continue;const a=h[t.clientIdentifier],c=e.serviceRequestId;if(e.path===s&&e.channelGroupQuery===r)return ae(`Found identical request started by '${n.clientIdentifier}' client. \nWaiting for existing '${c}' request completion.`,a),e.serviceRequestId;{const r=v[e.serviceRequestId];if(i||(i=te(t.request)),o||(o=ee(t.request)),o.length&&!ne(r.channels,o))continue;if(i.length&&!ne(r.channelGroups,i))continue;return ce(r,`'${t.request.identifier}' request channels and groups are subset of ongoing '${c}' request \nwhich has started by '${n.clientIdentifier}' client. Waiting for existing '${c}' request completion.`,a),e.serviceRequestId}}},X=(e,t)=>{var n,r;const s=h[t.clientIdentifier];if(!s)return[];const i=t.request.queryParameters,o=se(s),a=null!==(n=i["filter-expr"])&&void 0!==n?n:"",c=i.uuid;return(null!==(r=f[t.subscriptionKey])&&void 0!==r?r:[]).filter((t=>t.userId===c&&se(t)===o&&t.subscription&&(0!==t.subscription.channels.length||0!==t.subscription.channelGroups.length)&&t.subscription.filterExpression===a&&("0"===e||"0"===t.subscription.timetoken||t.subscription.timetoken===e)))},Y=e=>Z(e),Z=(e,t)=>{var n;const r=null!=t?t:h[e.clientIdentifier];if(!r)return[];const s=e.request.queryParameters,i=se(r),o=s.uuid;return(null!==(n=f[e.subscriptionKey])&&void 0!==n?n:[]).filter((e=>e.userId===o&&se(e)===i))},ee=e=>{const t=e.path.split("/")[e.path.startsWith("/v2/subscribe/")?4:6];return","===t?[]:t.split(",").filter((e=>e.length>0))},te=e=>{var t;const n=null!==(t=e.queryParameters["channel-group"])&&void 0!==t?t:"";return 0===n.length?[]:n.split(",").filter((e=>e.length>0))},ne=(e,t)=>{const n=new Set(e);return t.every(n.has,n)},re=e=>{const t={type:"shared-worker-ping"},n=Object.values(h).filter((t=>t&&t.subscriptionKey===e));if(n.forEach((e=>{let r=!1;if(e&&e.lastPingRequest){const t=e.offlineClientsCheckInterval;if(!e.lastPongEvent||Math.abs(e.lastPongEvent-e.lastPingRequest)>.5*t){r=!0;for(const t of n)ae(`'${e.clientIdentifier}' client is inactive. Invalidating...`,t);V(e.subscriptionKey,e.clientIdentifier)}}e&&!r&&(e.lastPingRequest=(new Date).getTime()/1e3,R(e,t))})),n&&n.length>0&&n[0]){const t=n[0].offlineClientsCheckInterval;l[e]=setTimeout((()=>re(e)),500*t-1)}},se=e=>{var t;return e.accessToken&&null!==(t=e.accessToken.token)&&void 0!==t?t:e.authKey},ie=e=>{const t=e.filter((e=>!!e.accessToken)).sort(((e,t)=>e.accessToken.expiration-t.accessToken.expiration)).pop();return t?t.authKey:void 0},oe=e=>{const t=se(e);let n=`${e.userId}-${e.subscriptionKey}${t?`-${t}`:""}`;return e.subscription&&e.subscription.filterExpression&&(n+=`-${e.subscription.filterExpression}`),n},ae=(e,t)=>{const n=(t?[t]:Object.values(h)).filter((e=>e&&e.workerLogVerbosity)),r={type:"shared-worker-console-log",message:e};n.forEach((e=>{e&&R(e,r)}))},ce=(e,t,n)=>{const r=(n?[n]:Object.values(h)).filter((e=>e&&e.workerLogVerbosity)),s={type:"shared-worker-console-dir",message:t,data:e};r.forEach((e=>{e&&R(e,s)}))},le=e=>Object.keys(e).map((t=>{const n=e[t];return Array.isArray(n)?n.map((e=>`${t}=${ue(e)}`)).join("&"):`${t}=${ue(n)}`})).join("&"),ue=e=>encodeURIComponent(e).replace(/[!~*'()]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`))})); +!function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";var e,t,s,n;!function(e){e.Unregister="unregister",e.Disconnect="disconnect",e.IdentityChange="identityChange",e.AuthChange="authChange",e.HeartbeatIntervalChange="heartbeatIntervalChange",e.SendSubscribeRequest="sendSubscribeRequest",e.CancelSubscribeRequest="cancelSubscribeRequest",e.SendHeartbeatRequest="sendHeartbeatRequest",e.SendLeaveRequest="sendLeaveRequest"}(e||(e={}));class r extends CustomEvent{get client(){return this.detail.client}}class i extends r{constructor(t){super(e.Unregister,{detail:{client:t}})}clone(){return new i(this.client)}}class a extends r{constructor(t){super(e.Disconnect,{detail:{client:t}})}clone(){return new a(this.client)}}class o extends r{constructor(t,s,n){super(e.IdentityChange,{detail:{client:t,oldUserId:s,newUserId:n}})}get oldUserId(){return this.detail.oldUserId}get newUserId(){return this.detail.newUserId}clone(){return new o(this.client,this.oldUserId,this.newUserId)}}class c extends r{constructor(t,s,n){super(e.AuthChange,{detail:{client:t,oldAuth:n,newAuth:s}})}get oldAuth(){return this.detail.oldAuth}get newAuth(){return this.detail.newAuth}clone(){return new c(this.client,this.newAuth,this.oldAuth)}}class h extends r{constructor(t,s,n){super(e.HeartbeatIntervalChange,{detail:{client:t,oldInterval:n,newInterval:s}})}get oldInterval(){return this.detail.oldInterval}get newInterval(){return this.detail.newInterval}clone(){return new h(this.client,this.newInterval,this.oldInterval)}}class l extends r{constructor(t,s){super(e.SendSubscribeRequest,{detail:{client:t,request:s}})}get request(){return this.detail.request}clone(){return new l(this.client,this.request)}}class u extends r{constructor(t,s){super(e.CancelSubscribeRequest,{detail:{client:t,request:s}})}get request(){return this.detail.request}clone(){return new u(this.client,this.request)}}class d extends r{constructor(t,s){super(e.SendHeartbeatRequest,{detail:{client:t,request:s}})}get request(){return this.detail.request}clone(){return new d(this.client,this.request)}}class g extends r{constructor(t,s){super(e.SendLeaveRequest,{detail:{client:t,request:s}})}get request(){return this.detail.request}clone(){return new g(this.client,this.request)}}!function(e){e.Registered="Registered",e.Unregistered="Unregistered"}(t||(t={}));class p extends CustomEvent{constructor(e){super(t.Registered,{detail:e})}get client(){return this.detail}clone(){return new p(this.client)}}class f extends CustomEvent{constructor(e,s=!1){super(t.Unregistered,{detail:{client:e,withLeave:s}})}get client(){return this.detail.client}get withLeave(){return this.detail.withLeave}clone(){return new f(this.client,this.withLeave)}}!function(e){e.Changed="changed",e.Invalidated="invalidated"}(s||(s={}));class q extends CustomEvent{constructor(e,t,n,r){super(s.Changed,{detail:{withInitialResponse:e,newRequests:t,canceledRequests:n,leaveRequest:r}})}get requestsWithInitialResponse(){return this.detail.withInitialResponse}get newRequests(){return this.detail.newRequests}get leaveRequest(){return this.detail.leaveRequest}get canceledRequests(){return this.detail.canceledRequests}clone(){return new q(this.requestsWithInitialResponse,this.newRequests,this.canceledRequests,this.leaveRequest)}}class v extends CustomEvent{constructor(){super(s.Invalidated)}clone(){return new v}}!function(e){e.Started="started",e.Canceled="canceled",e.Success="success",e.Error="error"}(n||(n={}));class m extends CustomEvent{get request(){return this.detail.request}}class b extends m{constructor(e){super(n.Started,{detail:{request:e}})}clone(e){return new b(null!=e?e:this.request)}}class C extends m{constructor(e,t,s){super(n.Success,{detail:{request:e,fetchRequest:t,response:s}})}get fetchRequest(){return this.detail.fetchRequest}get response(){return this.detail.response}clone(e){return new C(null!=e?e:this.request,e?e.asFetchRequest:this.fetchRequest,this.response)}}class S extends m{constructor(e,t,s){super(n.Error,{detail:{request:e,fetchRequest:t,error:s}})}get fetchRequest(){return this.detail.fetchRequest}get error(){return this.detail.error}clone(e){return new S(null!=e?e:this.request,e?e.asFetchRequest:this.fetchRequest,this.error)}}class R extends m{constructor(e){super(n.Canceled,{detail:{request:e}})}clone(e){return new R(null!=e?e:this.request)}}"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function E(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var y,T,w={exports:{}}; +/*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */y=w,function(e){var t="0.1.0",s={3:/^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,4:/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,5:/^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,all:/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i};function n(){var e,t,s="";for(e=0;e<32;e++)t=16*Math.random()|0,8!==e&&12!==e&&16!==e&&20!==e||(s+="-"),s+=(12===e?4:16===e?3&t|8:t).toString(16);return s}function r(e,t){var n=s[t||"all"];return n&&n.test(e)||!1}n.isUUID=r,n.VERSION=t,e.uuid=n,e.isUUID=r}(T=w.exports),null!==y&&(y.exports=T.uuid);var I,k=E(w.exports),A={createUUID:()=>k.uuid?k.uuid():k()};class O extends EventTarget{constructor(e,t,s,n,r,i){super(),this.request=e,this.subscribeKey=t,this.channels=n,this.channelGroups=r,this.dependents={},this._completed=!1,this._canceled=!1,this.queryStringFromObject=e=>Object.keys(e).map((t=>{const s=e[t];return Array.isArray(s)?s.map((e=>`${t}=${this.encodeString(e)}`)).join("&"):`${t}=${this.encodeString(s)}`})).join("&"),this._accessToken=i,this._userId=s}get identifier(){return this.request.identifier}get origin(){return this.request.origin}get userId(){return this._userId}set userId(e){this._userId=e,this.request.queryParameters.uuid=e}get accessToken(){return this._accessToken}set accessToken(e){this._accessToken=e,e?this.request.queryParameters.auth=e.toString():delete this.request.queryParameters.auth}get client(){return this._client}set client(e){this._client=e}get completed(){return this._completed}get cancellable(){return this.request.cancellable}get canceled(){return this._canceled}set fetchAbortController(e){this.completed||this.canceled||(this.isServiceRequest?this._fetchAbortController?console.error("Only one abort controller can be set for service-provided requests."):this._fetchAbortController=e:console.error("Unexpected attempt to set fetch abort controller on client-provided request."))}get fetchAbortController(){return this._fetchAbortController}get asFetchRequest(){const e=this.request.queryParameters,t={};let s="";if(this.request.headers)for(const[e,s]of Object.entries(this.request.headers))t[e]=s;return e&&0!==Object.keys(e).length&&(s=`?${this.queryStringFromObject(e)}`),new Request(`${this.origin}${this.request.path}${s}`,{method:this.request.method,headers:Object.keys(t).length?t:void 0,redirect:"follow"})}get serviceRequest(){return this._serviceRequest}set serviceRequest(e){if(this.isServiceRequest)return void console.error("Unexpected attempt to set service-provided request on service-provided request.");const t=this.serviceRequest;this._serviceRequest=e,!t||e&&t.identifier===e.identifier||t.detachRequest(this),this.completed||this.canceled||e&&(e.completed||e.canceled)?this._serviceRequest=void 0:t&&e&&t.identifier===e.identifier||e&&e.attachRequest(this)}get isServiceRequest(){return!this.client}dependentRequests(){return this.isServiceRequest?Object.values(this.dependents):[]}attachRequest(e){this.isServiceRequest&&!this.dependents[e.identifier]?(this.dependents[e.identifier]=e,this.addEventListenersForRequest(e)):this.isServiceRequest||console.error("Unexpected attempt to attach requests using client-provided request.")}detachRequest(e){this.isServiceRequest&&this.dependents[e.identifier]?(delete this.dependents[e.identifier],e.removeEventListenersFromRequest(),0===Object.keys(this.dependents).length&&this.cancel("Cancel request")):this.isServiceRequest||console.error("Unexpected attempt to detach requests using client-provided request.")}cancel(e,t=!1){if(this.completed||this.canceled)return[];const s=this.dependentRequests();return this.isServiceRequest?(t||s.forEach((e=>e.serviceRequest=void 0)),this._fetchAbortController&&(this._fetchAbortController.abort(e),this._fetchAbortController=void 0)):this.serviceRequest=void 0,this._canceled=!0,this.stopRequestTimeoutTimer(),this.dispatchEvent(new R(this)),s}requestTimeoutTimer(){return new Promise(((e,t)=>{this._fetchTimeoutTimer=setTimeout((()=>{t(new Error("Request timeout")),this.cancel("Cancel because of timeout",!0)}),1e3*this.request.timeout)}))}stopRequestTimeoutTimer(){this._fetchTimeoutTimer&&(clearTimeout(this._fetchTimeoutTimer),this._fetchTimeoutTimer=void 0)}handleProcessingStarted(){this.logRequestStart(this),this.dispatchEvent(new b(this))}handleProcessingSuccess(e,t){this.addRequestInformationForResult(this,e,t),this.logRequestSuccess(this,t),this._completed=!0,this.stopRequestTimeoutTimer(),this.dispatchEvent(new C(this,e,t))}handleProcessingError(e,t){this.addRequestInformationForResult(this,e,t),this.logRequestError(this,t),this._completed=!0,this.stopRequestTimeoutTimer(),this.dispatchEvent(new S(this,e,t))}addEventListenersForRequest(e){this.isServiceRequest?(e.abortController=new AbortController,this.addEventListener(n.Started,(t=>{t instanceof b&&(e.logRequestStart(t.request),e.dispatchEvent(t.clone(e)))}),{signal:e.abortController.signal,once:!0}),this.addEventListener(n.Success,(t=>{t instanceof C&&(e.removeEventListenersFromRequest(),e.addRequestInformationForResult(t.request,t.fetchRequest,t.response),e.logRequestSuccess(t.request,t.response),e._completed=!0,e.dispatchEvent(t.clone(e)))}),{signal:e.abortController.signal,once:!0}),this.addEventListener(n.Error,(t=>{t instanceof S&&(e.removeEventListenersFromRequest(),e.addRequestInformationForResult(t.request,t.fetchRequest,t.error),e.logRequestError(t.request,t.error),e._completed=!0,e.dispatchEvent(t.clone(e)))}),{signal:e.abortController.signal,once:!0})):console.error("Unexpected attempt to add listeners using a client-provided request.")}removeEventListenersFromRequest(){!this.isServiceRequest&&this.abortController?(this.abortController.abort(),this.abortController=void 0):this.isServiceRequest&&console.error("Unexpected attempt to remove listeners using a client-provided request.")}hasAnyChannelsOrGroups(e,t){return this.channels.some((t=>e.includes(t)))||this.channelGroups.some((e=>t.includes(e)))}addRequestInformationForResult(e,t,s){this.isServiceRequest||(s.clientIdentifier=this.client.identifier,s.identifier=this.identifier,s.url=t.url)}logRequestStart(e){this.isServiceRequest||this.client.logger.debug((()=>({messageType:"network-request",message:e.request})))}logRequestSuccess(e,t){this.isServiceRequest||this.client.logger.debug((()=>{const{status:s,headers:n,body:r}=t.response,i=e.asFetchRequest;return Object.entries(n).forEach((([e,t])=>t)),{messageType:"network-response",message:{status:s,url:i.url,headers:n,body:r}}}))}logRequestError(e,t){this.isServiceRequest||((t.error?t.error.message:"Unknown").toLowerCase().includes("timeout")?this.client.logger.debug((()=>({messageType:"network-request",message:e.request,details:"Timeout",canceled:!0}))):this.client.logger.warn((()=>{const{details:s,canceled:n}=this.errorDetailsFromSendingError(t);let r=s;return n?r="Aborted":s.toLowerCase().includes("network")&&(r="Network error"),{messageType:"network-request",message:e.request,details:r,canceled:n,failed:!n}})))}errorDetailsFromSendingError(e){const t=!!e.error&&("TIMEOUT"===e.error.type||"ABORTED"===e.error.type);let s=e.error?e.error.message:"Unknown";if(e.response){const t=e.response.headers["content-type"];if(e.response.body&&t&&(-1!==t.indexOf("javascript")||-1!==t.indexOf("json")))try{const t=JSON.parse((new TextDecoder).decode(e.response.body));"message"in t?s=t.message:"error"in t&&("string"==typeof t.error?s=t.error:"object"==typeof t.error&&"message"in t.error&&(s=t.error.message))}catch(e){}"Unknown"===s&&(s=e.response.status>=500?"Internal Server Error":400==e.response.status?"Bad request":403==e.response.status?"Access denied":`${e.response.status}`)}return{details:s,canceled:t}}encodeString(e){return encodeURIComponent(e).replace(/[!~*'()]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`))}}class F extends O{static fromTransportRequest(e,t,s){return new F(e,t,s)}static fromCachedState(e,t,s,n,r,i){return new F(e,t,i,s,n,r)}static fromRequests(e,t,s,n){const r=e[Math.floor(Math.random()*e.length)],i=Object.assign({},r.request);let a={};const o=new Set,c=new Set;for(const t of e)t.state&&(a=Object.assign(Object.assign({},a),t.state)),t.channelGroups.forEach(o.add,o),t.channels.forEach(c.add,c);if(c.size||o.size){const e=i.path.split("/");e[4]=c.size?[...c].sort().join(","):",",i.path=e.join("/")}o.size&&(i.queryParameters["channel-group"]=[...o].sort().join(",")),Object.keys(a).length?i.queryParameters.state=JSON.stringify(a):delete i.queryParameters.state,t&&(i.queryParameters.auth=t.toString()),i.identifier=A.createUUID();const h=new F(i,r.subscribeKey,t);for(const t of e)t.serviceRequest=h;return h.isInitialSubscribe&&s&&"0"!==s&&(h.timetokenOverride=s,n&&(h.timetokenRegionOverride=n)),h}constructor(e,t,s,n,r,i){var a;const o=!!e.queryParameters&&"on-demand"in e.queryParameters;if(delete e.queryParameters["on-demand"],super(e,t,e.queryParameters.uuid,null!=r?r:F.channelsFromRequest(e),null!=n?n:F.channelGroupsFromRequest(e),s),this._creationDate=Date.now(),this.timetokenRegionOverride="0",this._creationDate<=F.lastCreationDate?(F.lastCreationDate++,this._creationDate=F.lastCreationDate):F.lastCreationDate=this._creationDate,this._requireCachedStateReset=o,e.queryParameters["filter-expr"]&&(this.filterExpression=e.queryParameters["filter-expr"]),this._timetoken=null!==(a=e.queryParameters.tt)&&void 0!==a?a:"0",e.queryParameters.tr&&(this._region=e.queryParameters.tr),i&&(this.state=i),this.state||!e.queryParameters.state||0===e.queryParameters.state.length)return;const c=JSON.parse(e.queryParameters.state);for(const e of Object.keys(c))this.channels.includes(e)||this.channelGroups.includes(e)||delete c[e];this.state=c}get creationDate(){return this._creationDate}get asIdentifier(){const e=this.accessToken?this.accessToken.asIdentifier:void 0,t=`${this.userId}-${this.subscribeKey}${e?`-${e}`:""}`;return this.filterExpression?`${t}-${this.filterExpression}`:t}get isInitialSubscribe(){return"0"===this._timetoken}get timetoken(){return this._timetoken}set timetoken(e){this._timetoken=e,this.request.queryParameters.tt=e}get region(){return this._region}set region(e){this._region=e,e?this.request.queryParameters.tr=e:delete this.request.queryParameters.tr}get requireCachedStateReset(){return this._requireCachedStateReset}static useCachedState(e){return!!e.queryParameters&&!("on-demand"in e.queryParameters)}resetToInitialRequest(){this._requireCachedStateReset=!0,this._timetoken="0",this._region=void 0,delete this.request.queryParameters.tt}isSubsetOf(e){return!(e.channelGroups.length&&!this.includesStrings(e.channelGroups,this.channelGroups))&&(!(e.channels.length&&!this.includesStrings(e.channels,this.channels))&&("0"===this.timetoken||this.timetoken===e.timetoken||"0"===e.timetoken))}toString(){return`SubscribeRequest { clientIdentifier: ${this.client?this.client.identifier:"service request"}, requestIdentifier: ${this.identifier}, serviceRequestIdentified: ${this.client?this.serviceRequest?this.serviceRequest.identifier:"'not set'":"'is service request"}, channels: [${this.channels.length?this.channels.map((e=>`'${e}'`)).join(", "):""}], channelGroups: [${this.channelGroups.length?this.channelGroups.map((e=>`'${e}'`)).join(", "):""}], timetoken: ${this.timetoken}, region: ${this.region}, reset: ${this._requireCachedStateReset?"'reset'":"'do not reset'"} }`}toJSON(){return this.toString()}static channelsFromRequest(e){const t=e.path.split("/")[4];return","===t?[]:t.split(",").filter((e=>e.length>0))}static channelGroupsFromRequest(e){if(!e.queryParameters||!e.queryParameters["channel-group"])return[];const t=e.queryParameters["channel-group"];return 0===t.length?[]:t.split(",").filter((e=>e.length>0))}includesStrings(e,t){const s=new Set(e);return t.every(s.has,s)}}F.lastCreationDate=0;class L{static compare(e,t){var s,n;return(null!==(s=e.expiration)&&void 0!==s?s:0)-(null!==(n=t.expiration)&&void 0!==n?n:0)}constructor(e,t,s){this.token=e,this.simplifiedToken=t,this.expiration=s}get asIdentifier(){var e;return null!==(e=this.simplifiedToken)&&void 0!==e?e:this.token}equalTo(e,t=!1){return this.asIdentifier===e.asIdentifier&&(!t||this.expiration===e.expiration)}isNewerThan(e){return!!this.simplifiedToken&&this.expiration>e.expiration}toString(){return this.token}}!function(e){e.GET="GET",e.POST="POST",e.PATCH="PATCH",e.DELETE="DELETE",e.LOCAL="LOCAL"}(I||(I={}));class P extends O{static fromTransportRequest(e,t,s){return new P(e,t,s)}constructor(e,t,s){const n=P.channelGroupsFromRequest(e),r=P.channelsFromRequest(e),i=n.filter((e=>!e.endsWith("-pnpres"))),a=r.filter((e=>!e.endsWith("-pnpres")));super(e,t,e.queryParameters.uuid,a,i,s),this.allChannelGroups=n,this.allChannels=r}toString(){return`LeaveRequest { channels: [${this.channels.length?this.channels.map((e=>`'${e}'`)).join(", "):""}], channelGroups: [${this.channelGroups.length?this.channelGroups.map((e=>`'${e}'`)).join(", "):""}] }`}toJSON(){return this.toString()}static channelsFromRequest(e){const t=e.path.split("/")[6];return","===t?[]:t.split(",").filter((e=>e.length>0))}static channelGroupsFromRequest(e){if(!e.queryParameters||!e.queryParameters["channel-group"])return[];const t=e.queryParameters["channel-group"];return 0===t.length?[]:t.split(",").filter((e=>e.length>0))}}const _=(e,t,s)=>{if(t=t.filter((e=>!e.endsWith("-pnpres"))).map((e=>j(e))).sort(),s=s.filter((e=>!e.endsWith("-pnpres"))).map((e=>j(e))).sort(),0===t.length&&0===s.length)return;const n=s.length>0?s.join(","):void 0,r=0===t.length?",":t.join(","),i=Object.assign(Object.assign({instanceid:e.identifier,uuid:e.userId,requestid:A.createUUID()},e.accessToken?{auth:e.accessToken.toString()}:{}),n?{"channel-group":n}:{}),a={origin:e.origin,path:`/v2/presence/sub-key/${e.subKey}/channel/${r}/leave`,queryParameters:i,method:I.GET,headers:{},timeout:10,cancellable:!1,compressible:!1,identifier:i.requestid};return P.fromTransportRequest(a,e.subKey,e.accessToken)},j=e=>encodeURIComponent(e).replace(/[!~*'()]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`));class x{static squashedChanges(e){if(!e.length||1===e.length)return e;const t=e.sort(((e,t)=>e.timestamp-t.timestamp)),s=t.filter((e=>!e.remove));s.forEach((e=>{for(let n=0;n{if(n[e.clientIdentifier]){const s=t.indexOf(e);s>=0&&t.splice(s,1)}n[e.clientIdentifier]=e})),t}constructor(e,t,s,n,r=!1){this.clientIdentifier=e,this.request=t,this.remove=s,this.sendLeave=n,this.clientInvalidate=r,this._timestamp=this.timestampForChange()}get timestamp(){return this._timestamp}toString(){return`SubscriptionStateChange { timestamp: ${this.timestamp}, client: ${this.clientIdentifier}, request: ${this.request.toString()}, remove: ${this.remove?"'remove'":"'do not remove'"}, sendLeave: ${this.sendLeave?"'send'":"'do not send'"} }`}toJSON(){return this.toString()}timestampForChange(){const e=Date.now();return e<=x.previousChangeTimestamp?x.previousChangeTimestamp++:x.previousChangeTimestamp=e,x.previousChangeTimestamp}}x.previousChangeTimestamp=0;class U extends EventTarget{constructor(e){super(),this.identifier=e,this.requestListenersAbort={},this.clientsState={},this.lastCompletedRequest={},this.clientsForInvalidation=[],this.requests={},this.serviceRequests=[],this.channelGroups=new Set,this.channels=new Set}hasStateForClient(e){return!!this.clientsState[e.identifier]}uniqueStateForClient(e,t,s){let n=[...s],r=[...t];return Object.entries(this.clientsState).forEach((([t,s])=>{t!==e.identifier&&(n=n.filter((e=>!s.channelGroups.has(e))),r=r.filter((e=>!s.channels.has(e))))})),{channels:r,channelGroups:n}}requestForClient(e,t=!1){var s;return null!==(s=this.requests[e.identifier])&&void 0!==s?s:t?this.lastCompletedRequest[e.identifier]:void 0}updateClientAccessToken(e){this.accessToken&&!e.isNewerThan(this.accessToken)||(this.accessToken=e)}invalidateClient(e){this.clientsForInvalidation.includes(e.identifier)||this.clientsForInvalidation.push(e.identifier)}processChanges(e){if(e.length&&(e=x.squashedChanges(e)),!e.length)return;let t=0===this.channelGroups.size&&0===this.channels.size;t||(t=e.some((e=>e.remove||e.request.requireCachedStateReset)));const s=this.applyChanges(e);let n;t&&(n=this.refreshInternalState()),this.handleSubscriptionStateChange(e,n,s.initial,s.continuation,s.removed),Object.keys(this.clientsState).length||this.dispatchEvent(new v)}applyChanges(e){const t=[],s=[],n=[];return e.forEach((e=>{const{remove:r,request:i,clientIdentifier:a,clientInvalidate:o}=e;r||(i.isInitialSubscribe?s.push(i):t.push(i),this.requests[a]=i,this.addListenersForRequestEvents(i)),r&&(this.requests[a]||this.lastCompletedRequest[a])&&(o&&(delete this.lastCompletedRequest[a],delete this.clientsState[a]),delete this.requests[a],n.push(i))})),{initial:s,continuation:t,removed:n}}handleSubscriptionStateChange(e,t,s,n,r){var i,a,o,c;const h=this.serviceRequests.filter((e=>!e.completed&&!e.canceled)),l=[],u=[],d=[],g=[];let p,f,v,m;const b=e=>{g.push(e);const t=e.dependentRequests().filter((e=>!r.includes(e)));0!==t.length&&(t.forEach((e=>e.serviceRequest=void 0)),(e.isInitialSubscribe?s:n).push(...t))};if(t)if(t.channels.added||t.channelGroups.added){for(const e of h)b(e);h.length=0}else if(t.channels.removed||t.channelGroups.removed){const e=null!==(i=t.channelGroups.removed)&&void 0!==i?i:[],s=null!==(a=t.channels.removed)&&void 0!==a?a:[];for(let t=h.length-1;t>=0;t--){const n=h[t];n.hasAnyChannelsOrGroups(s,e)&&(b(n),h.splice(t,1))}}n=this.squashSameClientRequests(n),((s=this.squashSameClientRequests(s)).length?n:[]).forEach((e=>{let t=!m;t||"0"===e.timetoken||("0"===m?t=!0:e.timetokenf)),t&&(f=e.creationDate,m=e.timetoken,v=e.region)}));const C={};n.forEach((e=>{C[e.timetoken]?C[e.timetoken].push(e):C[e.timetoken]=[e]})),this.attachToServiceRequest(h,s);for(let e=s.length-1;e>=0;e--){const t=s[e];h.forEach((n=>{if(!t.isSubsetOf(n)||n.isInitialSubscribe)return;const{region:r,timetoken:i}=n;l.push({request:t,timetoken:i,region:r}),s.splice(e,1)}))}if(s.length){let e;if(n.length){m=Object.keys(C).sort().pop();const t=C[m];v=t[0].region,delete C[m],t.forEach((e=>e.resetToInitialRequest())),e=[...s,...t]}else e=s;this.createAggregatedRequest(e,d,m,v)}Object.values(C).forEach((e=>{this.attachToServiceRequest(d,e),this.attachToServiceRequest(h,e),this.createAggregatedRequest(e,u)}));const S=new Set,R=new Set;if(t&&r.length&&(t.channels.removed||t.channelGroups.removed)){const s=null!==(o=t.channelGroups.removed)&&void 0!==o?o:[],n=null!==(c=t.channels.removed)&&void 0!==c?c:[],i=r[0].client;e.filter((e=>e.remove&&e.sendLeave)).forEach((e=>{const{channels:t,channelGroups:r}=e.request;s.forEach((e=>r.includes(e)&&S.add(e))),n.forEach((e=>t.includes(e)&&R.add(e)))})),p=_(i,[...R],[...S])}(l.length||d.length||u.length||g.length||p)&&this.dispatchEvent(new q(l,[...d,...u],g,p))}refreshInternalState(){const e=new Set,t=new Set;Object.entries(this.requests).forEach((([s,n])=>{var r,i;const a=null!==(r=(i=this.clientsState)[s])&&void 0!==r?r:i[s]={channels:new Set,channelGroups:new Set};n.channelGroups.forEach(a.channelGroups.add,a.channelGroups),n.channels.forEach(a.channels.add,a.channels),n.channelGroups.forEach(e.add,e),n.channels.forEach(t.add,t)}));const s=this.subscriptionStateChanges(t,e);this.channelGroups=e,this.channels=t;const n=Object.values(this.requests).flat().filter((e=>!!e.accessToken)).map((e=>e.accessToken)).sort(L.compare);return n&&n.length>0&&(this.accessToken=n.pop()),s}addListenersForRequestEvents(e){const t=this.requestListenersAbort[e.identifier]=new AbortController,s=()=>{if(this.removeListenersFromRequestEvents(e),!e.isServiceRequest){if(this.requests[e.client.identifier]){this.lastCompletedRequest[e.client.identifier]=e,delete this.requests[e.client.identifier];const t=this.clientsForInvalidation.indexOf(e.client.identifier);t>0&&(this.clientsForInvalidation.splice(t,1),delete this.lastCompletedRequest[e.client.identifier],delete this.clientsState[e.client.identifier],Object.keys(this.clientsState).length||this.dispatchEvent(new v))}return}const t=this.serviceRequests.indexOf(e);t>=0&&this.serviceRequests.splice(t,1)};e.addEventListener(n.Success,s,{signal:t.signal,once:!0}),e.addEventListener(n.Error,s,{signal:t.signal,once:!0}),e.addEventListener(n.Canceled,s,{signal:t.signal,once:!0})}removeListenersFromRequestEvents(e){this.requestListenersAbort[e.request.identifier]&&(this.requestListenersAbort[e.request.identifier].abort(),delete this.requestListenersAbort[e.request.identifier])}subscriptionStateChanges(e,t){const s=0===this.channelGroups.size&&0===this.channels.size,n={channelGroups:{},channels:{}},r=[],i=[],a=[],o=[];for(const e of t)this.channelGroups.has(e)||i.push(e);for(const t of e)this.channels.has(t)||o.push(t);if(!s){for(const e of this.channelGroups)t.has(e)||r.push(e);for(const t of this.channels)e.has(t)||a.push(t)}return(o.length||a.length)&&(n.channels=Object.assign(Object.assign({},o.length?{added:o}:{}),a.length?{removed:a}:{})),(i.length||r.length)&&(n.channelGroups=Object.assign(Object.assign({},i.length?{added:i}:{}),r.length?{removed:r}:{})),0===Object.keys(n.channelGroups).length&&0===Object.keys(n.channels).length?void 0:n}squashSameClientRequests(e){if(!e.length||1===e.length)return e;const t=e.sort(((e,t)=>e.creationDate-t.creationDate));return Object.values(t.reduce(((e,t)=>(e[t.client.identifier]=t,e)),{}))}attachToServiceRequest(e,t){e.length&&t.length&&[...t].forEach((s=>{for(const n of e){if(s.serviceRequest||!s.isSubsetOf(n)||s.isInitialSubscribe&&!n.isInitialSubscribe)continue;s.serviceRequest=n;const e=t.indexOf(s);t.splice(e,1);break}}))}createAggregatedRequest(e,t,s,n){if(0===e.length)return;const r=F.fromRequests(e,this.accessToken,s,n);this.addListenersForRequestEvents(r),e.forEach((e=>e.serviceRequest=r)),this.serviceRequests.push(r),t.push(r)}}function G(e,t,s,n){return new(s||(s=Promise))((function(r,i){function a(e){try{c(n.next(e))}catch(e){i(e)}}function o(e){try{c(n.throw(e))}catch(e){i(e)}}function c(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(a,o)}c((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class $ extends EventTarget{sendRequest(e,t,s,n){e.handleProcessingStarted(),e.cancellable&&(e.fetchAbortController=new AbortController);const r=e.asFetchRequest;(()=>{G(this,void 0,void 0,(function*(){Promise.race([fetch(r,Object.assign(Object.assign({},e.fetchAbortController?{signal:e.fetchAbortController.signal}:{}),{keepalive:!0})),e.requestTimeoutTimer()]).then((e=>e.arrayBuffer().then((t=>[e,t])))).then((e=>n?n(e):e)).then((e=>{e[0].status>=400?s(r,this.requestProcessingError(void 0,e)):t(r,this.requestProcessingSuccess(e))})).catch((t=>{let n=t;if("string"==typeof t){const e=t.toLowerCase();n=new Error(t),!e.includes("timeout")&&e.includes("cancel")&&(n.name="AbortError")}e.stopRequestTimeoutTimer(),s(r,this.requestProcessingError(n))}))}))})()}requestProcessingSuccess(e){var t;const[s,n]=e,r=n.byteLength>0?n:void 0,i=parseInt(null!==(t=s.headers.get("Content-Length"))&&void 0!==t?t:"0",10),a=s.headers.get("Content-Type"),o={};return s.headers.forEach(((e,t)=>o[t.toLowerCase()]=e.toLowerCase())),{type:"request-process-success",clientIdentifier:"",identifier:"",url:"",response:{contentLength:i,contentType:a,headers:o,status:s.status,body:r}}}requestProcessingError(e,t){if(t)return Object.assign(Object.assign({},this.requestProcessingSuccess(t)),{type:"request-process-error"});let s="NETWORK_ISSUE",n="Unknown error",r="Error";e&&e instanceof Error&&(n=e.message,r=e.name);const i=n.toLowerCase();return i.includes("timeout")?s="TIMEOUT":("AbortError"===r||i.includes("aborted")||i.includes("cancel"))&&(n="Request aborted",s="ABORTED"),{type:"request-process-error",clientIdentifier:"",identifier:"",url:"",error:{name:r,type:s,message:n}}}encodeString(e){return encodeURIComponent(e).replace(/[!~*'()]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`))}}class D extends ${constructor(e){super(),this.clientsManager=e,this.requestsChangeAggregationQueue={},this.clientAbortControllers={},this.subscriptionStates={},this.addEventListenersForClientsManager(e)}requestsChangeAggregationQueueForClient(e){for(const t of Object.keys(this.requestsChangeAggregationQueue)){const{changes:s}=this.requestsChangeAggregationQueue[t];if(Array.from(s).some((t=>t.clientIdentifier===e.identifier)))return[t,s]}return[void 0,new Set]}moveClient(e){const[t,s]=this.requestsChangeAggregationQueueForClient(e);let n=this.subscriptionStateForClient(e);const r=null==n?void 0:n.requestForClient(e);if(!n&&!s.size)return;n&&n.invalidateClient(e);let i=null==r?void 0:r.asIdentifier;if(!i&&s.size){const[e]=s;i=e.request.asIdentifier}if(!i)return;if(r&&(r.serviceRequest=void 0,n.processChanges([new x(e.identifier,r,!0,!1,!0)]),n=this.subscriptionStateForIdentifier(i),r.resetToInitialRequest(),n.processChanges([new x(e.identifier,r,!1,!1)])),!s.size||!this.requestsChangeAggregationQueue[t])return;this.startAggregationTimer(i);const a=this.requestsChangeAggregationQueue[t].changes;x.squashedChanges([...s]).filter((t=>t.clientIdentifier!==e.identifier||t.remove)).forEach(a.delete,a);const{changes:o}=this.requestsChangeAggregationQueue[i];x.squashedChanges([...s]).filter((t=>t.clientIdentifier===e.identifier&&!t.request.completed&&t.request.canceled&&!t.remove)).forEach(o.add,o)}removeClient(e,t,s,n=!1){var r;const[i,a]=this.requestsChangeAggregationQueueForClient(e),o=this.subscriptionStateForClient(e),c=null==o?void 0:o.requestForClient(e,n);if(!o&&!a.size)return;const h=null!==(r=o&&o.identifier)&&void 0!==r?r:i;if(a.size&&this.requestsChangeAggregationQueue[h]){const{changes:e}=this.requestsChangeAggregationQueue[h];a.forEach(e.delete,e),this.stopAggregationTimerIfEmptyQueue(h)}c&&(c.serviceRequest=void 0,t?(this.startAggregationTimer(h),this.enqueueForAggregation(e,c,!0,s,n)):o&&o.processChanges([new x(e.identifier,c,!0,s,n)]))}enqueueForAggregation(e,t,s,n,r=!1){const i=t.asIdentifier;this.startAggregationTimer(i);const{changes:a}=this.requestsChangeAggregationQueue[i];a.add(new x(e.identifier,t,s,n,r))}startAggregationTimer(e){this.requestsChangeAggregationQueue[e]||(this.requestsChangeAggregationQueue[e]={timeout:setTimeout((()=>this.handleDelayedAggregation(e)),50),changes:new Set})}stopAggregationTimerIfEmptyQueue(e){const t=this.requestsChangeAggregationQueue[e];t&&0===t.changes.size&&(t.timeout&&clearTimeout(t.timeout),delete this.requestsChangeAggregationQueue[e])}handleDelayedAggregation(e){if(!this.requestsChangeAggregationQueue[e])return;const t=this.subscriptionStateForIdentifier(e),s=[...this.requestsChangeAggregationQueue[e].changes];delete this.requestsChangeAggregationQueue[e],t.processChanges(s)}subscriptionStateForIdentifier(e){let t=this.subscriptionStates[e];return t||(t=this.subscriptionStates[e]=new U(e),this.addListenerForSubscriptionStateEvents(t)),t}addEventListenersForClientsManager(s){s.addEventListener(t.Registered,(t=>{const{client:s}=t,n=new AbortController;this.clientAbortControllers[s.identifier]=n,s.addEventListener(e.IdentityChange,(e=>{e instanceof o&&(!!e.oldUserId!=!!e.newUserId||e.oldUserId&&e.newUserId&&e.newUserId!==e.oldUserId)&&this.moveClient(s)}),{signal:n.signal}),s.addEventListener(e.AuthChange,(e=>{var t;e instanceof c&&(!!e.oldAuth!=!!e.newAuth||e.oldAuth&&e.newAuth&&!e.oldAuth.equalTo(e.newAuth)?this.moveClient(s):e.oldAuth&&e.newAuth&&e.oldAuth.equalTo(e.newAuth)&&(null===(t=this.subscriptionStateForClient(s))||void 0===t||t.updateClientAccessToken(e.newAuth)))}),{signal:n.signal}),s.addEventListener(e.SendSubscribeRequest,(e=>{e instanceof l&&this.enqueueForAggregation(e.client,e.request,!1,!1)}),{signal:n.signal}),s.addEventListener(e.CancelSubscribeRequest,(e=>{e instanceof u&&this.enqueueForAggregation(e.client,e.request,!0,!1)}),{signal:n.signal}),s.addEventListener(e.SendLeaveRequest,(e=>{if(!(e instanceof g))return;const t=this.patchedLeaveRequest(e.request);t&&this.sendRequest(t,((e,s)=>t.handleProcessingSuccess(e,s)),((e,s)=>t.handleProcessingError(e,s)))}),{signal:n.signal})})),s.addEventListener(t.Unregistered,(e=>{const{client:t,withLeave:s}=e,n=this.clientAbortControllers[t.identifier];delete this.clientAbortControllers[t.identifier],n&&n.abort(),this.removeClient(t,!1,s,!0)}))}addListenerForSubscriptionStateEvents(e){const t=new AbortController;e.addEventListener(s.Changed,(e=>{const{requestsWithInitialResponse:t,canceledRequests:s,newRequests:n,leaveRequest:r}=e;s.forEach((e=>e.cancel("Cancel request"))),n.forEach((e=>{this.sendRequest(e,((t,s)=>e.handleProcessingSuccess(t,s)),((t,s)=>e.handleProcessingError(t,s)),e.isInitialSubscribe&&"0"!==e.timetokenOverride?t=>this.patchInitialSubscribeResponse(t,e.timetokenOverride,e.timetokenRegionOverride):void 0)})),t.forEach((e=>{const{request:t,timetoken:s,region:n}=e;t.handleProcessingStarted(),this.makeResponseOnHandshakeRequest(t,s,n)})),r&&this.sendRequest(r,((e,t)=>r.handleProcessingSuccess(e,t)),((e,t)=>r.handleProcessingError(e,t)))}),{signal:t.signal}),e.addEventListener(s.Invalidated,(()=>{delete this.subscriptionStates[e.identifier],t.abort()}),{signal:t.signal,once:!0})}subscriptionStateForClient(e){return Object.values(this.subscriptionStates).find((t=>t.hasStateForClient(e)))}patchedLeaveRequest(e){const t=this.subscriptionStateForClient(e.client);if(!t)return void e.cancel();const s=t.uniqueStateForClient(e.client,e.channels,e.channelGroups),n=_(e.client,s.channels,s.channelGroups);return n&&(e.serviceRequest=n),n}makeResponseOnHandshakeRequest(e,t,s){const n=(new TextEncoder).encode(`{"t":{"t":"${t}","r":${null!=s?s:"0"}},"m":[]}`);e.handleProcessingSuccess(e.asFetchRequest,{type:"request-process-success",clientIdentifier:"",identifier:"",url:"",response:{contentType:'text/javascript; charset="UTF-8"',contentLength:n.length,headers:{"content-type":'text/javascript; charset="UTF-8"',"content-length":`${n.length}`},status:200,body:n}})}patchInitialSubscribeResponse(e,t,s){if(void 0===t||"0"===t||e[0].status>=400)return e;let n;const r=e[0];let i=r,a=e[1];try{n=JSON.parse(D.textDecoder.decode(a))}catch(t){return console.error(`Subscribe response parse error: ${t}`),e}n.t.t=t,s&&(n.t.r=parseInt(s,10));try{if(a=D.textEncoder.encode(JSON.stringify(n)).buffer,a.byteLength){const e=new Headers(r.headers);e.set("Content-Length",`${a.byteLength}`),i=new Response(a,{status:r.status,statusText:r.statusText,headers:e})}}catch(t){return console.error(`Subscribe serialization error: ${t}`),e}return a.byteLength>0?[i,a]:e}}var K,H;D.textDecoder=new TextDecoder,D.textEncoder=new TextEncoder,function(e){e.Heartbeat="heartbeat",e.Invalidated="invalidated"}(K||(K={}));class B extends CustomEvent{constructor(e){super(K.Heartbeat,{detail:e})}get request(){return this.detail}clone(){return new B(this.request)}}class N extends CustomEvent{constructor(){super(K.Invalidated)}clone(){return new N}}class Q extends O{static fromTransportRequest(e,t,s){return new Q(e,t,s)}static fromCachedState(e,t,s,n,r,i){if(n.length||s.length){const t=e.path.split("/");t[6]=n.length?[...n].sort().join(","):",",e.path=t.join("/")}return s.length&&(e.queryParameters["channel-group"]=[...s].sort().join(",")),r&&Object.keys(r).length?e.queryParameters.state=JSON.stringify(r):delete e.queryParameters.aggregatedState,i&&(e.queryParameters.auth=i.toString()),e.identifier=A.createUUID(),new Q(e,t,i)}constructor(e,t,s){const n=Q.channelGroupsFromRequest(e).filter((e=>!e.endsWith("-pnpres"))),r=Q.channelsFromRequest(e).filter((e=>!e.endsWith("-pnpres")));if(super(e,t,e.queryParameters.uuid,r,n,s),!e.queryParameters.state||0===e.queryParameters.state.length)return;const i=JSON.parse(e.queryParameters.state);for(const e of Object.keys(i))this.channels.includes(e)||this.channelGroups.includes(e)||delete i[e];this.state=i}get asIdentifier(){const e=this.accessToken?this.accessToken.asIdentifier:void 0;return`${this.userId}-${this.subscribeKey}${e?`-${e}`:""}`}toString(){return`HeartbeatRequest { channels: [${this.channels.length?this.channels.map((e=>`'${e}'`)).join(", "):""}], channelGroups: [${this.channelGroups.length?this.channelGroups.map((e=>`'${e}'`)).join(", "):""}] }`}toJSON(){return this.toString()}static channelsFromRequest(e){const t=e.path.split("/")[6];return","===t?[]:t.split(",").filter((e=>e.length>0))}static channelGroupsFromRequest(e){if(!e.queryParameters||!e.queryParameters["channel-group"])return[];const t=e.queryParameters["channel-group"];return 0===t.length?[]:t.split(",").filter((e=>e.length>0))}}class W extends EventTarget{constructor(e){super(),this.identifier=e,this.clientsState={},this.requests={},this.lastHeartbeatTimestamp=0,this.canSendBackupHeartbeat=!0,this.isAccessDeniedError=!1,this._interval=0}set interval(e){const t=this._interval!==e;this._interval=e,t&&(0===e?this.stopTimer():this.startTimer())}set accessToken(e){if(!e)return void(this._accessToken=e);const t=Object.values(this.requests).filter((e=>!!e.accessToken)).map((e=>e.accessToken));t.push(e),this._accessToken=t.sort(L.compare).pop(),this.isAccessDeniedError&&(this.canSendBackupHeartbeat=!0,this.startTimer(this.presenceTimerTimeout()))}stateForClient(e){if(!this.clientsState[e.identifier])return;const t=this.clientsState[e.identifier];return t?{channels:[...t.channels],channelGroups:[...t.channelGroups],state:t.state}:{channels:[],channelGroups:[]}}requestForClient(e){return this.requests[e.identifier]}addClientRequest(e,t){this.requests[e.identifier]=t,this.clientsState[e.identifier]={channels:t.channels,channelGroups:t.channelGroups},t.state&&(this.clientsState[e.identifier].state=Object.assign({},t.state));const s=Object.values(this.requests).filter((e=>!!e.accessToken)).map((e=>e.accessToken)).sort(L.compare);s&&s.length>0&&(this._accessToken=s.pop()),this.sendAggregatedHeartbeat(t)}removeClient(e){delete this.clientsState[e.identifier],delete this.requests[e.identifier],Object.keys(this.clientsState).length||(this.stopTimer(),this.dispatchEvent(new N))}removeFromClientState(e,t,s){const n=this.clientsState[e.identifier];n&&(n.channelGroups=n.channelGroups.filter((e=>!s.includes(e))),n.channels=n.channels.filter((e=>!t.includes(e))),0!==n.channels.length||0!==n.channelGroups.length?n.state&&Object.keys(n.state).forEach((e=>{n.channels.includes(e)||n.channelGroups.includes(e)||delete n.state[e]})):this.removeClient(e))}startTimer(e){this.stopTimer(),0!==Object.keys(this.clientsState).length&&(this.timeout=setTimeout((()=>this.handlePresenceTimer()),1e3*(null!=e?e:this._interval)))}stopTimer(){this.timeout&&clearTimeout(this.timeout),this.timeout=void 0}sendAggregatedHeartbeat(e){if(0!==this.lastHeartbeatTimestamp){const t=this.lastHeartbeatTimestamp+1e3*this._interval;let s=.05*this._interval;this._interval-s<3&&(s=0);if(t-Date.now()>1e3*s){if(e&&this.previousRequestResult){const t=e.asFetchRequest,s=Object.assign(Object.assign({},this.previousRequestResult),{clientIdentifier:e.client.identifier,identifier:e.identifier,url:t.url});return e.handleProcessingStarted(),void e.handleProcessingSuccess(t,s)}if(!e)return}}const t=Object.values(this.requests),s=t[Math.floor(Math.random()*t.length)],n=Object.assign({},s.request);let r={};const i=new Set,a=new Set;Object.values(this.clientsState).forEach((e=>{e.state&&(r=Object.assign(Object.assign({},r),e.state)),e.channelGroups.forEach(i.add,i),e.channels.forEach(a.add,a)})),this.lastHeartbeatTimestamp=Date.now();const o=Q.fromCachedState(n,t[0].subscribeKey,[...i],[...a],Object.keys(r).length>0?r:void 0,this._accessToken);Object.values(this.requests).forEach((e=>!e.serviceRequest&&(e.serviceRequest=o))),this.addListenersForRequest(o),this.dispatchEvent(new B(o)),e&&this.startTimer()}addListenersForRequest(e){const t=new AbortController,s=e=>{if(t.abort(),e instanceof C){const{response:t}=e;this.previousRequestResult=t}else if(e instanceof S){const{error:t}=e;this.canSendBackupHeartbeat=!0,this.isAccessDeniedError=!1,t.response&&t.response.status>=400&&t.response.status<500&&(this.isAccessDeniedError=403===t.response.status,this.canSendBackupHeartbeat=!1)}};e.addEventListener(n.Success,s,{signal:t.signal,once:!0}),e.addEventListener(n.Error,s,{signal:t.signal,once:!0}),e.addEventListener(n.Canceled,s,{signal:t.signal,once:!0})}handlePresenceTimer(){if(0===Object.keys(this.clientsState).length||!this.canSendBackupHeartbeat)return;const e=this.presenceTimerTimeout();this.sendAggregatedHeartbeat(),this.startTimer(e)}presenceTimerTimeout(){const e=(Date.now()-this.lastHeartbeatTimestamp)/1e3;let t=this._interval;return e0&&e.heartbeatInterval>0&&e.heartbeatInterval{const{client:s}=t,n=new AbortController;this.clientAbortControllers[s.identifier]=n,s.addEventListener(e.Disconnect,(()=>this.removeClient(s)),{signal:n.signal}),s.addEventListener(e.IdentityChange,(e=>{if(e instanceof o&&(!!e.oldUserId!=!!e.newUserId||e.oldUserId&&e.newUserId&&e.newUserId!==e.oldUserId)){const t=this.heartbeatStateForClient(s),n=t?t.requestForClient(s):void 0;n&&(n.userId=e.newUserId),this.moveClient(s)}}),{signal:n.signal}),s.addEventListener(e.AuthChange,(e=>{if(!(e instanceof c))return;const t=this.heartbeatStateForClient(s),n=t?t.requestForClient(s):void 0;n&&(n.accessToken=e.newAuth),(!!e.oldAuth!=!!e.newAuth||e.oldAuth&&e.newAuth&&!e.newAuth.equalTo(e.oldAuth))&&this.moveClient(s)}),{signal:n.signal}),s.addEventListener(e.HeartbeatIntervalChange,(e=>{var t;const n=e,r=this.heartbeatStateForClient(s);r&&(r.interval=null!==(t=n.newInterval)&&void 0!==t?t:0)}),{signal:n.signal}),s.addEventListener(e.SendHeartbeatRequest,(e=>this.addClient(s,e.request)),{signal:n.signal}),s.addEventListener(e.SendLeaveRequest,(e=>{const{request:t}=e,n=this.heartbeatStateForClient(s);n&&n.removeFromClientState(s,t.channels,t.channelGroups)}),{signal:n.signal})})),s.addEventListener(t.Unregistered,(e=>{const{client:t}=e,s=this.clientAbortControllers[t.identifier];delete this.clientAbortControllers[t.identifier],s&&s.abort(),this.removeClient(t)}))}addListenerForHeartbeatStateEvents(e){const t=new AbortController;e.addEventListener(K.Heartbeat,(e=>{const{request:t}=e;this.sendRequest(t,((e,s)=>t.handleProcessingSuccess(e,s)),((e,s)=>t.handleProcessingError(e,s)))}),{signal:t.signal}),e.addEventListener(K.Invalidated,(()=>{delete this.heartbeatStates[e.identifier],t.abort()}),{signal:t.signal,once:!0})}}M.textDecoder=new TextDecoder,function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Info=2]="Info",e[e.Warn=3]="Warn",e[e.Error=4]="Error",e[e.None=5]="None"}(H||(H={}));class z{constructor(e,t){this.minLogLevel=e,this.port=t}debug(e){this.log(e,H.Debug)}error(e){this.log(e,H.Error)}info(e){this.log(e,H.Info)}trace(e){this.log(e,H.Trace)}warn(e){this.log(e,H.Warn)}log(e,t){if(t{"client-unregister"===e.data.type?this.handleUnregisterEvent():"client-update"===e.data.type?this.handleConfigurationUpdateEvent(e.data):"send-request"===e.data.type?this.handleSendRequestEvent(e.data):"cancel-request"===e.data.type?this.handleCancelRequestEvent(e.data):"client-disconnect"===e.data.type?this.handleDisconnectEvent():"client-pong"===e.data.type&&this.handlePongEvent()}),{signal:this.listenerAbortController.signal})}handleUnregisterEvent(){this.invalidate(),this.dispatchEvent(new i(this))}handleConfigurationUpdateEvent(e){const{userId:t,accessToken:s,preProcessedToken:n,heartbeatInterval:r,workerLogLevel:i}=e;if(this.logger.minLogLevel=i,this.logger.debug((()=>({messageType:"object",message:{userId:t,authKey:s,token:n,heartbeatInterval:r,workerLogLevel:i},details:"Update client configuration with parameters:"}))),s||this.accessToken){const e=s?new L(s,(null!=n?n:{}).token,(null!=n?n:{}).expiration):void 0;if(!!e!=!!this.accessToken||e&&this.accessToken&&!e.equalTo(this.accessToken,!0)){const t=this._accessToken;this._accessToken=e,Object.values(this.requests).filter((e=>!e.completed&&e instanceof F||e instanceof Q)).forEach((t=>t.accessToken=e)),this.dispatchEvent(new c(this,e,t))}}if(this.userId!==t){const e=this.userId;this.userId=t,Object.values(this.requests).filter((e=>!e.completed&&e instanceof F||e instanceof Q)).forEach((e=>e.userId=t)),this.dispatchEvent(new o(this,e,t))}if(this._heartbeatInterval!==r){const e=this._heartbeatInterval;this._heartbeatInterval=r,this.dispatchEvent(new h(this,r,e))}}handleSendRequestEvent(e){var t;let s;if(!this._accessToken&&(null===(t=e.request.queryParameters)||void 0===t?void 0:t.auth)&&e.preProcessedToken){const t=e.request.queryParameters.auth;this._accessToken=new L(t,e.preProcessedToken.token,e.preProcessedToken.expiration)}e.request.path.startsWith("/v2/subscribe")?F.useCachedState(e.request)&&(this.cachedSubscriptionChannelGroups.length||this.cachedSubscriptionChannels.length)?s=F.fromCachedState(e.request,this.subKey,this.cachedSubscriptionChannelGroups,this.cachedSubscriptionChannels,this.cachedSubscriptionState,this.accessToken):(s=F.fromTransportRequest(e.request,this.subKey,this.accessToken),this.cachedSubscriptionChannelGroups=[...s.channelGroups],this.cachedSubscriptionChannels=[...s.channels],s.state?this.cachedSubscriptionState=Object.assign({},s.state):this.cachedSubscriptionState=void 0):s=e.request.path.endsWith("/heartbeat")?Q.fromTransportRequest(e.request,this.subKey,this.accessToken):P.fromTransportRequest(e.request,this.subKey,this.accessToken),s.client=this,this.requests[s.request.identifier]=s,this._origin||(this._origin=s.origin),this.listenRequestCompletion(s),this.dispatchEvent(this.eventWithRequest(s))}handleCancelRequestEvent(e){if(!this.requests[e.identifier])return;this.requests[e.identifier].cancel("Cancel request")}handleDisconnectEvent(){this.dispatchEvent(new a(this))}handlePongEvent(){this._lastPongEvent=Date.now()/1e3}listenRequestCompletion(e){const t=new AbortController,s=s=>{delete this.requests[e.identifier],t.abort(),s instanceof C?this.postEvent(s.response):s instanceof S?this.postEvent(s.error):s instanceof R&&(this.postEvent(this.requestCancelError(e)),!this._invalidated&&e instanceof F&&this.dispatchEvent(new u(e.client,e)))};e.addEventListener(n.Success,s,{signal:t.signal,once:!0}),e.addEventListener(n.Error,s,{signal:t.signal,once:!0}),e.addEventListener(n.Canceled,s,{signal:t.signal,once:!0})}cancelRequests(){Object.values(this.requests).forEach((e=>e.cancel()))}eventWithRequest(e){let t;return t=e instanceof F?new l(this,e):e instanceof Q?new d(this,e):new g(this,e),t}requestCancelError(e){return{type:"request-process-error",clientIdentifier:this.identifier,identifier:e.request.identifier,url:e.asFetchRequest.url,error:{name:"AbortError",type:"ABORTED",message:"Request aborted"}}}}class V extends EventTarget{constructor(e){super(),this.sharedWorkerIdentifier=e,this.timeouts={},this.clients={},this.clientBySubscribeKey={}}createClient(e,t){var s;if(this.clients[e.clientIdentifier])return this.clients[e.clientIdentifier];const n=new J(e.clientIdentifier,e.subscriptionKey,e.userId,t,e.workerLogLevel,e.heartbeatInterval);return this.registerClient(n),e.workerOfflineClientsCheckInterval&&this.startClientTimeoutCheck(e.subscriptionKey,e.workerOfflineClientsCheckInterval,null!==(s=e.workerUnsubscribeOfflineClients)&&void 0!==s&&s),n}registerClient(e){this.clients[e.identifier]={client:e,abortController:new AbortController},this.clientBySubscribeKey[e.subKey]?this.clientBySubscribeKey[e.subKey].push(e):this.clientBySubscribeKey[e.subKey]=[e],this.forEachClient(e.subKey,(t=>t.logger.debug(`'${e.identifier}' client registered with '${this.sharedWorkerIdentifier}' shared worker (${this.clientBySubscribeKey[e.subKey].length} active clients).`))),this.subscribeOnClientEvents(e),this.dispatchEvent(new p(e))}unregisterClient(e,t=!1,s=!1){if(!this.clients[e.identifier])return;this.clients[e.identifier].abortController&&this.clients[e.identifier].abortController.abort(),delete this.clients[e.identifier];const n=this.clientBySubscribeKey[e.subKey];if(n){const t=n.indexOf(e);n.splice(t,1),0===n.length&&(delete this.clientBySubscribeKey[e.subKey],this.stopClientTimeoutCheck(e))}this.forEachClient(e.subKey,(t=>t.logger.debug(`'${this.sharedWorkerIdentifier}' shared worker unregistered '${e.identifier}' client (${this.clientBySubscribeKey[e.subKey].length} active clients).`))),s||e.invalidate(),this.dispatchEvent(new f(e,t))}startClientTimeoutCheck(e,t,s){this.timeouts[e]||(this.forEachClient(e,(e=>e.logger.debug(`Setup PubNub client ping for every ${t} seconds.`))),this.timeouts[e]={interval:t,unsubscribeOffline:s,timeout:setTimeout((()=>this.handleTimeoutCheck(e)),500*t-1)})}stopClientTimeoutCheck(e){this.timeouts[e.subKey]&&(this.timeouts[e.subKey].timeout&&clearTimeout(this.timeouts[e.subKey].timeout),delete this.timeouts[e.subKey])}handleTimeoutCheck(e){if(!this.timeouts[e])return;const t=this.timeouts[e].interval;[...this.clientBySubscribeKey[e]].forEach((s=>{s.lastPingRequest&&Date.now()/1e3-s.lastPingRequest-.2>.5*t&&(s.logger.warn("PubNub clients timeout timer fired after throttling past due time."),s.lastPingRequest=void 0),s.lastPingRequest&&(!s.lastPongEvent||Math.abs(s.lastPongEvent-s.lastPingRequest)>t)&&(this.unregisterClient(s,this.timeouts[e].unsubscribeOffline),this.forEachClient(e,(e=>{e.identifier!==s.identifier&&e.logger.debug(`'${s.identifier}' client is inactive. Invalidating...`)}))),this.clients[s.identifier]&&(s.lastPingRequest=Date.now()/1e3,s.postEvent({type:"shared-worker-ping"}))})),this.timeouts[e]&&(this.timeouts[e].timeout=setTimeout((()=>this.handleTimeoutCheck(e)),500*t))}subscribeOnClientEvents(t){t.addEventListener(e.Unregister,(()=>this.unregisterClient(t,!!this.timeouts[t.subKey]&&this.timeouts[t.subKey].unsubscribeOffline,!0)),{signal:this.clients[t.identifier].abortController.signal,once:!0})}forEachClient(e,t){this.clientBySubscribeKey[e]&&this.clientBySubscribeKey[e].forEach(t)}}const X=new V(A.createUUID());new D(X),new M(X),self.onconnect=e=>{e.ports.forEach((e=>{e.start(),e.onmessage=t=>{const s=t.data;"client-register"===s.type&&X.createClient(s,e)},e.postMessage({type:"shared-worker-connected"})}))}})); diff --git a/lib/core/components/configuration.js b/lib/core/components/configuration.js index 8b76066fc..cc67a2ad6 100644 --- a/lib/core/components/configuration.js +++ b/lib/core/components/configuration.js @@ -134,6 +134,10 @@ const makeConfiguration = (base, setupCryptoModule) => { getUseRandomIVs() { return base.useRandomIVs; }, + isSharedWorkerEnabled() { + // @ts-expect-error: Access field from web-based SDK configuration. + return base.sdkFamily === 'Web' && base['subscriptionWorkerUrl']; + }, getKeepPresenceChannelsInPresenceRequests() { // @ts-expect-error: Access field from web-based SDK configuration. return base.sdkFamily === 'Web' && base['subscriptionWorkerUrl']; @@ -164,7 +168,7 @@ const makeConfiguration = (base, setupCryptoModule) => { return base.PubNubFile; }, get version() { - return '9.8.4'; + return '9.9.0'; }, getVersion() { return this.version; diff --git a/lib/core/components/logger-manager.js b/lib/core/components/logger-manager.js index 17affaf06..3789f1af1 100644 --- a/lib/core/components/logger-manager.js +++ b/lib/core/components/logger-manager.js @@ -18,6 +18,15 @@ class LoggerManager { * @internal */ constructor(pubNubId, minLogLevel, loggers) { + /** + * Keeping track of previous entry timestamp. + * + * This information will be used to make sure that multiple sequential entries doesn't have same timestamp. Adjustment + * on .001 will be done to make it possible to properly stort entries. + * + * @internal + */ + this.previousEntryTimestamp = 0; this.pubNubId = pubNubId; this.minLogLevel = minLogLevel; this.loggers = loggers; @@ -100,8 +109,15 @@ class LoggerManager { // Check whether a log message should be handled at all or not. if (logLevel < this.minLogLevel || this.loggers.length === 0) return; + const date = new Date(); + if (date.getTime() <= this.previousEntryTimestamp) { + this.previousEntryTimestamp++; + date.setTime(this.previousEntryTimestamp); + } + else + this.previousEntryTimestamp = date.getTime(); const level = logger_1.LogLevel[logLevel].toLowerCase(); - const message = Object.assign({ timestamp: new Date(), pubNubId: this.pubNubId, level: logLevel, minimumLevel: this.minLogLevel, location }, (typeof messageFactory === 'function' ? messageFactory() : { messageType: 'text', message: messageFactory })); + const message = Object.assign({ timestamp: date, pubNubId: this.pubNubId, level: logLevel, minimumLevel: this.minLogLevel, location }, (typeof messageFactory === 'function' ? messageFactory() : { messageType: 'text', message: messageFactory })); this.loggers.forEach((logger) => logger[level](message)); } } diff --git a/lib/core/components/subscription-manager.js b/lib/core/components/subscription-manager.js index 5f4a943db..9a31bffd1 100644 --- a/lib/core/components/subscription-manager.js +++ b/lib/core/components/subscription-manager.js @@ -20,8 +20,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SubscriptionManager = void 0; -const utils_1 = require("../utils"); const subscribe_1 = require("../endpoints/subscribe"); +const utils_1 = require("../utils"); const reconnection_manager_1 = require("./reconnection_manager"); const categories_1 = __importDefault(require("../constants/categories")); const deduping_manager_1 = require("./deduping_manager"); @@ -38,6 +38,13 @@ class SubscriptionManager { this.subscribeCall = subscribeCall; this.heartbeatCall = heartbeatCall; this.leaveCall = leaveCall; + /** + * Whether user code in event handlers requested disconnection or not. + * + * Won't continue subscription loop if user requested disconnection/unsubscribe from all in response to received + * event. + */ + this.disconnectedWhileHandledEvent = false; configuration.logger().trace('SubscriptionManager', 'Create manager.'); this.reconnectionManager = new reconnection_manager_1.ReconnectionManager(time); this.dedupingManager = new deduping_manager_1.DedupingManager(this.configuration); @@ -83,6 +90,9 @@ class SubscriptionManager { // endregion // region Subscription disconnect() { + // Potentially called during received events handling. + // Mark to prevent subscription loop continuation in subscribe response handler. + this.disconnectedWhileHandledEvent = true; this.stopSubscribeLoop(); this.stopHeartbeatTimer(); this.reconnectionManager.stopPolling(); @@ -168,6 +178,22 @@ class SubscriptionManager { // There is no need to unsubscribe to empty list of data sources. if (actualChannels.size === 0 && actualChannelGroups.size === 0) return; + const lastTimetoken = this.lastTimetoken; + const currentTimetoken = this.currentTimetoken; + if (Object.keys(this.channels).length === 0 && + Object.keys(this.presenceChannels).length === 0 && + Object.keys(this.channelGroups).length === 0 && + Object.keys(this.presenceChannelGroups).length === 0) { + this.lastTimetoken = '0'; + this.currentTimetoken = '0'; + this.referenceTimetoken = null; + this.storedTimetoken = null; + this.region = null; + this.reconnectionManager.stopPolling(); + } + this.reconnect(true); + // Send leave request after long-poll connection closed and loop restarted (the same way as it happens in new + // subscription flow). if (this.configuration.suppressLeaveEvents === false && !isOffline) { channelGroups = Array.from(actualChannelGroups); channels = Array.from(actualChannels); @@ -183,23 +209,13 @@ class SubscriptionManager { else if ('message' in status && typeof status.message === 'string') errorMessage = status.message; } - this.emitStatus(Object.assign(Object.assign({}, restOfStatus), { error: errorMessage !== null && errorMessage !== void 0 ? errorMessage : false, affectedChannels: channels, affectedChannelGroups: channelGroups, currentTimetoken: this.currentTimetoken, lastTimetoken: this.lastTimetoken })); + this.emitStatus(Object.assign(Object.assign({}, restOfStatus), { error: errorMessage !== null && errorMessage !== void 0 ? errorMessage : false, affectedChannels: channels, affectedChannelGroups: channelGroups, currentTimetoken, + lastTimetoken })); }); } - if (Object.keys(this.channels).length === 0 && - Object.keys(this.presenceChannels).length === 0 && - Object.keys(this.channelGroups).length === 0 && - Object.keys(this.presenceChannelGroups).length === 0) { - this.lastTimetoken = '0'; - this.currentTimetoken = '0'; - this.referenceTimetoken = null; - this.storedTimetoken = null; - this.region = null; - this.reconnectionManager.stopPolling(); - } - this.reconnect(true); } unsubscribeAll(isOffline = false) { + this.disconnectedWhileHandledEvent = true; this.unsubscribe({ channels: this.subscribedChannels, channelGroups: this.subscribedChannelGroups, @@ -214,6 +230,7 @@ class SubscriptionManager { * @internal */ startSubscribeLoop(restartOnUnsubscribe = false) { + this.disconnectedWhileHandledEvent = false; this.stopSubscribeLoop(); const channelGroups = [...Object.keys(this.channelGroups)]; const channels = [...Object.keys(this.channels)]; @@ -222,8 +239,8 @@ class SubscriptionManager { // There is no need to start subscription loop for an empty list of data sources. if (channels.length === 0 && channelGroups.length === 0) return; - this.subscribeCall(Object.assign(Object.assign({ channels, - channelGroups, state: this.presenceState, heartbeat: this.configuration.getPresenceTimeout(), timetoken: this.currentTimetoken }, (this.region !== null ? { region: this.region } : {})), (this.configuration.filterExpression ? { filterExpression: this.configuration.filterExpression } : {})), (status, result) => { + this.subscribeCall(Object.assign(Object.assign(Object.assign({ channels, + channelGroups, state: this.presenceState, heartbeat: this.configuration.getPresenceTimeout(), timetoken: this.currentTimetoken }, (this.region !== null ? { region: this.region } : {})), (this.configuration.filterExpression ? { filterExpression: this.configuration.filterExpression } : {})), { onDemand: !this.subscriptionStatusAnnounced || restartOnUnsubscribe }), (status, result) => { this.processSubscribeResponse(status, result); }); if (!restartOnUnsubscribe && this.configuration.useSmartHeartbeat) @@ -354,7 +371,10 @@ class SubscriptionManager { this.emitStatus(errorStatus); } this.region = result.cursor.region; - this.startSubscribeLoop(); + if (!this.disconnectedWhileHandledEvent) + this.startSubscribeLoop(); + else + this.disconnectedWhileHandledEvent = false; } // endregion // region Presence diff --git a/lib/core/constants/categories.js b/lib/core/constants/categories.js index c60d4aea9..0dec2465d 100644 --- a/lib/core/constants/categories.js +++ b/lib/core/constants/categories.js @@ -100,5 +100,12 @@ var StatusCategory; * PubNub client unexpectedly disconnected from the real-time updates streams. */ StatusCategory["PNDisconnectedUnexpectedlyCategory"] = "PNDisconnectedUnexpectedlyCategory"; + // -------------------------------------------------------- + // ------------------ Shared worker events ---------------- + // -------------------------------------------------------- + /** + * SDK will announce when newer shared worker will be 'noticed'. + */ + StatusCategory["PNSharedWorkerUpdatedCategory"] = "PNSharedWorkerUpdatedCategory"; })(StatusCategory || (StatusCategory = {})); exports.default = StatusCategory; diff --git a/lib/core/endpoints/subscribe.js b/lib/core/endpoints/subscribe.js index a06301b8e..dbf13bd39 100644 --- a/lib/core/endpoints/subscribe.js +++ b/lib/core/endpoints/subscribe.js @@ -344,8 +344,10 @@ class SubscribeRequest extends BaseSubscribeRequest { return `/v2/subscribe/${subscribeKey}/${(0, utils_1.encodeNames)((_a = channels === null || channels === void 0 ? void 0 : channels.sort()) !== null && _a !== void 0 ? _a : [], ',')}/0`; } get queryParameters() { - const { channelGroups, filterExpression, heartbeat, state, timetoken, region } = this.parameters; + const { channelGroups, filterExpression, heartbeat, state, timetoken, region, onDemand } = this.parameters; const query = {}; + if (onDemand) + query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) diff --git a/lib/core/endpoints/subscriptionUtils/handshake.js b/lib/core/endpoints/subscriptionUtils/handshake.js index 7f74d0743..7a1110180 100644 --- a/lib/core/endpoints/subscriptionUtils/handshake.js +++ b/lib/core/endpoints/subscriptionUtils/handshake.js @@ -28,8 +28,11 @@ class HandshakeSubscribeRequest extends subscribe_1.BaseSubscribeRequest { return `/v2/subscribe/${subscribeKey}/${(0, utils_1.encodeNames)(channels.sort(), ',')}/0`; } get queryParameters() { - const { channelGroups, filterExpression, state } = this.parameters; + const { channelGroups, filterExpression, state, onDemand } = this + .parameters; const query = { ee: '' }; + if (onDemand) + query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) diff --git a/lib/core/endpoints/subscriptionUtils/receiveMessages.js b/lib/core/endpoints/subscriptionUtils/receiveMessages.js index 93e8379b9..ca0f99c04 100644 --- a/lib/core/endpoints/subscriptionUtils/receiveMessages.js +++ b/lib/core/endpoints/subscriptionUtils/receiveMessages.js @@ -35,8 +35,11 @@ class ReceiveMessagesSubscribeRequest extends subscribe_1.BaseSubscribeRequest { return `/v2/subscribe/${subscribeKey}/${(0, utils_1.encodeNames)(channels.sort(), ',')}/0`; } get queryParameters() { - const { channelGroups, filterExpression, timetoken, region } = this.parameters; + const { channelGroups, filterExpression, timetoken, region, onDemand } = this + .parameters; const query = { ee: '' }; + if (onDemand) + query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) diff --git a/lib/core/pubnub-common.js b/lib/core/pubnub-common.js index 9939b5d02..8e411b2ac 100644 --- a/lib/core/pubnub-common.js +++ b/lib/core/pubnub-common.js @@ -458,6 +458,9 @@ class PubNubCore { * 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. * @@ -1197,6 +1200,9 @@ class PubNubCore { */ makeSubscribe(parameters, callback) { if (process.env.SUBSCRIBE_MANAGER_MODULE !== 'disabled') { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) + parameters.onDemand = false; const request = new subscribe_1.SubscribeRequest(Object.assign(Object.assign({}, parameters), { keySet: this._configuration.keySet, crypto: this._configuration.getCryptoModule(), getFileUrl: this.getFileUrl.bind(this) })); this.sendRequest(request, (status, result) => { var _a; @@ -1358,6 +1364,9 @@ class PubNubCore { subscribeHandshake(parameters) { return __awaiter(this, void 0, void 0, function* () { if (process.env.SUBSCRIBE_EVENT_ENGINE_MODULE !== 'disabled') { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) + parameters.onDemand = false; const request = new handshake_1.HandshakeSubscribeRequest(Object.assign(Object.assign({}, parameters), { keySet: this._configuration.keySet, crypto: this._configuration.getCryptoModule(), getFileUrl: this.getFileUrl.bind(this) })); const abortUnsubscribe = parameters.abortSignal.subscribe((err) => { request.abort('Cancel subscribe handshake request'); @@ -1388,6 +1397,9 @@ class PubNubCore { subscribeReceiveMessages(parameters) { return __awaiter(this, void 0, void 0, function* () { if (process.env.SUBSCRIBE_EVENT_ENGINE_MODULE !== 'disabled') { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) + parameters.onDemand = false; const request = new receiveMessages_1.ReceiveMessagesSubscribeRequest(Object.assign(Object.assign({}, parameters), { keySet: this._configuration.keySet, crypto: this._configuration.getCryptoModule(), getFileUrl: this.getFileUrl.bind(this) })); const abortUnsubscribe = parameters.abortSignal.subscribe((err) => { request.abort('Cancel long-poll subscribe request'); @@ -1879,12 +1891,10 @@ class PubNubCore { // Filtering out presence channels and groups. let { channels, channelGroups } = parameters; // Remove `-pnpres` channels / groups if they not acceptable in the current PubNub client configuration. - if (!this._configuration.getKeepPresenceChannelsInPresenceRequests()) { - if (channelGroups) - channelGroups = channelGroups.filter((channelGroup) => !channelGroup.endsWith('-pnpres')); - if (channels) - channels = channels.filter((channel) => !channel.endsWith('-pnpres')); - } + if (channelGroups) + channelGroups = channelGroups.filter((channelGroup) => !channelGroup.endsWith('-pnpres')); + if (channels) + channels = channels.filter((channel) => !channel.endsWith('-pnpres')); // Complete immediately request only for presence channels. if ((channelGroups !== null && channelGroups !== void 0 ? channelGroups : []).length === 0 && (channels !== null && channels !== void 0 ? channels : []).length === 0) { const responseStatus = { diff --git a/lib/event-engine/dispatcher.js b/lib/event-engine/dispatcher.js index adef88fee..3eb354fe9 100644 --- a/lib/event-engine/dispatcher.js +++ b/lib/event-engine/dispatcher.js @@ -69,7 +69,7 @@ class EventEngineDispatcher extends core_1.Dispatcher { this.on(effects.handshake.type, (0, core_1.asyncHandler)((payload_1, abortSignal_1, _a) => __awaiter(this, [payload_1, abortSignal_1, _a], void 0, function* (payload, abortSignal, { handshake, presenceState, config }) { abortSignal.throwIfAborted(); try { - const result = yield handshake(Object.assign({ abortSignal: abortSignal, channels: payload.channels, channelGroups: payload.groups, filterExpression: config.filterExpression }, (config.maintainPresenceState && { state: presenceState }))); + const result = yield handshake(Object.assign(Object.assign({ abortSignal: abortSignal, channels: payload.channels, channelGroups: payload.groups, filterExpression: config.filterExpression }, (config.maintainPresenceState && { state: presenceState })), { onDemand: payload.onDemand })); return engine.transition(events.handshakeSuccess(result)); } catch (e) { @@ -90,6 +90,7 @@ class EventEngineDispatcher extends core_1.Dispatcher { timetoken: payload.cursor.timetoken, region: payload.cursor.region, filterExpression: config.filterExpression, + onDemand: payload.onDemand, }); engine.transition(events.receiveSuccess(result.cursor, result.messages)); } diff --git a/lib/event-engine/effects.js b/lib/event-engine/effects.js index 504e70334..0c2307b19 100644 --- a/lib/event-engine/effects.js +++ b/lib/event-engine/effects.js @@ -14,9 +14,10 @@ const core_1 = require("./core"); * * @internal */ -exports.handshake = (0, core_1.createManagedEffect)('HANDSHAKE', (channels, groups) => ({ +exports.handshake = (0, core_1.createManagedEffect)('HANDSHAKE', (channels, groups, onDemand) => ({ channels, groups, + onDemand, })); /** * Real-time updates receive effect. @@ -26,7 +27,12 @@ exports.handshake = (0, core_1.createManagedEffect)('HANDSHAKE', (channels, grou * * @internal */ -exports.receiveMessages = (0, core_1.createManagedEffect)('RECEIVE_MESSAGES', (channels, groups, cursor) => ({ channels, groups, cursor })); +exports.receiveMessages = (0, core_1.createManagedEffect)('RECEIVE_MESSAGES', (channels, groups, cursor, onDemand) => ({ + channels, + groups, + cursor, + onDemand, +})); /** * Emit real-time updates effect. * diff --git a/lib/event-engine/states/handshake_failed.js b/lib/event-engine/states/handshake_failed.js index 22032d95d..31373a74a 100644 --- a/lib/event-engine/states/handshake_failed.js +++ b/lib/event-engine/states/handshake_failed.js @@ -22,9 +22,14 @@ exports.HandshakeFailedState = new state_1.State('HANDSHAKE_FAILED'); exports.HandshakeFailedState.on(events_1.subscriptionChange.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return unsubscribed_1.UnsubscribedState.with(undefined); - return handshaking_1.HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return handshaking_1.HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); -exports.HandshakeFailedState.on(events_1.reconnect.type, (context, { payload }) => handshaking_1.HandshakingState.with(Object.assign(Object.assign({}, context), { cursor: payload.cursor || context.cursor }))); +exports.HandshakeFailedState.on(events_1.reconnect.type, (context, { payload }) => handshaking_1.HandshakingState.with(Object.assign(Object.assign({}, context), { cursor: payload.cursor || context.cursor, onDemand: true }))); exports.HandshakeFailedState.on(events_1.restore.type, (context, { payload }) => { var _a, _b; if (payload.channels.length === 0 && payload.groups.length === 0) @@ -36,6 +41,7 @@ exports.HandshakeFailedState.on(events_1.restore.type, (context, { payload }) => timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region ? payload.cursor.region : ((_b = (_a = context === null || context === void 0 ? void 0 : context.cursor) === null || _a === void 0 ? void 0 : _a.region) !== null && _b !== void 0 ? _b : 0), }, + onDemand: true, }); }); exports.HandshakeFailedState.on(events_1.unsubscribeAll.type, (_) => unsubscribed_1.UnsubscribedState.with()); diff --git a/lib/event-engine/states/handshake_stopped.js b/lib/event-engine/states/handshake_stopped.js index e2d6b3495..53c639c85 100644 --- a/lib/event-engine/states/handshake_stopped.js +++ b/lib/event-engine/states/handshake_stopped.js @@ -24,7 +24,7 @@ exports.HandshakeStoppedState.on(events_1.subscriptionChange.type, (context, { p return unsubscribed_1.UnsubscribedState.with(undefined); return exports.HandshakeStoppedState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); }); -exports.HandshakeStoppedState.on(events_1.reconnect.type, (context, { payload }) => handshaking_1.HandshakingState.with(Object.assign(Object.assign({}, context), { cursor: payload.cursor || context.cursor }))); +exports.HandshakeStoppedState.on(events_1.reconnect.type, (context, { payload }) => handshaking_1.HandshakingState.with(Object.assign(Object.assign({}, context), { cursor: payload.cursor || context.cursor, onDemand: true }))); exports.HandshakeStoppedState.on(events_1.restore.type, (context, { payload }) => { var _a; if (payload.channels.length === 0 && payload.groups.length === 0) diff --git a/lib/event-engine/states/handshaking.js b/lib/event-engine/states/handshaking.js index 09f448813..5f62ce8d3 100644 --- a/lib/event-engine/states/handshaking.js +++ b/lib/event-engine/states/handshaking.js @@ -29,12 +29,17 @@ const utils_1 = require("../../core/utils"); * @internal */ exports.HandshakingState = new state_1.State('HANDSHAKING'); -exports.HandshakingState.onEnter((context) => (0, effects_1.handshake)(context.channels, context.groups)); +exports.HandshakingState.onEnter((context) => { var _a; return (0, effects_1.handshake)(context.channels, context.groups, (_a = context.onDemand) !== null && _a !== void 0 ? _a : false); }); exports.HandshakingState.onExit(() => effects_1.handshake.cancel); exports.HandshakingState.on(events_1.subscriptionChange.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return unsubscribed_1.UnsubscribedState.with(undefined); - return exports.HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return exports.HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); exports.HandshakingState.on(events_1.handshakeSuccess.type, (context, { payload }) => { var _a, _b, _c, _d, _e; @@ -83,6 +88,7 @@ exports.HandshakingState.on(events_1.restore.type, (context, { payload }) => { channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region || ((_a = context === null || context === void 0 ? void 0 : context.cursor) === null || _a === void 0 ? void 0 : _a.region) || 0 }, + onDemand: true, }); }); exports.HandshakingState.on(events_1.unsubscribeAll.type, (_) => unsubscribed_1.UnsubscribedState.with()); diff --git a/lib/event-engine/states/receive_failed.js b/lib/event-engine/states/receive_failed.js index 81a367ad5..be560c905 100644 --- a/lib/event-engine/states/receive_failed.js +++ b/lib/event-engine/states/receive_failed.js @@ -28,12 +28,18 @@ exports.ReceiveFailedState.on(events_1.reconnect.type, (context, { payload }) => timetoken: !!payload.cursor.timetoken ? (_a = payload.cursor) === null || _a === void 0 ? void 0 : _a.timetoken : context.cursor.timetoken, region: payload.cursor.region || context.cursor.region, }, + onDemand: true, }); }); exports.ReceiveFailedState.on(events_1.subscriptionChange.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return unsubscribed_1.UnsubscribedState.with(undefined); - return handshaking_1.HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return handshaking_1.HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); exports.ReceiveFailedState.on(events_1.restore.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) @@ -42,6 +48,7 @@ exports.ReceiveFailedState.on(events_1.restore.type, (context, { payload }) => { channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region || context.cursor.region }, + onDemand: true, }); }); exports.ReceiveFailedState.on(events_1.unsubscribeAll.type, (_) => unsubscribed_1.UnsubscribedState.with(undefined)); diff --git a/lib/event-engine/states/receive_stopped.js b/lib/event-engine/states/receive_stopped.js index 747b430c7..77c6ff751 100644 --- a/lib/event-engine/states/receive_stopped.js +++ b/lib/event-engine/states/receive_stopped.js @@ -42,6 +42,7 @@ exports.ReceiveStoppedState.on(events_1.reconnect.type, (context, { payload }) = timetoken: !!payload.cursor.timetoken ? (_a = payload.cursor) === null || _a === void 0 ? void 0 : _a.timetoken : context.cursor.timetoken, region: payload.cursor.region || context.cursor.region, }, + onDemand: true, }); }); exports.ReceiveStoppedState.on(events_1.unsubscribeAll.type, () => unsubscribed_1.UnsubscribedState.with(undefined)); diff --git a/lib/event-engine/states/receiving.js b/lib/event-engine/states/receiving.js index e9a46ffd3..bc4ea1718 100644 --- a/lib/event-engine/states/receiving.js +++ b/lib/event-engine/states/receiving.js @@ -27,7 +27,7 @@ const state_1 = require("../core/state"); * @internal */ exports.ReceivingState = new state_1.State('RECEIVING'); -exports.ReceivingState.onEnter((context) => (0, effects_1.receiveMessages)(context.channels, context.groups, context.cursor)); +exports.ReceivingState.onEnter((context) => { var _a; return (0, effects_1.receiveMessages)(context.channels, context.groups, context.cursor, (_a = context.onDemand) !== null && _a !== void 0 ? _a : false); }); exports.ReceivingState.onExit(() => effects_1.receiveMessages.cancel); exports.ReceivingState.on(events_1.receiveSuccess.type, (context, { payload }) => exports.ReceivingState.with({ channels: context.channels, @@ -52,6 +52,7 @@ exports.ReceivingState.on(events_1.subscriptionChange.type, (context, { payload groups: payload.groups, cursor: context.cursor, referenceTimetoken: context.referenceTimetoken, + onDemand: true, }, [ (0, effects_1.emitStatus)({ category: categories_1.default.PNSubscriptionChangedCategory, @@ -69,6 +70,7 @@ exports.ReceivingState.on(events_1.restore.type, (context, { payload }) => { groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region || context.cursor.region }, referenceTimetoken: (0, utils_1.referenceSubscribeTimetoken)(context.cursor.timetoken, `${payload.cursor.timetoken}`, context.referenceTimetoken), + onDemand: true, }, [ (0, effects_1.emitStatus)({ category: categories_1.default.PNSubscriptionChangedCategory, diff --git a/lib/event-engine/states/unsubscribed.js b/lib/event-engine/states/unsubscribed.js index ce627e1bf..d93ebd3d7 100644 --- a/lib/event-engine/states/unsubscribed.js +++ b/lib/event-engine/states/unsubscribed.js @@ -20,7 +20,7 @@ exports.UnsubscribedState = new state_1.State('UNSUBSCRIBED'); exports.UnsubscribedState.on(events_1.subscriptionChange.type, (_, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return exports.UnsubscribedState.with(undefined); - return handshaking_1.HandshakingState.with({ channels: payload.channels, groups: payload.groups }); + return handshaking_1.HandshakingState.with({ channels: payload.channels, groups: payload.groups, onDemand: true }); }); exports.UnsubscribedState.on(events_1.restore.type, (_, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) @@ -29,5 +29,6 @@ exports.UnsubscribedState.on(events_1.restore.type, (_, { payload }) => { channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region }, + onDemand: true, }); }); diff --git a/lib/loggers/console-logger.js b/lib/loggers/console-logger.js index 2b947a87f..e9dffa7cd 100644 --- a/lib/loggers/console-logger.js +++ b/lib/loggers/console-logger.js @@ -16,7 +16,7 @@ const utils_1 = require("../core/utils"); */ class ConsoleLogger { /** - * Process a `trace` level message. + * Process a `debug` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -24,7 +24,7 @@ class ConsoleLogger { this.log(message); } /** - * Process a `debug` level message. + * Process a `error` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -40,7 +40,7 @@ class ConsoleLogger { this.log(message); } /** - * Process a `warn` level message. + * Process a `trace` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -48,13 +48,21 @@ class ConsoleLogger { this.log(message); } /** - * Process an `error` level message. + * Process an `warn` level message. * * @param message - Message which should be handled by custom logger implementation. */ warn(message) { this.log(message); } + /** + * Stringify logger object. + * + * @returns Serialized logger object. + */ + toString() { + return `ConsoleLogger {}`; + } /** * Process log message object. * diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index cc170c10c..36592c168 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -137,6 +137,9 @@ declare class PubNubCore< * 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. * @@ -2194,6 +2197,10 @@ declare namespace PubNub { * PubNub client unexpectedly disconnected from the real-time updates streams. */ PNDisconnectedUnexpectedlyCategory = 'PNDisconnectedUnexpectedlyCategory', + /** + * SDK will announce when newer shared worker will be 'noticed'. + */ + PNSharedWorkerUpdatedCategory = 'PNSharedWorkerUpdatedCategory', } /** diff --git a/package-lock.json b/package-lock.json index a38b72746..00a8b4e2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pubnub", - "version": "9.8.3", + "version": "9.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pubnub", - "version": "9.8.3", + "version": "9.8.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "agentkeepalive": "^3.5.2", @@ -3586,18 +3586,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@pkgr/core": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", diff --git a/package.json b/package.json index a531155f1..863d42993 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pubnub", - "version": "9.8.4", + "version": "9.9.0", "author": "PubNub ", "description": "Publish & Subscribe Real-time Messaging with PubNub", "scripts": { diff --git a/src/core/components/configuration.ts b/src/core/components/configuration.ts index aa0527ec7..0149d43b8 100644 --- a/src/core/components/configuration.ts +++ b/src/core/components/configuration.ts @@ -202,6 +202,10 @@ export const makeConfiguration = ( getUseRandomIVs(): boolean | undefined { return base.useRandomIVs; }, + isSharedWorkerEnabled(): boolean { + // @ts-expect-error: Access field from web-based SDK configuration. + return base.sdkFamily === 'Web' && base['subscriptionWorkerUrl']; + }, getKeepPresenceChannelsInPresenceRequests(): boolean { // @ts-expect-error: Access field from web-based SDK configuration. return base.sdkFamily === 'Web' && base['subscriptionWorkerUrl']; @@ -232,7 +236,7 @@ export const makeConfiguration = ( return base.PubNubFile; }, get version(): string { - return '9.8.4'; + return '9.9.0'; }, getVersion(): string { return this.version; diff --git a/src/core/components/logger-manager.ts b/src/core/components/logger-manager.ts index e91927f8f..c98cf0679 100644 --- a/src/core/components/logger-manager.ts +++ b/src/core/components/logger-manager.ts @@ -34,6 +34,16 @@ export class LoggerManager { */ private readonly loggers: Logger[]; + /** + * Keeping track of previous entry timestamp. + * + * This information will be used to make sure that multiple sequential entries doesn't have same timestamp. Adjustment + * on .001 will be done to make it possible to properly stort entries. + * + * @internal + */ + private previousEntryTimestamp: number = 0; + /** * Create and configure loggers' manager. * @@ -133,9 +143,15 @@ export class LoggerManager { // Check whether a log message should be handled at all or not. if (logLevel < this.minLogLevel || this.loggers.length === 0) return; + const date = new Date(); + if (date.getTime() <= this.previousEntryTimestamp) { + this.previousEntryTimestamp++; + date.setTime(this.previousEntryTimestamp); + } else this.previousEntryTimestamp = date.getTime(); + const level = LogLevel[logLevel].toLowerCase() as LogLevelString; const message: BaseLogMessage = { - timestamp: new Date(), + timestamp: date, pubNubId: this.pubNubId, level: logLevel, minimumLevel: this.minLogLevel, diff --git a/src/core/components/subscription-manager.ts b/src/core/components/subscription-manager.ts index eda78fd19..d774bb169 100644 --- a/src/core/components/subscription-manager.ts +++ b/src/core/components/subscription-manager.ts @@ -4,8 +4,8 @@ * @internal */ -import { messageFingerprint, referenceSubscribeTimetoken, subscriptionTimetokenFromReference } from '../utils'; import { PubNubEventType, SubscribeRequestParameters as SubscribeRequestParameters } from '../endpoints/subscribe'; +import { messageFingerprint, referenceSubscribeTimetoken, subscriptionTimetokenFromReference } from '../utils'; import { Payload, ResultCallback, Status, StatusCallback, StatusEvent } from '../types/api'; import { PrivateClientConfiguration } from '../interfaces/configuration'; import { HeartbeatRequest } from '../endpoints/presence/heartbeat'; @@ -115,6 +115,14 @@ export class SubscriptionManager { */ private isOnline: boolean; + /** + * Whether user code in event handlers requested disconnection or not. + * + * Won't continue subscription loop if user requested disconnection/unsubscribe from all in response to received + * event. + */ + private disconnectedWhileHandledEvent: boolean = false; + /** * Active subscription request abort method. * @@ -140,7 +148,9 @@ export class SubscriptionManager { ) => void, private readonly emitStatus: (status: Status | StatusEvent) => void, private readonly subscribeCall: ( - parameters: Omit, + parameters: Omit & { + onDemand: boolean; + }, callback: ResultCallback, ) => void, private readonly heartbeatCall: ( @@ -204,6 +214,10 @@ export class SubscriptionManager { // region Subscription public disconnect() { + // Potentially called during received events handling. + // Mark to prevent subscription loop continuation in subscribe response handler. + this.disconnectedWhileHandledEvent = true; + this.stopSubscribeLoop(); this.stopHeartbeatTimer(); this.reconnectionManager.stopPolling(); @@ -299,6 +313,27 @@ export class SubscriptionManager { // There is no need to unsubscribe to empty list of data sources. if (actualChannels.size === 0 && actualChannelGroups.size === 0) return; + const lastTimetoken = this.lastTimetoken; + const currentTimetoken = this.currentTimetoken; + + if ( + Object.keys(this.channels).length === 0 && + Object.keys(this.presenceChannels).length === 0 && + Object.keys(this.channelGroups).length === 0 && + Object.keys(this.presenceChannelGroups).length === 0 + ) { + this.lastTimetoken = '0'; + this.currentTimetoken = '0'; + this.referenceTimetoken = null; + this.storedTimetoken = null; + this.region = null; + this.reconnectionManager.stopPolling(); + } + + this.reconnect(true); + + // Send leave request after long-poll connection closed and loop restarted (the same way as it happens in new + // subscription flow). if (this.configuration.suppressLeaveEvents === false && !isOffline) { channelGroups = Array.from(actualChannelGroups); channels = Array.from(actualChannels); @@ -323,30 +358,16 @@ export class SubscriptionManager { error: errorMessage ?? false, affectedChannels: channels, affectedChannelGroups: channelGroups, - currentTimetoken: this.currentTimetoken, - lastTimetoken: this.lastTimetoken, + currentTimetoken, + lastTimetoken, } as StatusEvent); }); } - - if ( - Object.keys(this.channels).length === 0 && - Object.keys(this.presenceChannels).length === 0 && - Object.keys(this.channelGroups).length === 0 && - Object.keys(this.presenceChannelGroups).length === 0 - ) { - this.lastTimetoken = '0'; - this.currentTimetoken = '0'; - this.referenceTimetoken = null; - this.storedTimetoken = null; - this.region = null; - this.reconnectionManager.stopPolling(); - } - - this.reconnect(true); } public unsubscribeAll(isOffline: boolean = false) { + this.disconnectedWhileHandledEvent = true; + this.unsubscribe( { channels: this.subscribedChannels, @@ -365,6 +386,7 @@ export class SubscriptionManager { * @internal */ private startSubscribeLoop(restartOnUnsubscribe: boolean = false) { + this.disconnectedWhileHandledEvent = false; this.stopSubscribeLoop(); const channelGroups = [...Object.keys(this.channelGroups)]; @@ -385,6 +407,7 @@ export class SubscriptionManager { timetoken: this.currentTimetoken, ...(this.region !== null ? { region: this.region } : {}), ...(this.configuration.filterExpression ? { filterExpression: this.configuration.filterExpression } : {}), + onDemand: !this.subscriptionStatusAnnounced || restartOnUnsubscribe, }, (status, result) => { this.processSubscribeResponse(status, result); @@ -537,7 +560,8 @@ export class SubscriptionManager { } this.region = result!.cursor.region; - this.startSubscribeLoop(); + if (!this.disconnectedWhileHandledEvent) this.startSubscribeLoop(); + else this.disconnectedWhileHandledEvent = false; } // endregion diff --git a/src/core/constants/categories.ts b/src/core/constants/categories.ts index 2aa6608cf..eb0d33248 100644 --- a/src/core/constants/categories.ts +++ b/src/core/constants/categories.ts @@ -117,6 +117,15 @@ enum StatusCategory { * PubNub client unexpectedly disconnected from the real-time updates streams. */ PNDisconnectedUnexpectedlyCategory = 'PNDisconnectedUnexpectedlyCategory', + + // -------------------------------------------------------- + // ------------------ Shared worker events ---------------- + // -------------------------------------------------------- + + /** + * SDK will announce when newer shared worker will be 'noticed'. + */ + PNSharedWorkerUpdatedCategory = 'PNSharedWorkerUpdatedCategory', } export default StatusCategory; diff --git a/src/core/endpoints/subscribe.ts b/src/core/endpoints/subscribe.ts index 9d4053e77..206a1de73 100644 --- a/src/core/endpoints/subscribe.ts +++ b/src/core/endpoints/subscribe.ts @@ -591,6 +591,11 @@ export type SubscribeRequestParameters = Subscription.SubscribeParameters & { * @param channel - Name of the channel from which file should be downloaded. */ getFileUrl: (parameters: FileSharing.FileUrlParameters) => string; + + /** + * Whether request has been created on user demand or not. + */ + onDemand?: boolean; }; // endregion @@ -908,9 +913,10 @@ export class SubscribeRequest extends BaseSubscribeRequest { } protected get queryParameters(): Query { - const { channelGroups, filterExpression, heartbeat, state, timetoken, region } = this.parameters; + const { channelGroups, filterExpression, heartbeat, state, timetoken, region, onDemand } = this.parameters; const query: Query = {}; + if (onDemand) query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) query['filter-expr'] = filterExpression; if (heartbeat) query.heartbeat = heartbeat; diff --git a/src/core/endpoints/subscriptionUtils/handshake.ts b/src/core/endpoints/subscriptionUtils/handshake.ts index d35eb2c77..d3ad99089 100644 --- a/src/core/endpoints/subscriptionUtils/handshake.ts +++ b/src/core/endpoints/subscriptionUtils/handshake.ts @@ -4,6 +4,7 @@ * @internal */ +import * as Subscription from '../../types/api/subscription'; import RequestOperation from '../../constants/operations'; import { BaseSubscribeRequest } from '../subscribe'; import { encodeNames } from '../../utils'; @@ -31,9 +32,11 @@ export class HandshakeSubscribeRequest extends BaseSubscribeRequest { } protected get queryParameters(): Query { - const { channelGroups, filterExpression, state } = this.parameters; + const { channelGroups, filterExpression, state, onDemand } = this + .parameters as unknown as Subscription.CancelableSubscribeParameters; const query: Query = { ee: '' }; + if (onDemand) query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) query['filter-expr'] = filterExpression; if (state && Object.keys(state).length > 0) query['state'] = JSON.stringify(state); diff --git a/src/core/endpoints/subscriptionUtils/receiveMessages.ts b/src/core/endpoints/subscriptionUtils/receiveMessages.ts index 31b55f2d3..95754496b 100644 --- a/src/core/endpoints/subscriptionUtils/receiveMessages.ts +++ b/src/core/endpoints/subscriptionUtils/receiveMessages.ts @@ -4,6 +4,7 @@ * @internal */ +import * as Subscription from '../../types/api/subscription'; import RequestOperation from '../../constants/operations'; import { BaseSubscribeRequest } from '../subscribe'; import { encodeNames } from '../../utils'; @@ -37,9 +38,11 @@ export class ReceiveMessagesSubscribeRequest extends BaseSubscribeRequest { } protected get queryParameters(): Query { - const { channelGroups, filterExpression, timetoken, region } = this.parameters; + const { channelGroups, filterExpression, timetoken, region, onDemand } = this + .parameters as unknown as Subscription.CancelableSubscribeParameters; const query: Query = { ee: '' }; + if (onDemand) query['on-demand'] = 1; if (channelGroups && channelGroups.length > 0) query['channel-group'] = channelGroups.sort().join(','); if (filterExpression && filterExpression.length > 0) query['filter-expr'] = filterExpression; if (typeof timetoken === 'string') { diff --git a/src/core/interfaces/configuration.ts b/src/core/interfaces/configuration.ts index a77e1ca42..1cd449deb 100644 --- a/src/core/interfaces/configuration.ts +++ b/src/core/interfaces/configuration.ts @@ -621,6 +621,13 @@ export interface PrivateClientConfiguration */ getCryptoModule(): ICryptoModule | undefined; + /** + * Whether SDK client use `SharedWorker` or not. + * + * @returns `true` if SDK build for browser and SDK configured to use `SharedWorker`. + */ + isSharedWorkerEnabled(): boolean; + /** * Whether `-pnpres` should not be filtered out from list of channels / groups in presence-related requests or not. * diff --git a/src/core/pubnub-common.ts b/src/core/pubnub-common.ts index c6468e80a..1a91ee83a 100644 --- a/src/core/pubnub-common.ts +++ b/src/core/pubnub-common.ts @@ -714,6 +714,9 @@ export class PubNubCore< * 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. * @@ -1659,6 +1662,9 @@ export class PubNubCore< callback: ResultCallback, ): void { if (process.env.SUBSCRIBE_MANAGER_MODULE !== 'disabled') { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) parameters.onDemand = false; + const request = new SubscribeRequest({ ...parameters, keySet: this._configuration.keySet, @@ -1820,6 +1826,9 @@ export class PubNubCore< */ private async subscribeHandshake(parameters: Subscription.CancelableSubscribeParameters) { if (process.env.SUBSCRIBE_EVENT_ENGINE_MODULE !== 'disabled') { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) parameters.onDemand = false; + const request = new HandshakeSubscribeRequest({ ...parameters, keySet: this._configuration.keySet, @@ -1854,6 +1863,9 @@ export class PubNubCore< */ private async subscribeReceiveMessages(parameters: Subscription.CancelableSubscribeParameters) { if (process.env.SUBSCRIBE_EVENT_ENGINE_MODULE !== 'disabled') { + // `on-demand` query parameter not required for non-SharedWorker environment. + if (!this._configuration.isSharedWorkerEnabled()) parameters.onDemand = false; + const request = new ReceiveMessagesSubscribeRequest({ ...parameters, keySet: this._configuration.keySet, @@ -2692,10 +2704,8 @@ export class PubNubCore< let { channels, channelGroups } = parameters; // Remove `-pnpres` channels / groups if they not acceptable in the current PubNub client configuration. - if (!this._configuration.getKeepPresenceChannelsInPresenceRequests()) { - if (channelGroups) channelGroups = channelGroups.filter((channelGroup) => !channelGroup.endsWith('-pnpres')); - if (channels) channels = channels.filter((channel) => !channel.endsWith('-pnpres')); - } + if (channelGroups) channelGroups = channelGroups.filter((channelGroup) => !channelGroup.endsWith('-pnpres')); + if (channels) channels = channels.filter((channel) => !channel.endsWith('-pnpres')); // Complete immediately request only for presence channels. if ((channelGroups ?? []).length === 0 && (channels ?? []).length === 0) { diff --git a/src/core/types/api/subscription.ts b/src/core/types/api/subscription.ts index 5f45072e8..419eb66fa 100644 --- a/src/core/types/api/subscription.ts +++ b/src/core/types/api/subscription.ts @@ -489,6 +489,11 @@ export type CancelableSubscribeParameters = Omit< SubscribeRequestParameters, 'crypto' | 'timeout' | 'keySet' | 'getFileUrl' > & { + /** + * Whether request has been created user demand or not. + */ + onDemand: boolean; + /** * Long-poll request termination signal. */ diff --git a/src/event-engine/dispatcher.ts b/src/event-engine/dispatcher.ts index 83e356873..436fc98b0 100644 --- a/src/event-engine/dispatcher.ts +++ b/src/event-engine/dispatcher.ts @@ -63,6 +63,7 @@ export class EventEngineDispatcher extends Dispatcher ({ - channels, - groups, -})); +export const handshake = createManagedEffect( + 'HANDSHAKE', + (channels: string[], groups: string[], onDemand: boolean) => ({ + channels, + groups, + onDemand, + }), +); /** * Real-time updates receive effect. @@ -30,7 +34,12 @@ export const handshake = createManagedEffect('HANDSHAKE', (channels: string[], g */ export const receiveMessages = createManagedEffect( 'RECEIVE_MESSAGES', - (channels: string[], groups: string[], cursor: Subscription.SubscriptionCursor) => ({ channels, groups, cursor }), + (channels: string[], groups: string[], cursor: Subscription.SubscriptionCursor, onDemand: boolean) => ({ + channels, + groups, + cursor, + onDemand, + }), ); /** diff --git a/src/event-engine/states/handshake_failed.ts b/src/event-engine/states/handshake_failed.ts index c6ae6536a..241805dc2 100644 --- a/src/event-engine/states/handshake_failed.ts +++ b/src/event-engine/states/handshake_failed.ts @@ -38,11 +38,16 @@ export const HandshakeFailedState = new State { if (payload.channels.length === 0 && payload.groups.length === 0) return UnsubscribedState.with(undefined); - return HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); HandshakeFailedState.on(reconnect.type, (context, { payload }) => - HandshakingState.with({ ...context, cursor: payload.cursor || context.cursor }), + HandshakingState.with({ ...context, cursor: payload.cursor || context.cursor, onDemand: true }), ); HandshakeFailedState.on(restore.type, (context, { payload }) => { @@ -55,6 +60,7 @@ HandshakeFailedState.on(restore.type, (context, { payload }) => { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region ? payload.cursor.region : (context?.cursor?.region ?? 0), }, + onDemand: true, }); }); diff --git a/src/event-engine/states/handshake_stopped.ts b/src/event-engine/states/handshake_stopped.ts index 61bd2f562..d64fadc83 100644 --- a/src/event-engine/states/handshake_stopped.ts +++ b/src/event-engine/states/handshake_stopped.ts @@ -39,7 +39,7 @@ HandshakeStoppedState.on(subscriptionChange.type, (context, { payload }) => { }); HandshakeStoppedState.on(reconnect.type, (context, { payload }) => - HandshakingState.with({ ...context, cursor: payload.cursor || context.cursor }), + HandshakingState.with({ ...context, cursor: payload.cursor || context.cursor, onDemand: true }), ); HandshakeStoppedState.on(restore.type, (context, { payload }) => { diff --git a/src/event-engine/states/handshaking.ts b/src/event-engine/states/handshaking.ts index e62e69898..147bc5c4f 100644 --- a/src/event-engine/states/handshaking.ts +++ b/src/event-engine/states/handshaking.ts @@ -34,6 +34,7 @@ export type HandshakingStateContext = { channels: string[]; groups: string[]; cursor?: Subscription.SubscriptionCursor; + onDemand?: boolean; }; /** @@ -46,13 +47,18 @@ export type HandshakingStateContext = { */ export const HandshakingState = new State('HANDSHAKING'); -HandshakingState.onEnter((context) => handshake(context.channels, context.groups)); +HandshakingState.onEnter((context) => handshake(context.channels, context.groups, context.onDemand ?? false)); HandshakingState.onExit(() => handshake.cancel); HandshakingState.on(subscriptionChange.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return UnsubscribedState.with(undefined); - return HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); HandshakingState.on(handshakeSuccess.type, (context, { payload }) => @@ -106,6 +112,7 @@ HandshakingState.on(restore.type, (context, { payload }) => { channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region || context?.cursor?.region || 0 }, + onDemand: true, }); }); diff --git a/src/event-engine/states/receive_failed.ts b/src/event-engine/states/receive_failed.ts index cd788aeea..3268f7ccf 100644 --- a/src/event-engine/states/receive_failed.ts +++ b/src/event-engine/states/receive_failed.ts @@ -43,13 +43,19 @@ ReceiveFailedState.on(reconnect.type, (context, { payload }) => timetoken: !!payload.cursor.timetoken ? payload.cursor?.timetoken : context.cursor.timetoken, region: payload.cursor.region || context.cursor.region, }, + onDemand: true, }), ); ReceiveFailedState.on(subscriptionChange.type, (context, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return UnsubscribedState.with(undefined); - return HandshakingState.with({ channels: payload.channels, groups: payload.groups, cursor: context.cursor }); + return HandshakingState.with({ + channels: payload.channels, + groups: payload.groups, + cursor: context.cursor, + onDemand: true, + }); }); ReceiveFailedState.on(restore.type, (context, { payload }) => { @@ -59,6 +65,7 @@ ReceiveFailedState.on(restore.type, (context, { payload }) => { channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region || context.cursor.region }, + onDemand: true, }); }); diff --git a/src/event-engine/states/receive_stopped.ts b/src/event-engine/states/receive_stopped.ts index a65ba328a..4504d2397 100644 --- a/src/event-engine/states/receive_stopped.ts +++ b/src/event-engine/states/receive_stopped.ts @@ -56,6 +56,7 @@ ReceiveStoppedState.on(reconnect.type, (context, { payload }) => timetoken: !!payload.cursor.timetoken ? payload.cursor?.timetoken : context.cursor.timetoken, region: payload.cursor.region || context.cursor.region, }, + onDemand: true, }), ); diff --git a/src/event-engine/states/receiving.ts b/src/event-engine/states/receiving.ts index 582d3250a..e8dfc87ae 100644 --- a/src/event-engine/states/receiving.ts +++ b/src/event-engine/states/receiving.ts @@ -34,6 +34,7 @@ export type ReceivingStateContext = { groups: string[]; cursor: Subscription.SubscriptionCursor; referenceTimetoken?: string; + onDemand?: boolean; }; /** @@ -45,7 +46,9 @@ export type ReceivingStateContext = { */ export const ReceivingState = new State('RECEIVING'); -ReceivingState.onEnter((context) => receiveMessages(context.channels, context.groups, context.cursor)); +ReceivingState.onEnter((context) => + receiveMessages(context.channels, context.groups, context.cursor, context.onDemand ?? false), +); ReceivingState.onExit(() => receiveMessages.cancel); ReceivingState.on(receiveSuccess.type, (context, { payload }) => @@ -84,6 +87,7 @@ ReceivingState.on(subscriptionChange.type, (context, { payload }) => { groups: payload.groups, cursor: context.cursor, referenceTimetoken: context.referenceTimetoken, + onDemand: true, }, [ emitStatus({ @@ -110,6 +114,7 @@ ReceivingState.on(restore.type, (context, { payload }) => { `${payload.cursor.timetoken}`, context.referenceTimetoken, ), + onDemand: true, }, [ emitStatus({ diff --git a/src/event-engine/states/unsubscribed.ts b/src/event-engine/states/unsubscribed.ts index aa416f766..7ecea591f 100644 --- a/src/event-engine/states/unsubscribed.ts +++ b/src/event-engine/states/unsubscribed.ts @@ -21,7 +21,7 @@ export const UnsubscribedState = new State('UNSUBSCRIBED' UnsubscribedState.on(subscriptionChange.type, (_, { payload }) => { if (payload.channels.length === 0 && payload.groups.length === 0) return UnsubscribedState.with(undefined); - return HandshakingState.with({ channels: payload.channels, groups: payload.groups }); + return HandshakingState.with({ channels: payload.channels, groups: payload.groups, onDemand: true }); }); UnsubscribedState.on(restore.type, (_, { payload }) => { @@ -31,5 +31,6 @@ UnsubscribedState.on(restore.type, (_, { payload }) => { channels: payload.channels, groups: payload.groups, cursor: { timetoken: `${payload.cursor.timetoken}`, region: payload.cursor.region }, + onDemand: true, }); }); diff --git a/src/loggers/console-logger.ts b/src/loggers/console-logger.ts index c0f2994dd..950f29f7b 100644 --- a/src/loggers/console-logger.ts +++ b/src/loggers/console-logger.ts @@ -30,7 +30,7 @@ export class ConsoleLogger implements Logger { private static readonly decoder = new TextDecoder(); /** - * Process a `trace` level message. + * Process a `debug` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -39,7 +39,7 @@ export class ConsoleLogger implements Logger { } /** - * Process a `debug` level message. + * Process a `error` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -57,7 +57,7 @@ export class ConsoleLogger implements Logger { } /** - * Process a `warn` level message. + * Process a `trace` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -66,7 +66,7 @@ export class ConsoleLogger implements Logger { } /** - * Process an `error` level message. + * Process an `warn` level message. * * @param message - Message which should be handled by custom logger implementation. */ @@ -74,6 +74,15 @@ export class ConsoleLogger implements Logger { this.log(message); } + /** + * Stringify logger object. + * + * @returns Serialized logger object. + */ + toString(): string { + return `ConsoleLogger {}`; + } + /** * Process log message object. * diff --git a/src/transport/subscription-worker/components/access-token.ts b/src/transport/subscription-worker/components/access-token.ts new file mode 100644 index 000000000..cda4c0c39 --- /dev/null +++ b/src/transport/subscription-worker/components/access-token.ts @@ -0,0 +1,78 @@ +/** + * PubNub access token. + * + * Object used to simplify manipulations with requests (aggregation) in the Shared Worker context. + */ +export class AccessToken { + /** + * Token comparison based on expiration date. + * + * The access token with the most distant expiration date (which should be used in requests) will be at the end of the + * sorted array. + * + * **Note:** `compare` used with {@link Array.sort|sort} function to identify token with more distant expiration date. + * + * @param lhToken - Left-hand access token which will be used in {@link Array.sort|sort} comparison. + * @param rhToken - Right-hand access token which will be used in {@link Array.sort|sort} comparison. + * @returns Comparison result. + */ + static compare(lhToken: AccessToken, rhToken: AccessToken): number { + const lhTokenExpiration = lhToken.expiration ?? 0; + const rhTokenExpiration = rhToken.expiration ?? 0; + return lhTokenExpiration - rhTokenExpiration; + } + + /** + * Create access token object for PubNub client. + * + * @param token - Authorization key or access token for `read` access to the channels and groups. + * @param [simplifiedToken] - Simplified access token based only on content of `resources`, `patterns`, and + * `authorized_uuid`. + * @param [expiration] - Access token expiration date. + */ + constructor( + private readonly token: string, + private readonly simplifiedToken?: string, + private readonly expiration?: number, + ) {} + + /** + * Represent the access token as identifier. + * + * @returns String that lets us identify other access tokens that have similar configurations. + */ + get asIdentifier() { + return this.simplifiedToken ?? this.token; + } + + /** + * Check whether two access token objects represent the same permissions or not. + * + * @param other - Other access token that should be used in comparison. + * @param checkExpiration - Whether the token expiration date also should be compared or not. + * @returns `true` if received and another access token object represents the same permissions (and `expiration` if + * has been requested). + */ + equalTo(other: AccessToken, checkExpiration: boolean = false): boolean { + return this.asIdentifier === other.asIdentifier && (checkExpiration ? this.expiration === other.expiration : true); + } + + /** + * Check whether the receiver is a newer auth token than another. + * + * @param other - Other access token that should be used in comparison. + * @returns `true` if received has a more distant expiration date than another token. + */ + isNewerThan(other: AccessToken) { + return this.simplifiedToken ? this.expiration! > other.expiration! : false; + } + + /** + * Stringify object to actual access token / key value. + * + * @returns Actual access token / key value. + */ + toString() { + return this.token; + } +} diff --git a/src/transport/subscription-worker/components/custom-events/client-event.ts b/src/transport/subscription-worker/components/custom-events/client-event.ts new file mode 100644 index 000000000..1bb0f82f6 --- /dev/null +++ b/src/transport/subscription-worker/components/custom-events/client-event.ts @@ -0,0 +1,404 @@ +import { SubscribeRequest } from '../subscribe-request'; +import { HeartbeatRequest } from '../heartbeat-request'; +import { PubNubClient } from '../pubnub-client'; +import { LeaveRequest } from '../leave-request'; +import { AccessToken } from '../access-token'; + +/** + * Type with events which is emitted by PubNub client and can be handled with callback passed to the + * {@link EventTarget#addEventListener|addEventListener}. + */ +export enum PubNubClientEvent { + /** + * Client unregistered (no connection through SharedWorker connection ports). + * + */ + Unregister = 'unregister', + + /** + * Client temporarily disconnected. + */ + Disconnect = 'disconnect', + + /** + * User ID for current PubNub client has been changed. + * + * On identity change for proper further operation expected following actions: + * - send immediate heartbeat with new `user ID` (if has been sent before) + */ + IdentityChange = 'identityChange', + + /** + * Authentication token change event. + * + * On authentication token change for proper further operation expected following actions: + * - cached `heartbeat` request query parameter updated + */ + AuthChange = 'authChange', + + /** + * Presence heartbeat interval change event. + * + * On heartbeat interval change for proper further operation expected following actions: + * - restart _backup_ heartbeat timer with new interval. + */ + HeartbeatIntervalChange = 'heartbeatIntervalChange', + + /** + * Core PubNub client module request to send `subscribe` request. + */ + SendSubscribeRequest = 'sendSubscribeRequest', + + /** + * Core PubNub client module request to _cancel_ specific `subscribe` request. + */ + CancelSubscribeRequest = 'cancelSubscribeRequest', + + /** + * Core PubNub client module request to send `heartbeat` request. + */ + SendHeartbeatRequest = 'sendHeartbeatRequest', + + /** + * Core PubNub client module request to send `leave` request. + */ + SendLeaveRequest = 'sendLeaveRequest', +} + +/** + * Base request processing event class. + */ +class BasePubNubClientEvent extends CustomEvent<{ client: PubNubClient } & T> { + /** + * Retrieve reference to PubNub client which dispatched event. + * + * @returns Reference to PubNub client which dispatched event. + */ + get client(): PubNubClient { + return this.detail.client; + } +} + +/** + * Dispatched by PubNub client when it has been unregistered. + */ +export class PubNubClientUnregisterEvent extends BasePubNubClientEvent { + /** + * Create PubNub client unregister event. + * + * @param client - Reference to unregistered PubNub client. + */ + constructor(client: PubNubClient) { + super(PubNubClientEvent.Unregister, { detail: { client } }); + } + + /** + * Create a clone of `unregister` event to make it possible to forward event upstream. + * + * @returns Clone of `unregister` event. + */ + clone() { + return new PubNubClientUnregisterEvent(this.client); + } +} + +/** + * Dispatched by PubNub client when it has been disconnected. + */ +export class PubNubClientDisconnectEvent extends BasePubNubClientEvent { + /** + * Create PubNub client disconnect event. + * + * @param client - Reference to disconnected PubNub client. + */ + constructor(client: PubNubClient) { + super(PubNubClientEvent.Disconnect, { detail: { client } }); + } + + /** + * Create a clone of `disconnect` event to make it possible to forward event upstream. + * + * @returns Clone of `disconnect` event. + */ + clone() { + return new PubNubClientDisconnectEvent(this.client); + } +} + +/** + * Dispatched by PubNub client when it changes user identity (`userId` has been changed). + */ +export class PubNubClientIdentityChangeEvent extends BasePubNubClientEvent<{ + oldUserId: string; + newUserId: string; +}> { + /** + * Create PubNub client identity change event. + * + * @param client - Reference to the PubNub client which changed identity. + * @param oldUserId - User ID which has been previously used by the `client`. + * @param newUserId - User ID which will used by the `client`. + */ + constructor(client: PubNubClient, oldUserId: string, newUserId: string) { + super(PubNubClientEvent.IdentityChange, { detail: { client, oldUserId, newUserId } }); + } + + /** + * Retrieve `userId` which has been previously used by the `client`. + * + * @returns `userId` which has been previously used by the `client`. + */ + get oldUserId() { + return this.detail.oldUserId; + } + + /** + * Retrieve `userId` which will used by the `client`. + * + * @returns `userId` which will used by the `client`. + */ + get newUserId() { + return this.detail.newUserId; + } + + /** + * Create a clone of `identity` _change_ event to make it possible to forward event upstream. + * + * @returns Clone of `identity` _change_ event. + */ + clone() { + return new PubNubClientIdentityChangeEvent(this.client, this.oldUserId, this.newUserId); + } +} + +/** + * Dispatched by PubNub client when it changes authentication data (`auth` has been changed). + */ +export class PubNubClientAuthChangeEvent extends BasePubNubClientEvent<{ + oldAuth?: AccessToken; + newAuth?: AccessToken; +}> { + /** + * Create PubNub client authentication change event. + * + * @param client - Reference to the PubNub client which changed authentication. + * @param [newAuth] - Authentication which will used by the `client`. + * @param [oldAuth] - Authentication which has been previously used by the `client`. + */ + constructor(client: PubNubClient, newAuth?: AccessToken, oldAuth?: AccessToken) { + super(PubNubClientEvent.AuthChange, { detail: { client, oldAuth, newAuth } }); + } + + /** + * Retrieve authentication which has been previously used by the `client`. + * + * @returns Authentication which has been previously used by the `client`. + */ + get oldAuth() { + return this.detail.oldAuth; + } + + /** + * Retrieve authentication which will used by the `client`. + * + * @returns Authentication which will used by the `client`. + */ + get newAuth() { + return this.detail.newAuth; + } + + /** + * Create a clone of `authentication` _change_ event to make it possible to forward event upstream. + * + * @returns Clone `authentication` _change_ event. + */ + clone() { + return new PubNubClientAuthChangeEvent(this.client, this.newAuth, this.oldAuth); + } +} + +/** + * Dispatched by PubNub client when it changes heartbeat interval. + */ +export class PubNubClientHeartbeatIntervalChangeEvent extends BasePubNubClientEvent<{ + oldInterval?: number; + newInterval?: number; +}> { + /** + * Create PubNub client heartbeat interval change event. + * + * @param client - Reference to the PubNub client which changed heartbeat interval. + * @param [newInterval] - New heartbeat request send interval. + * @param [oldInterval] - Previous heartbeat request send interval. + */ + constructor(client: PubNubClient, newInterval?: number, oldInterval?: number) { + super(PubNubClientEvent.HeartbeatIntervalChange, { detail: { client, oldInterval, newInterval } }); + } + + /** + * Retrieve previous heartbeat request send interval. + * + * @returns Previous heartbeat request send interval. + */ + get oldInterval() { + return this.detail.oldInterval; + } + + /** + * Retrieve new heartbeat request send interval. + * + * @returns New heartbeat request send interval. + */ + get newInterval() { + return this.detail.newInterval; + } + + /** + * Create a clone of the `heartbeat interval` _change_ event to make it possible to forward the event upstream. + * + * @returns Clone of `heartbeat interval` _change_ event. + */ + clone() { + return new PubNubClientHeartbeatIntervalChangeEvent(this.client, this.newInterval, this.oldInterval); + } +} + +/** + * Dispatched when the core PubNub client module requested to _send_ a `subscribe` request. + */ +export class PubNubClientSendSubscribeEvent extends BasePubNubClientEvent<{ + request: SubscribeRequest; +}> { + /** + * Create subscribe request send event. + * + * @param client - Reference to the PubNub client which requested to send request. + * @param request - Subscription request object. + */ + constructor(client: PubNubClient, request: SubscribeRequest) { + super(PubNubClientEvent.SendSubscribeRequest, { detail: { client, request } }); + } + + /** + * Retrieve subscription request object. + * + * @returns Subscription request object. + */ + get request() { + return this.detail.request; + } + + /** + * Create clone of _send_ `subscribe` request event to make it possible to forward event upstream. + * + * @returns Clone of _send_ `subscribe` request event. + */ + clone() { + return new PubNubClientSendSubscribeEvent(this.client, this.request); + } +} + +/** + * Dispatched when the core PubNub client module requested to _cancel_ `subscribe` request. + */ +export class PubNubClientCancelSubscribeEvent extends BasePubNubClientEvent<{ + request: SubscribeRequest; +}> { + /** + * Create `subscribe` request _cancel_ event. + * + * @param client - Reference to the PubNub client which requested to _send_ request. + * @param request - Subscription request object. + */ + constructor(client: PubNubClient, request: SubscribeRequest) { + super(PubNubClientEvent.CancelSubscribeRequest, { detail: { client, request } }); + } + + /** + * Retrieve subscription request object. + * + * @returns Subscription request object. + */ + get request() { + return this.detail.request; + } + + /** + * Create clone of _cancel_ `subscribe` request event to make it possible to forward event upstream. + * + * @returns Clone of _cancel_ `subscribe` request event. + */ + clone() { + return new PubNubClientCancelSubscribeEvent(this.client, this.request); + } +} + +/** + * Dispatched when the core PubNub client module requested to _send_ `heartbeat` request. + */ +export class PubNubClientSendHeartbeatEvent extends BasePubNubClientEvent<{ + request: HeartbeatRequest; +}> { + /** + * Create `heartbeat` request _send_ event. + * + * @param client - Reference to the PubNub client which requested to send request. + * @param request - Heartbeat request object. + */ + constructor(client: PubNubClient, request: HeartbeatRequest) { + super(PubNubClientEvent.SendHeartbeatRequest, { detail: { client, request } }); + } + + /** + * Retrieve heartbeat request object. + * + * @returns Heartbeat request object. + */ + get request() { + return this.detail.request; + } + + /** + * Create clone of _send_ `heartbeat` request event to make it possible to forward event upstream. + * + * @returns Clone of _send_ `heartbeat` request event. + */ + clone() { + return new PubNubClientSendHeartbeatEvent(this.client, this.request); + } +} + +/** + * Dispatched when the core PubNub client module requested to _send_ `leave` request. + */ +export class PubNubClientSendLeaveEvent extends BasePubNubClientEvent<{ + request: LeaveRequest; +}> { + /** + * Create `leave` request _send_ event. + * + * @param client - Reference to the PubNub client which requested to send request. + * @param request - Leave request object. + */ + constructor(client: PubNubClient, request: LeaveRequest) { + super(PubNubClientEvent.SendLeaveRequest, { detail: { client, request } }); + } + + /** + * Retrieve leave request object. + * + * @returns Leave request object. + */ + get request() { + return this.detail.request; + } + + /** + * Create clone of _send_ `leave` request event to make it possible to forward event upstream. + * + * @returns Clone of _send_ `leave` request event. + */ + clone() { + return new PubNubClientSendLeaveEvent(this.client, this.request); + } +} diff --git a/src/transport/subscription-worker/components/custom-events/client-manager-event.ts b/src/transport/subscription-worker/components/custom-events/client-manager-event.ts new file mode 100644 index 000000000..6f0ade14c --- /dev/null +++ b/src/transport/subscription-worker/components/custom-events/client-manager-event.ts @@ -0,0 +1,91 @@ +import { PubNubClient } from '../pubnub-client'; + +/** + * Type with events which is dispatched by PubNub clients manager and can be handled with callback passed to the + * {@link EventTarget#addEventListener|addEventListener}. + */ +export enum PubNubClientsManagerEvent { + /** + * New PubNub client has been registered. + */ + Registered = 'Registered', + + /** + * PubNub client has been unregistered. + */ + Unregistered = 'Unregistered', +} + +/** + * Dispatched by clients manager when new PubNub client registers within `SharedWorker`. + */ +export class PubNubClientManagerRegisterEvent extends CustomEvent { + /** + * Create client registration event. + * + * @param client - Reference to the registered PubNub client. + */ + constructor(client: PubNubClient) { + super(PubNubClientsManagerEvent.Registered, { detail: client }); + } + + /** + * Retrieve reference to registered PubNub client. + * + * @returns Reference to registered PubNub client. + */ + get client(): PubNubClient { + return this.detail; + } + + /** + * Create clone of new client register event to make it possible to forward event upstream. + * + * @returns Client new client register event. + */ + clone() { + return new PubNubClientManagerRegisterEvent(this.client); + } +} + +/** + * Dispatched by clients manager when PubNub client unregisters from `SharedWorker`. + */ +export class PubNubClientManagerUnregisterEvent extends CustomEvent<{ client: PubNubClient; withLeave: boolean }> { + /** + * Create client unregistration event. + * + * @param client - Reference to the unregistered PubNub client. + * @param withLeave - Whether `leave` request should be sent or not. + */ + constructor(client: PubNubClient, withLeave = false) { + super(PubNubClientsManagerEvent.Unregistered, { detail: { client, withLeave } }); + } + + /** + * Retrieve reference to the unregistered PubNub client. + * + * @returns Reference to the unregistered PubNub client. + */ + get client(): PubNubClient { + return this.detail.client; + } + + /** + * Retrieve whether `leave` request should be sent or not. + * + * @returns `true` if `leave` request should be sent for previously used channels and groups. + */ + get withLeave() { + return this.detail.withLeave; + } + + /** + * Create clone of client unregister event to make it possible to forward event upstream. + * + * @returns Client client unregister event. + */ + clone() { + return new PubNubClientManagerUnregisterEvent(this.client, this.withLeave); + } +} diff --git a/src/transport/subscription-worker/components/custom-events/heartbeat-state-event.ts b/src/transport/subscription-worker/components/custom-events/heartbeat-state-event.ts new file mode 100644 index 000000000..324d8c23a --- /dev/null +++ b/src/transport/subscription-worker/components/custom-events/heartbeat-state-event.ts @@ -0,0 +1,71 @@ +import { HeartbeatRequest } from '../heartbeat-request'; +import { SubscriptionStateInvalidateEvent } from './subscription-state-event'; + +/** + * Type with events which is dispatched by heartbeat state in response to client-provided requests and PubNub + * client state change. + */ +export enum HeartbeatStateEvent { + /** + * Heartbeat state ready to send another heartbeat. + */ + Heartbeat = 'heartbeat', + + /** + * Heartbeat state has been invalidated after all clients' state was removed from it. + */ + Invalidated = 'invalidated', +} + +/** + * Dispatched by heartbeat state when new heartbeat can be sent. + */ +export class HeartbeatStateHeartbeatEvent extends CustomEvent { + /** + * Create heartbeat state heartbeat event. + * + * @param request - Aggregated heartbeat request which can be sent. + */ + constructor(request: HeartbeatRequest) { + super(HeartbeatStateEvent.Heartbeat, { detail: request }); + } + + /** + * Retrieve aggregated heartbeat request which can be sent. + * + * @returns Aggregated heartbeat request which can be sent. + */ + get request() { + return this.detail; + } + + /** + * Create clone of heartbeat event to make it possible to forward event upstream. + * + * @returns Client heartbeat event. + */ + clone() { + return new HeartbeatStateHeartbeatEvent(this.request); + } +} + +/** + * Dispatched by heartbeat state when it has been invalidated. + */ +export class HeartbeatStateInvalidateEvent extends CustomEvent { + /** + * Create heartbeat state invalidation event. + */ + constructor() { + super(HeartbeatStateEvent.Invalidated); + } + + /** + * Create clone of invalidate event to make it possible to forward event upstream. + * + * @returns Client invalidate event. + */ + clone() { + return new HeartbeatStateInvalidateEvent(); + } +} diff --git a/src/transport/subscription-worker/components/custom-events/request-processing-event.ts b/src/transport/subscription-worker/components/custom-events/request-processing-event.ts new file mode 100644 index 000000000..4f6bd5b30 --- /dev/null +++ b/src/transport/subscription-worker/components/custom-events/request-processing-event.ts @@ -0,0 +1,192 @@ +import { RequestSendingError, RequestSendingSuccess } from '../../subscription-worker-types'; +import { BasePubNubRequest } from '../request'; + +/** + * Type with events which is emitted by request and can be handled with callback passed to the + * {@link EventTarget#addEventListener|addEventListener}. + */ +export enum PubNubSharedWorkerRequestEvents { + /** + * Request processing started. + */ + Started = 'started', + + /** + * Request processing has been canceled. + * + * **Note:** This event dispatched only by client-provided requests. + */ + Canceled = 'canceled', + + /** + * Request successfully completed. + */ + Success = 'success', + + /** + * Request completed with error. + * + * Error can be caused by: + * - missing permissions (403) + * - network issues + */ + Error = 'error', +} + +/** + * Base request processing event class. + */ +class BaseRequestEvent extends CustomEvent<{ request: BasePubNubRequest } & T> { + /** + * Retrieve service (aggregated / updated) request. + * + * @returns Service (aggregated / updated) request. + */ + get request(): BasePubNubRequest { + return this.detail.request; + } +} + +/** + * Dispatched by request when linked service request processing started. + */ +export class RequestStartEvent extends BaseRequestEvent { + /** + * Create request processing start event. + * + * @param request - Service (aggregated / updated) request. + */ + constructor(request: BasePubNubRequest) { + super(PubNubSharedWorkerRequestEvents.Started, { detail: { request } }); + } + + /** + * Create clone of request processing start event to make it possible to forward event upstream. + * + * @param request - Custom requests with this event should be cloned. + * @returns Client request processing start event. + */ + clone(request?: BasePubNubRequest) { + return new RequestStartEvent(request ?? this.request); + } +} + +/** + * Dispatched by request when linked service request processing completed. + */ +export class RequestSuccessEvent extends BaseRequestEvent<{ fetchRequest: Request; response: RequestSendingSuccess }> { + /** + * Create request processing success event. + * + * @param request - Service (aggregated / updated) request. + * @param fetchRequest - Actual request which has been used with {@link fetch}. + * @param response - PubNub service response. + */ + constructor(request: BasePubNubRequest, fetchRequest: Request, response: RequestSendingSuccess) { + super(PubNubSharedWorkerRequestEvents.Success, { detail: { request, fetchRequest, response } }); + } + + /** + * Retrieve actual request which has been used with {@link fetch}. + * + * @returns Actual request which has been used with {@link fetch}. + */ + get fetchRequest() { + return this.detail.fetchRequest; + } + + /** + * Retrieve PubNub service response. + * + * @returns Service response. + */ + get response() { + return this.detail.response; + } + + /** + * Create clone of request processing success event to make it possible to forward event upstream. + * + * @param request - Custom requests with this event should be cloned. + * @returns Client request processing success event. + */ + clone(request?: BasePubNubRequest) { + return new RequestSuccessEvent( + request ?? this.request, + request ? request.asFetchRequest : this.fetchRequest, + this.response, + ); + } +} + +/** + * Dispatched by request when linked service request processing failed / service error response. + */ +export class RequestErrorEvent extends BaseRequestEvent<{ fetchRequest: Request; error: RequestSendingError }> { + /** + * Create request processing error event. + * + * @param request - Service (aggregated / updated) request. + * @param fetchRequest - Actual request which has been used with {@link fetch}. + * @param error - Request processing error information. + */ + constructor(request: BasePubNubRequest, fetchRequest: Request, error: RequestSendingError) { + super(PubNubSharedWorkerRequestEvents.Error, { detail: { request, fetchRequest, error } }); + } + + /** + * Retrieve actual request which has been used with {@link fetch}. + * + * @returns Actual request which has been used with {@link fetch}. + */ + get fetchRequest() { + return this.detail.fetchRequest; + } + + /** + * Retrieve request processing error description. + * + * @returns Request processing error description. + */ + get error(): RequestSendingError { + return this.detail.error; + } + + /** + * Create clone of request processing failure event to make it possible to forward event upstream. + * + * @param request - Custom requests with this event should be cloned. + * @returns Client request processing failure event. + */ + clone(request?: BasePubNubRequest) { + return new RequestErrorEvent( + request ?? this.request, + request ? request.asFetchRequest : this.fetchRequest, + this.error, + ); + } +} + +/** + * Dispatched by request when it has been canceled. + */ +export class RequestCancelEvent extends BaseRequestEvent { + /** + * Create request cancelling event. + * + * @param request - Client-provided (original) request. + */ + constructor(request: BasePubNubRequest) { + super(PubNubSharedWorkerRequestEvents.Canceled, { detail: { request } }); + } + + /** + * Create clone of request cancel event to make it possible to forward event upstream. + * + * @param request - Custom requests with this event should be cloned. + * @returns Client request cancel event. + */ + clone(request?: BasePubNubRequest) { + return new RequestCancelEvent(request ?? this.request); + } +} diff --git a/src/transport/subscription-worker/components/custom-events/subscription-state-event.ts b/src/transport/subscription-worker/components/custom-events/subscription-state-event.ts new file mode 100644 index 000000000..a218e15e1 --- /dev/null +++ b/src/transport/subscription-worker/components/custom-events/subscription-state-event.ts @@ -0,0 +1,120 @@ +import { SubscribeRequest } from '../subscribe-request'; +import { LeaveRequest } from '../leave-request'; + +/** + * Type with events which is dispatched by subscription state in response to client-provided requests and PubNub + * client state change. + */ +export enum SubscriptionStateEvent { + /** + * Subscription state has been changed. + */ + Changed = 'changed', + + /** + * Subscription state has been invalidated after all clients' state was removed from it. + */ + Invalidated = 'invalidated', +} + +/** + * Dispatched by subscription state when state and service requests are changed. + */ +export class SubscriptionStateChangeEvent extends CustomEvent<{ + withInitialResponse: { request: SubscribeRequest; timetoken: string; region: string }[]; + newRequests: SubscribeRequest[]; + canceledRequests: SubscribeRequest[]; + leaveRequest?: LeaveRequest; +}> { + /** + * Create subscription state change event. + * + * @param withInitialResponse - List of initial `client`-provided {@link SubscribeRequest|subscribe} requests with + * timetokens and regions that should be returned right away. + * @param newRequests - List of new service requests which need to be scheduled for processing. + * @param canceledRequests - List of previously scheduled service requests which should be cancelled. + * @param leaveRequest - Request which should be used to announce `leave` from part of the channels and groups. + */ + constructor( + withInitialResponse: { request: SubscribeRequest; timetoken: string; region: string }[], + newRequests: SubscribeRequest[], + canceledRequests: SubscribeRequest[], + leaveRequest?: LeaveRequest, + ) { + super(SubscriptionStateEvent.Changed, { + detail: { withInitialResponse, newRequests, canceledRequests, leaveRequest }, + }); + } + + /** + * Retrieve list of initial `client`-provided {@link SubscribeRequest|subscribe} requests with timetokens and regions + * that should be returned right away. + * + * @returns List of initial `client`-provided {@link SubscribeRequest|subscribe} requests with timetokens and regions + * that should be returned right away. + */ + get requestsWithInitialResponse() { + return this.detail.withInitialResponse; + } + + /** + * Retrieve list of new service requests which need to be scheduled for processing. + * + * @returns List of new service requests which need to be scheduled for processing. + */ + get newRequests() { + return this.detail.newRequests; + } + + /** + * Retrieve request which should be used to announce `leave` from part of the channels and groups. + * + * @returns Request which should be used to announce `leave` from part of the channels and groups. + */ + get leaveRequest() { + return this.detail.leaveRequest; + } + + /** + * Retrieve list of previously scheduled service requests which should be cancelled. + * + * @returns List of previously scheduled service requests which should be cancelled. + */ + get canceledRequests() { + return this.detail.canceledRequests; + } + + /** + * Create clone of subscription state change event to make it possible to forward event upstream. + * + * @returns Client subscription state change event. + */ + clone() { + return new SubscriptionStateChangeEvent( + this.requestsWithInitialResponse, + this.newRequests, + this.canceledRequests, + this.leaveRequest, + ); + } +} +/** + * Dispatched by subscription state when it has been invalidated. + */ +export class SubscriptionStateInvalidateEvent extends CustomEvent { + /** + * Create subscription state invalidation event. + */ + constructor() { + super(SubscriptionStateEvent.Invalidated); + } + + /** + * Create clone of subscription state change event to make it possible to forward event upstream. + * + * @returns Client subscription state change event. + */ + clone() { + return new SubscriptionStateInvalidateEvent(); + } +} diff --git a/src/transport/subscription-worker/components/heartbeat-request.ts b/src/transport/subscription-worker/components/heartbeat-request.ts new file mode 100644 index 000000000..5e8634836 --- /dev/null +++ b/src/transport/subscription-worker/components/heartbeat-request.ts @@ -0,0 +1,178 @@ +import { TransportRequest } from '../../../core/types/transport-request'; +import uuidGenerator from '../../../core/components/uuid'; +import { BasePubNubRequest } from './request'; +import { Payload } from '../../../core/types/api'; +import { AccessToken } from './access-token'; + +export class HeartbeatRequest extends BasePubNubRequest { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Presence state associated with `userID` on {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + */ + readonly state: Record | undefined; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + + /** + * Create heartbeat request from received _transparent_ transport request. + * + * @param request - Object with heartbeat transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + * @returns Initialized and ready to use heartbeat request. + */ + static fromTransportRequest(request: TransportRequest, subscriptionKey: string, accessToken?: AccessToken) { + return new HeartbeatRequest(request, subscriptionKey, accessToken); + } + + /** + * Create heartbeat request from previously cached data. + * + * @param request - Object with subscribe transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [aggregatedChannelGroups] - List of aggregated channel groups for the same user. + * @param [aggregatedChannels] - List of aggregated channels for the same user. + * @param [aggregatedState] - State aggregated for the same user. + * @param [accessToken] - Access token with read permissions on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + * @retusns Initialized and ready to use heartbeat request. + */ + static fromCachedState( + request: TransportRequest, + subscriptionKey: string, + aggregatedChannelGroups: string[], + aggregatedChannels: string[], + aggregatedState?: Record, + accessToken?: AccessToken, + ) { + // Update request channels list (if required). + if (aggregatedChannels.length || aggregatedChannelGroups.length) { + const pathComponents = request.path.split('/'); + pathComponents[6] = aggregatedChannels.length ? [...aggregatedChannels].sort().join(',') : ','; + request.path = pathComponents.join('/'); + } + + // Update request channel groups list (if required). + if (aggregatedChannelGroups.length) + request.queryParameters!['channel-group'] = [...aggregatedChannelGroups].sort().join(','); + + // Update request `state` (if required). + if (aggregatedState && Object.keys(aggregatedState).length) + request.queryParameters!.state = JSON.stringify(aggregatedState); + else delete request.queryParameters!.aggregatedState; + + if (accessToken) request.queryParameters!.auth = accessToken.toString(); + request.identifier = uuidGenerator.createUUID(); + + return new HeartbeatRequest(request, subscriptionKey, accessToken); + } + + /** + * Create heartbeat request from received _transparent_ transport request. + * + * @param request - Object with heartbeat transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + */ + private constructor(request: TransportRequest, subscriptionKey: string, accessToken?: AccessToken) { + const channelGroups = HeartbeatRequest.channelGroupsFromRequest(request).filter( + (group) => !group.endsWith('-pnpres'), + ); + const channels = HeartbeatRequest.channelsFromRequest(request).filter((channel) => !channel.endsWith('-pnpres')); + + super(request, subscriptionKey, request.queryParameters!.uuid as string, channels, channelGroups, accessToken); + + // Clean up `state` from objects which is not used with request (if needed). + if (!request.queryParameters!.state || (request.queryParameters!.state as string).length === 0) return; + + const state = JSON.parse(request.queryParameters!.state as string) as Record; + for (const objectName of Object.keys(state)) + if (!this.channels.includes(objectName) && !this.channelGroups.includes(objectName)) delete state[objectName]; + + this.state = state; + } + // endregion + + // -------------------------------------------------------- + // ---------------------- Properties ---------------------- + // -------------------------------------------------------- + // region Properties + + /** + * Represent heartbeat request as identifier. + * + * Generated identifier will be identical for requests created for the same user. + */ + get asIdentifier() { + const auth = this.accessToken ? this.accessToken.asIdentifier : undefined; + return `${this.userId}-${this.subscribeKey}${auth ? `-${auth}` : ''}`; + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Serialize request for easier representation in logs. + * + * @returns Stringified `heartbeat` request. + */ + toString() { + return `HeartbeatRequest { channels: [${ + this.channels.length ? this.channels.map((channel) => `'${channel}'`).join(', ') : '' + }], channelGroups: [${ + this.channelGroups.length ? this.channelGroups.map((group) => `'${group}'`).join(', ') : '' + }] }`; + } + + /** + * Serialize request to "typed" JSON string. + * + * @returns "Typed" JSON string. + */ + toJSON() { + return this.toString(); + } + + /** + * Extract list of channels for presence announcement from request URI path. + * + * @param request - Transport request from which should be extracted list of channels for presence announcement. + * + * @returns List of channel names (not percent-decoded) for which `heartbeat` has been called. + */ + private static channelsFromRequest(request: TransportRequest): string[] { + const channels = request.path.split('/')[6]; + return channels === ',' ? [] : channels.split(',').filter((name) => name.length > 0); + } + + /** + * Extract list of channel groups for presence announcement from request query. + * + * @param request - Transport request from which should be extracted list of channel groups for presence announcement. + * + * @returns List of channel group names (not percent-decoded) for which `heartbeat` has been called. + */ + private static channelGroupsFromRequest(request: TransportRequest): string[] { + if (!request.queryParameters || !request.queryParameters['channel-group']) return []; + const group = request.queryParameters['channel-group'] as string; + return group.length === 0 ? [] : group.split(',').filter((name) => name.length > 0); + } + // endregion +} diff --git a/src/transport/subscription-worker/components/heartbeat-requests-manager.ts b/src/transport/subscription-worker/components/heartbeat-requests-manager.ts new file mode 100644 index 000000000..008dd785f --- /dev/null +++ b/src/transport/subscription-worker/components/heartbeat-requests-manager.ts @@ -0,0 +1,275 @@ +import { + PubNubClientEvent, + PubNubClientSendLeaveEvent, + PubNubClientAuthChangeEvent, + PubNubClientSendHeartbeatEvent, + PubNubClientIdentityChangeEvent, + PubNubClientHeartbeatIntervalChangeEvent, +} from './custom-events/client-event'; +import { + PubNubClientsManagerEvent, + PubNubClientManagerRegisterEvent, + PubNubClientManagerUnregisterEvent, +} from './custom-events/client-manager-event'; +import { HeartbeatStateEvent, HeartbeatStateHeartbeatEvent } from './custom-events/heartbeat-state-event'; +import { PubNubClientsManager } from './pubnub-clients-manager'; +import { HeartbeatRequest } from './heartbeat-request'; +import { RequestsManager } from './requests-manager'; +import { HeartbeatState } from './heartbeat-state'; +import { PubNubClient } from './pubnub-client'; + +/** + * Heartbeat requests manager responsible for heartbeat aggregation and backup of throttled clients (background tabs). + * + * On each heartbeat request from core PubNub client module manager will try to identify whether it is time to send it + * and also will try to aggregate call for channels / groups for the same user. + */ +export class HeartbeatRequestsManager extends RequestsManager { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Service response binary data decoder. + */ + private static textDecoder = new TextDecoder(); + + /** + * Map of unique user identifier (composed from multiple request object properties) to the aggregated heartbeat state. + * @private + */ + private heartbeatStates: Record = {}; + + /** + * Map of client identifiers to `AbortController` instances which is used to detach added listeners when PubNub client + * unregister. + */ + private clientAbortControllers: Record = {}; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + + /** + * Create heartbeat requests manager. + * + * @param clientsManager - Reference to the core PubNub clients manager to track their life-cycle and make + * corresponding state changes. + */ + constructor(private readonly clientsManager: PubNubClientsManager) { + super(); + this.subscribeOnClientEvents(clientsManager); + } + // endregion + + // -------------------------------------------------------- + // --------------------- Aggregation ---------------------- + // -------------------------------------------------------- + // region Aggregation + + /** + * Retrieve heartbeat state with which specific client is working. + * + * @param client - Reference to the PubNub client for which heartbeat state should be found. + * @returns Reference to the heartbeat state if client has ongoing requests. + */ + private heartbeatStateForClient(client: PubNubClient) { + for (const heartbeatState of Object.values(this.heartbeatStates)) + if (!!heartbeatState.stateForClient(client)) return heartbeatState; + + return undefined; + } + + /** + * Move client between heartbeat states. + * + * This function used when PubNub client changed its identity (`userId`) or auth (`access token`) and can't be + * aggregated with previous requests. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which should be moved to new state. + */ + private moveClient(client: PubNubClient) { + const state = this.heartbeatStateForClient(client); + const request = state ? state.requestForClient(client) : undefined; + if (!state || !request) return; + + this.removeClient(client); + this.addClient(client, request); + } + + /** + * Add client-provided heartbeat request into heartbeat state for aggregation. + * + * @param client - Reference to the client which provided heartbeat request. + * @param request - Reference to the heartbeat request which should be used in aggregation. + */ + private addClient(client: PubNubClient, request: HeartbeatRequest) { + const identifier = request.asIdentifier; + + let state = this.heartbeatStates[identifier]; + if (!state) { + state = this.heartbeatStates[identifier] = new HeartbeatState(identifier); + state.interval = client.heartbeatInterval ?? 0; + + // Make sure to receive updates from heartbeat state. + this.addListenerForHeartbeatStateEvents(state); + } else if ( + client.heartbeatInterval && + state.interval > 0 && + client.heartbeatInterval > 0 && + client.heartbeatInterval < state.interval + ) + state.interval = client.heartbeatInterval; + + state.addClientRequest(client, request); + } + + /** + * Remove client and its requests from further aggregated heartbeat calls. + * + * @param client - Reference to the PubNub client which should be removed from heartbeat state. + */ + private removeClient(client: PubNubClient) { + const state = this.heartbeatStateForClient(client); + if (!state) return; + + state.removeClient(client); + } + // endregion + + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + + /** + * Listen for PubNub clients manager events which affects aggregated subscribe / heartbeat requests. + * + * @param clientsManager - Clients manager for which change in clients should be tracked. + */ + private subscribeOnClientEvents(clientsManager: PubNubClientsManager) { + // Listen for new core PubNub client registrations. + clientsManager.addEventListener(PubNubClientsManagerEvent.Registered, (evt) => { + const { client } = evt as PubNubClientManagerRegisterEvent; + + // Keep track of the client's listener abort controller. + const abortController = new AbortController(); + this.clientAbortControllers[client.identifier] = abortController; + + client.addEventListener(PubNubClientEvent.Disconnect, () => this.removeClient(client), { + signal: abortController.signal, + }); + client.addEventListener( + PubNubClientEvent.IdentityChange, + (event) => { + if (!(event instanceof PubNubClientIdentityChangeEvent)) return; + // Make changes into state only if `userId` actually changed. + if ( + !!event.oldUserId !== !!event.newUserId || + (event.oldUserId && event.newUserId && event.newUserId !== event.oldUserId) + ) { + const state = this.heartbeatStateForClient(client); + const request = state ? state.requestForClient(client) : undefined; + if (request) request.userId = event.newUserId; + + this.moveClient(client); + } + }, + { + signal: abortController.signal, + }, + ); + client.addEventListener( + PubNubClientEvent.AuthChange, + (event) => { + if (!(event instanceof PubNubClientAuthChangeEvent)) return; + const state = this.heartbeatStateForClient(client); + const request = state ? state.requestForClient(client) : undefined; + if (request) request.accessToken = event.newAuth; + + // Check whether the client should be moved to another state because of a permissions change or whether the + // same token with the same permissions should be used for the next requests. + if ( + !!event.oldAuth !== !!event.newAuth || + (event.oldAuth && event.newAuth && !event.newAuth.equalTo(event.oldAuth)) + ) + this.moveClient(client); + }, + { + signal: abortController.signal, + }, + ); + client.addEventListener( + PubNubClientEvent.HeartbeatIntervalChange, + (evt) => { + const event = evt as PubNubClientHeartbeatIntervalChangeEvent; + const state = this.heartbeatStateForClient(client); + if (state) state.interval = event.newInterval ?? 0; + }, + { signal: abortController.signal }, + ); + client.addEventListener( + PubNubClientEvent.SendHeartbeatRequest, + (evt) => this.addClient(client, (evt as PubNubClientSendHeartbeatEvent).request), + { signal: abortController.signal }, + ); + client.addEventListener( + PubNubClientEvent.SendLeaveRequest, + (evt) => { + const { request } = evt as PubNubClientSendLeaveEvent; + const state = this.heartbeatStateForClient(client); + if (!state) return; + + state.removeFromClientState(client, request.channels, request.channelGroups); + }, + { signal: abortController.signal }, + ); + }); + // Listen for core PubNub client module disappearance. + clientsManager.addEventListener(PubNubClientsManagerEvent.Unregistered, (evt) => { + const { client } = evt as PubNubClientManagerUnregisterEvent; + + // Remove all listeners added for the client. + const abortController = this.clientAbortControllers[client.identifier]; + delete this.clientAbortControllers[client.identifier]; + if (abortController) abortController.abort(); + + this.removeClient(client); + }); + } + + /** + * Listen for heartbeat state events. + * + * @param state - Reference to the subscription object for which listeners should be added. + */ + private addListenerForHeartbeatStateEvents(state: HeartbeatState) { + const abortController = new AbortController(); + + state.addEventListener( + HeartbeatStateEvent.Heartbeat, + (evt) => { + const { request } = evt as HeartbeatStateHeartbeatEvent; + + this.sendRequest( + request, + (fetchRequest, response) => request.handleProcessingSuccess(fetchRequest, response), + (fetchRequest, error) => request.handleProcessingError(fetchRequest, error), + ); + }, + { signal: abortController.signal }, + ); + state.addEventListener( + HeartbeatStateEvent.Invalidated, + () => { + delete this.heartbeatStates[state.identifier]; + abortController.abort(); + }, + { signal: abortController.signal, once: true }, + ); + } + // endregion +} diff --git a/src/transport/subscription-worker/components/heartbeat-state.ts b/src/transport/subscription-worker/components/heartbeat-state.ts new file mode 100644 index 000000000..02bdace54 --- /dev/null +++ b/src/transport/subscription-worker/components/heartbeat-state.ts @@ -0,0 +1,385 @@ +import { + RequestErrorEvent, + RequestSuccessEvent, + PubNubSharedWorkerRequestEvents, +} from './custom-events/request-processing-event'; +import { HeartbeatStateHeartbeatEvent, HeartbeatStateInvalidateEvent } from './custom-events/heartbeat-state-event'; +import { RequestSendingSuccess } from '../subscription-worker-types'; +import { HeartbeatRequest } from './heartbeat-request'; +import { Payload } from '../../../core/types/api'; +import { PubNubClient } from './pubnub-client'; +import { AccessToken } from './access-token'; + +export class HeartbeatState extends EventTarget { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Map of client identifiers to their portion of data which affects heartbeat state. + * + * **Note:** This information removed only with {@link HeartbeatState.removeClient|removeClient} function call. + */ + private clientsState: Record< + string, + { channels: string[]; channelGroups: string[]; state?: Record } + > = {}; + + /** + * Map of client to its requests which is pending for service request processing results. + */ + private requests: Record = {}; + + /** + * Backout timer timeout. + */ + private timeout?: ReturnType; + + /** + * Time when previous heartbeat request has been done. + */ + private lastHeartbeatTimestamp: number = 0; + + /** + * Reference to the most suitable access token to access {@link HeartbeatState#channels|channels} and + * {@link HeartbeatState#channelGroups|channelGroups}. + */ + private _accessToken?: AccessToken; + + /** + * Stores response from the previous heartbeat request. + */ + private previousRequestResult?: RequestSendingSuccess; + + /** + * Stores whether automated _backup_ timer can fire or not. + */ + private canSendBackupHeartbeat = true; + + /** + * Whether previous call failed with `Access Denied` error or not. + */ + private isAccessDeniedError = false; + + /** + * Presence heartbeat interval. + * + * Value used to decide whether new request should be handled right away or should wait for _backup_ timer in state + * to send aggregated request. + */ + private _interval: number = 0; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructor ---------------------- + // -------------------------------------------------------- + // region Constructor + + /** + * Create heartbeat state management object. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + constructor(public readonly identifier: string) { + super(); + } + // endregion + + // -------------------------------------------------------- + // --------------------- Properties ----------------------- + // -------------------------------------------------------- + // region Properties + + /** + * Update presence heartbeat interval. + * + * @param value - New heartbeat interval. + */ + set interval(value: number) { + const changed = this._interval !== value; + this._interval = value; + + if (!changed) return; + + // Restart timer if required. + if (value === 0) this.stopTimer(); + else this.startTimer(); + } + + /** + * Update access token which should be used for aggregated heartbeat requests. + * + * @param value - New access token for heartbeat requests. + */ + set accessToken(value: AccessToken | undefined) { + if (!value) { + this._accessToken = value; + return; + } + + const accessTokens = Object.values(this.requests) + .filter((request) => !!request.accessToken) + .map((request) => request.accessToken!); + accessTokens.push(value); + + this._accessToken = accessTokens.sort(AccessToken.compare).pop(); + + // Restart _backup_ heartbeat if previous call failed because of permissions error. + if (this.isAccessDeniedError) { + this.canSendBackupHeartbeat = true; + this.startTimer(this.presenceTimerTimeout()); + } + } + // endregion + + // -------------------------------------------------------- + // ---------------------- Accessors ----------------------- + // -------------------------------------------------------- + // region Accessors + + /** + * Retrieve portion of heartbeat state which is related to the specific client. + * + * @param client - Reference to the PubNub client for which state should be retrieved. + * @returns PubNub client's state in heartbeat. + */ + stateForClient(client: PubNubClient): + | { + channels: string[]; + channelGroups: string[]; + state?: Record; + } + | undefined { + if (!this.clientsState[client.identifier]) return undefined; + + const clientState = this.clientsState[client.identifier]; + + return clientState + ? { channels: [...clientState.channels], channelGroups: [...clientState.channelGroups], state: clientState.state } + : { channels: [], channelGroups: [] }; + } + + /** + * Retrieve recent heartbeat request for the client. + * + * @param client - Reference to the client for which request should be retrieved. + * @returns List of client's ongoing requests. + */ + requestForClient(client: PubNubClient): HeartbeatRequest | undefined { + return this.requests[client.identifier]; + } + // endregion + + // -------------------------------------------------------- + // --------------------- Aggregation ---------------------- + // -------------------------------------------------------- + // region Aggregation + + /** + * Add new client's request to the state. + * + * @param client - Reference to PubNub client which is adding new requests for processing. + * @param request - New client-provided heartbeat request for processing. + */ + addClientRequest(client: PubNubClient, request: HeartbeatRequest) { + this.requests[client.identifier] = request; + this.clientsState[client.identifier] = { channels: request.channels, channelGroups: request.channelGroups }; + if (request.state) this.clientsState[client.identifier].state = { ...request.state }; + + // Update access token information (use the one which will provide permissions for longer period). + const sortedTokens = Object.values(this.requests) + .filter((request) => !!request.accessToken) + .map((request) => request.accessToken!) + .sort(AccessToken.compare); + if (sortedTokens && sortedTokens.length > 0) this._accessToken = sortedTokens.pop(); + + this.sendAggregatedHeartbeat(request); + } + + /** + * Remove client and requests associated with it from the state. + * + * @param client - Reference to the PubNub client which should be removed. + */ + removeClient(client: PubNubClient) { + delete this.clientsState[client.identifier]; + delete this.requests[client.identifier]; + + // Stop backup timer if there is no more channels and groups left. + if (!Object.keys(this.clientsState).length) { + this.stopTimer(); + this.dispatchEvent(new HeartbeatStateInvalidateEvent()); + } + } + + removeFromClientState(client: PubNubClient, channels: string[], channelGroups: string[]) { + const clientState = this.clientsState[client.identifier]; + if (!clientState) return; + + clientState.channelGroups = clientState.channelGroups.filter((group) => !channelGroups.includes(group)); + clientState.channels = clientState.channels.filter((channel) => !channels.includes(channel)); + + if (clientState.channels.length === 0 && clientState.channelGroups.length === 0) { + this.removeClient(client); + return; + } + + // Clean up user's presence state from removed channels and groups. + if (!clientState.state) return; + Object.keys(clientState.state).forEach((key) => { + if (!clientState.channels.includes(key) && !clientState.channelGroups.includes(key)) + delete clientState.state![key]; + }); + } + + /** + * Start "backup" presence heartbeat timer. + * + * @param targetInterval - Interval after which heartbeat request should be sent. + */ + private startTimer(targetInterval?: number) { + this.stopTimer(); + if (Object.keys(this.clientsState).length === 0) return; + + this.timeout = setTimeout(() => this.handlePresenceTimer(), (targetInterval ?? this._interval) * 1000); + } + + /** + * Stop "backup" presence heartbeat timer. + */ + private stopTimer() { + if (this.timeout) clearTimeout(this.timeout); + this.timeout = undefined; + } + + /** + * Send aggregated heartbeat request (if possible). + * + * @param [request] - Client provided request which tried to announce presence. + */ + private sendAggregatedHeartbeat(request?: HeartbeatRequest) { + if (this.lastHeartbeatTimestamp !== 0) { + // Check whether it is too soon to send request or not. + const expected = this.lastHeartbeatTimestamp + this._interval * 1000; + let leeway = this._interval * 0.05; + if (this._interval - leeway < 3) leeway = 0; + const current = Date.now(); + + if (expected - current > leeway * 1000) { + if (request && !!this.previousRequestResult) { + const fetchRequest = request.asFetchRequest; + const result = { + ...this.previousRequestResult, + clientIdentifier: request.client.identifier, + identifier: request.identifier, + url: fetchRequest.url, + }; + request.handleProcessingStarted(); + request.handleProcessingSuccess(fetchRequest, result); + return; + } else if (!request) return; + } + } + + const requests = Object.values(this.requests); + const baseRequest = requests[Math.floor(Math.random() * requests.length)]; + const aggregatedRequest = { ...baseRequest.request }; + let state: Record = {}; + const channelGroups = new Set(); + const channels = new Set(); + + Object.values(this.clientsState).forEach((clientState) => { + if (clientState.state) state = { ...state, ...clientState.state }; + clientState.channelGroups.forEach(channelGroups.add, channelGroups); + clientState.channels.forEach(channels.add, channels); + }); + + this.lastHeartbeatTimestamp = Date.now(); + const serviceRequest = HeartbeatRequest.fromCachedState( + aggregatedRequest, + requests[0].subscribeKey, + [...channelGroups], + [...channels], + Object.keys(state).length > 0 ? state : undefined, + this._accessToken, + ); + + // Set service request for all client-provided requests without response. + Object.values(this.requests).forEach( + (request) => !request.serviceRequest && (request.serviceRequest = serviceRequest), + ); + + this.addListenersForRequest(serviceRequest); + this.dispatchEvent(new HeartbeatStateHeartbeatEvent(serviceRequest)); + + // Restart _backup_ timer after regular client-provided request triggered heartbeat. + if (request) this.startTimer(); + } + // endregion + + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + + /** + * Add listeners to the service request. + * + * Listeners used to capture last service success response and mark whether further _backup_ requests possible or not. + * + * @param request - Service `heartbeat` request for which events will be listened once. + */ + private addListenersForRequest(request: HeartbeatRequest) { + const ac = new AbortController(); + const callback = (evt: Event) => { + // Clean up service request listeners. + ac.abort(); + + if (evt instanceof RequestSuccessEvent) { + const { response } = evt as RequestSuccessEvent; + this.previousRequestResult = response; + } else if (evt instanceof RequestErrorEvent) { + const { error } = evt as RequestErrorEvent; + this.canSendBackupHeartbeat = true; + this.isAccessDeniedError = false; + + if (error.response && error.response.status >= 400 && error.response.status < 500) { + this.isAccessDeniedError = error.response.status === 403; + this.canSendBackupHeartbeat = false; + } + } + }; + request.addEventListener(PubNubSharedWorkerRequestEvents.Success, callback, { signal: ac.signal, once: true }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Error, callback, { signal: ac.signal, once: true }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Canceled, callback, { signal: ac.signal, once: true }); + } + + /** + * Handle periodic _backup_ heartbeat timer. + */ + private handlePresenceTimer() { + if (Object.keys(this.clientsState).length === 0 || !this.canSendBackupHeartbeat) return; + + const targetInterval = this.presenceTimerTimeout(); + + this.sendAggregatedHeartbeat(); + this.startTimer(targetInterval); + } + + /** + * Compute timeout for _backup_ heartbeat timer. + * + * @returns Number of seconds after which new aggregated heartbeat request should be sent. + */ + private presenceTimerTimeout() { + const timePassed = (Date.now() - this.lastHeartbeatTimestamp) / 1000; + let targetInterval = this._interval; + if (timePassed < targetInterval) targetInterval -= timePassed; + if (targetInterval === this._interval) targetInterval += 0.05; + targetInterval = Math.max(targetInterval, 3); + + return targetInterval; + } + // endregion +} diff --git a/src/transport/subscription-worker/components/helpers.ts b/src/transport/subscription-worker/components/helpers.ts new file mode 100644 index 000000000..9dbcbab2b --- /dev/null +++ b/src/transport/subscription-worker/components/helpers.ts @@ -0,0 +1,80 @@ +import { TransportMethod, TransportRequest } from '../../../core/types/transport-request'; +import uuidGenerator from '../../../core/components/uuid'; +import { Query } from '../../../core/types/api'; +import { LeaveRequest } from './leave-request'; +import { PubNubClient } from './pubnub-client'; + +/** + * Create service `leave` request for a specific PubNub client with channels and groups for removal. + * + * @param client - Reference to the PubNub client whose credentials should be used for new request. + * @param channels - List of channels that are not used by any other clients and can be left. + * @param channelGroups - List of channel groups that are not used by any other clients and can be left. + * @returns Service `leave` request. + */ +export const leaveRequest = (client: PubNubClient, channels: string[], channelGroups: string[]) => { + channels = channels + .filter((channel) => !channel.endsWith('-pnpres')) + .map((channel) => encodeString(channel)) + .sort(); + channelGroups = channelGroups + .filter((channelGroup) => !channelGroup.endsWith('-pnpres')) + .map((channelGroup) => encodeString(channelGroup)) + .sort(); + + if (channels.length === 0 && channelGroups.length === 0) return undefined; + + const channelGroupsString: string | undefined = channelGroups.length > 0 ? channelGroups.join(',') : undefined; + const channelsString = channels.length === 0 ? ',' : channels.join(','); + + const query: Query = { + instanceid: client.identifier, + uuid: client.userId, + requestid: uuidGenerator.createUUID(), + ...(client.accessToken ? { auth: client.accessToken.toString() } : {}), + ...(channelGroupsString ? { 'channel-group': channelGroupsString } : {}), + }; + + const transportRequest: TransportRequest = { + origin: client.origin, + path: `/v2/presence/sub-key/${client.subKey}/channel/${channelsString}/leave`, + queryParameters: query, + method: TransportMethod.GET, + headers: {}, + timeout: 10, + cancellable: false, + compressible: false, + identifier: query.requestid as string, + }; + + return LeaveRequest.fromTransportRequest(transportRequest, client.subKey, client.accessToken); +}; + +/** + * Stringify request query key/value pairs. + * + * @param query - Request query object. + * @returns Stringified query object. + */ +export const queryStringFromObject = (query: Query) => { + return Object.keys(query) + .map((key) => { + const queryValue = query[key]; + if (!Array.isArray(queryValue)) return `${key}=${encodeString(queryValue)}`; + + return queryValue.map((value) => `${key}=${encodeString(value)}`).join('&'); + }) + .join('&'); +}; + +/** + * Percent-encode input string. + * + * **Note:** Encode content in accordance of the `PubNub` service requirements. + * + * @param input - Source string or number for encoding. + * @returns Percent-encoded string. + */ +export const encodeString = (input: string | number) => { + return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); +}; diff --git a/src/transport/subscription-worker/components/leave-request.ts b/src/transport/subscription-worker/components/leave-request.ts new file mode 100644 index 000000000..4461621a5 --- /dev/null +++ b/src/transport/subscription-worker/components/leave-request.ts @@ -0,0 +1,115 @@ +import { TransportRequest } from '../../../core/types/transport-request'; +import { BasePubNubRequest } from './request'; +import { AccessToken } from './access-token'; + +export class LeaveRequest extends BasePubNubRequest { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Not filtered list of channel groups for which user's presence will be announced. + */ + readonly allChannelGroups: string[]; + + /** + * Not filtered list of channels for which user's presence will be announced. + */ + readonly allChannels: string[]; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + + /** + * Create `leave` request from received _transparent_ transport request. + * + * @param request - Object with heartbeat transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + * @returns Initialized and ready to use `leave` request. + */ + static fromTransportRequest(request: TransportRequest, subscriptionKey: string, accessToken?: AccessToken) { + return new LeaveRequest(request, subscriptionKey, accessToken); + } + + /** + * Create `leave` request from received _transparent_ transport request. + * + * @param request - Object with heartbeat transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + */ + private constructor(request: TransportRequest, subscriptionKey: string, accessToken?: AccessToken) { + const allChannelGroups = LeaveRequest.channelGroupsFromRequest(request); + const allChannels = LeaveRequest.channelsFromRequest(request); + const channelGroups = allChannelGroups.filter((group) => !group.endsWith('-pnpres')); + const channels = allChannels.filter((channel) => !channel.endsWith('-pnpres')); + + super(request, subscriptionKey, request.queryParameters!.uuid as string, channels, channelGroups, accessToken); + + this.allChannelGroups = allChannelGroups; + this.allChannels = allChannels; + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Serialize request for easier representation in logs. + * + * @returns Stringified `leave` request. + */ + toString() { + return `LeaveRequest { channels: [${ + this.channels.length ? this.channels.map((channel) => `'${channel}'`).join(', ') : '' + }], channelGroups: [${ + this.channelGroups.length ? this.channelGroups.map((group) => `'${group}'`).join(', ') : '' + }] }`; + } + + /** + * Serialize request to "typed" JSON string. + * + * @returns "Typed" JSON string. + */ + toJSON() { + return this.toString(); + } + + /** + * Extract list of channels for presence announcement from request URI path. + * + * @param request - Transport request from which should be extracted list of channels for presence announcement. + * + * @returns List of channel names (not percent-decoded) for which `leave` has been called. + */ + private static channelsFromRequest(request: TransportRequest): string[] { + const channels = request.path.split('/')[6]; + return channels === ',' ? [] : channels.split(',').filter((name) => name.length > 0); + } + + /** + * Extract list of channel groups for presence announcement from request query. + * + * @param request - Transport request from which should be extracted list of channel groups for presence announcement. + * + * @returns List of channel group names (not percent-decoded) for which `leave` has been called. + */ + private static channelGroupsFromRequest(request: TransportRequest): string[] { + if (!request.queryParameters || !request.queryParameters['channel-group']) return []; + const group = request.queryParameters['channel-group'] as string; + return group.length === 0 ? [] : group.split(',').filter((name) => name.length > 0); + } + // endregion +} diff --git a/src/transport/subscription-worker/components/logger.ts b/src/transport/subscription-worker/components/logger.ts new file mode 100644 index 000000000..5f21a44c1 --- /dev/null +++ b/src/transport/subscription-worker/components/logger.ts @@ -0,0 +1,87 @@ +import type { ClientLogMessage } from '../subscription-worker-types'; +import { LogLevel } from '../../../core/interfaces/logger'; + +/** + * Custom {@link Logger} implementation to send logs to the core PubNub client module from the shared worker context. + */ +export class ClientLogger { + /** + * Create logger for specific PubNub client representation object. + * + * @param minLogLevel - Minimum messages log level to be logged. + * @param port - Message port for two-way communication with core PunNub client module. + */ + constructor( + public minLogLevel: LogLevel, + private readonly port: MessagePort, + ) {} + + /** + * Process a `debug` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + debug(message: string | ClientLogMessage | (() => ClientLogMessage)) { + this.log(message, LogLevel.Debug); + } + + /** + * Process a `error` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + error(message: string | ClientLogMessage | (() => ClientLogMessage)): void { + this.log(message, LogLevel.Error); + } + + /** + * Process an `info` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + info(message: string | ClientLogMessage | (() => ClientLogMessage)): void { + this.log(message, LogLevel.Info); + } + + /** + * Process a `trace` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + trace(message: string | ClientLogMessage | (() => ClientLogMessage)): void { + this.log(message, LogLevel.Trace); + } + + /** + * Process an `warn` level message. + * + * @param message - Message which should be handled by custom logger implementation. + */ + warn(message: string | ClientLogMessage | (() => ClientLogMessage)): void { + this.log(message, LogLevel.Warn); + } + + /** + * Send log entry to the core PubNub client module. + * + * @param message - Object which should be sent to the core PubNub client module. + * @param level - Log entry level (will be handled by if core PunNub client module minimum log level matches). + */ + private log(message: string | ClientLogMessage | (() => ClientLogMessage), level: LogLevel) { + // Discard logged message if logger not enabled. + if (level < this.minLogLevel) return; + + let entry: ClientLogMessage & { level?: LogLevel }; + if (typeof message === 'string') entry = { messageType: 'text', message }; + else if (typeof message === 'function') entry = message(); + else entry = message; + entry.level = level; + + try { + this.port.postMessage({ type: 'shared-worker-console-log', message: entry }); + } catch (error) { + if (this.minLogLevel !== LogLevel.None) + console.error(`[SharedWorker] Unable send message using message port: ${error}`); + } + } +} diff --git a/src/transport/subscription-worker/components/pubnub-client.ts b/src/transport/subscription-worker/components/pubnub-client.ts new file mode 100644 index 000000000..6a5c76c36 --- /dev/null +++ b/src/transport/subscription-worker/components/pubnub-client.ts @@ -0,0 +1,509 @@ +import { + PubNubClientSendLeaveEvent, + PubNubClientAuthChangeEvent, + PubNubClientDisconnectEvent, + PubNubClientUnregisterEvent, + PubNubClientSendHeartbeatEvent, + PubNubClientSendSubscribeEvent, + PubNubClientIdentityChangeEvent, + PubNubClientCancelSubscribeEvent, + PubNubClientHeartbeatIntervalChangeEvent, +} from './custom-events/client-event'; +import { + ClientEvent, + UpdateEvent, + SendRequestEvent, + CancelRequestEvent, + RequestSendingError, + SubscriptionWorkerEvent, +} from '../subscription-worker-types'; +import { + RequestErrorEvent, + RequestCancelEvent, + RequestSuccessEvent, + PubNubSharedWorkerRequestEvents, +} from './custom-events/request-processing-event'; +import { LogLevel } from '../../../core/interfaces/logger'; +import { HeartbeatRequest } from './heartbeat-request'; +import { SubscribeRequest } from './subscribe-request'; +import { Payload } from '../../../core/types/api'; +import { LeaveRequest } from './leave-request'; +import { BasePubNubRequest } from './request'; +import { AccessToken } from './access-token'; +import { ClientLogger } from './logger'; + +/** + * PubNub client representation in Shared Worker context. + */ +export class PubNubClient extends EventTarget { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Map of ongoing PubNub client requests. + * + * Unique request identifiers mapped to the requests requested by the core PubNub client module. + */ + private readonly requests: Record = {}; + + /** + * Controller, which is used on PubNub client unregister event to clean up listeners. + */ + private readonly listenerAbortController = new AbortController(); + + /** + * Map of user's presence state for channels/groups after previous subscribe request. + * + * **Note:** Keep a local cache to reduce the amount of parsing with each received subscribe send request. + */ + private cachedSubscriptionState?: Record; + + /** + * List of subscription channel groups after previous subscribe request. + * + * **Note:** Keep a local cache to reduce the amount of parsing with each received subscribe send request. + */ + private cachedSubscriptionChannelGroups: string[] = []; + + /** + * List of subscription channels after previous subscribe request. + * + * **Note:** Keep a local cache to reduce the amount of parsing with each received subscribe send request. + */ + private cachedSubscriptionChannels: string[] = []; + + /** + * How often the client will announce itself to the server. The value is in seconds. + */ + private _heartbeatInterval?: number; + + /** + * Access token to have `read` access to resources used by this client. + */ + private _accessToken?: AccessToken; + + /** + * Last time, the core PubNub client module responded with the `PONG` event. + */ + private _lastPongEvent?: number; + + /** + * Origin which is used to access PubNub REST API. + */ + private _origin?: string; + + /** + * Last time, `SharedWorker` sent `PING` request. + */ + lastPingRequest?: number; + + /** + * Client-specific logger that will send log entries to the core PubNub client module. + */ + readonly logger: ClientLogger; + + /** + * Whether {@link PubNubClient|PubNub} client has been invalidated (unregistered) or not. + */ + private _invalidated = false; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + + /** + * Create PubNub client. + * + * @param identifier - Unique PubNub client identifier. + * @param subKey - Subscribe REST API access key. + * @param userId - Unique identifier of the user currently configured for the PubNub client. + * @param port - Message port for two-way communication with core PubNub client module. + * @param logLevel - Minimum messages log level which should be passed to the `Subscription` worker logger. + * @param [heartbeatInterval] - Interval that is used to announce a user's presence on channels/groups. + */ + constructor( + readonly identifier: string, + readonly subKey: string, + public userId: string, + private readonly port: MessagePort, + logLevel: LogLevel, + heartbeatInterval?: number, + ) { + super(); + + this.logger = new ClientLogger(logLevel, this.port); + this._heartbeatInterval = heartbeatInterval; + this.subscribeOnEvents(); + } + + /** + * Clean up resources used by this PubNub client. + */ + invalidate(dispatchEvent = false) { + // Remove the client's listeners. + this.listenerAbortController.abort(); + this._invalidated = true; + + this.cancelRequests(); + } + // endregion + + // -------------------------------------------------------- + // ---------------------- Properties ---------------------- + // -------------------------------------------------------- + // region Properties + + /** + * Retrieve origin which is used to access PubNub REST API. + * + * @returns Origin which is used to access PubNub REST API. + */ + get origin(): string { + return this._origin ?? ''; + } + + /** + * Retrieve heartbeat interval, which is used to announce a user's presence on channels/groups. + * + * @returns Heartbeat interval, which is used to announce a user's presence on channels/groups. + */ + get heartbeatInterval() { + return this._heartbeatInterval; + } + + /** + * Retrieve an access token to have `read` access to resources used by this client. + * + * @returns Access token to have `read` access to resources used by this client. + */ + get accessToken() { + return this._accessToken; + } + + /** + * Retrieve whether the {@link PubNubClient|PubNub} client has been invalidated (unregistered) or not. + * + * @returns `true` if the client has been invalidated during unregistration. + */ + get isInvalidated() { + return this._invalidated; + } + + /** + * Retrieve the last time, the core PubNub client module responded with the `PONG` event. + * + * @returns Last time, the core PubNub client module responded with the `PONG` event. + */ + get lastPongEvent() { + return this._lastPongEvent; + } + // endregion + + // -------------------------------------------------------- + // --------------------- Communication -------------------- + // -------------------------------------------------------- + // region Communication + + /** + * Post event to the core PubNub client module. + * + * @param event - Subscription worker event payload. + * @returns `true` if the event has been sent without any issues. + */ + postEvent(event: SubscriptionWorkerEvent) { + try { + this.port.postMessage(event); + return true; + } catch (error) { + this.logger.error(`Unable send message using message port: ${error}`); + } + + return false; + } + // endregion + + // -------------------------------------------------------- + // -------------------- Event handlers -------------------- + // -------------------------------------------------------- + // region Event handlers + + /** + * Subscribe to client-specific signals from the core PubNub client module. + */ + private subscribeOnEvents() { + this.port.addEventListener( + 'message', + (event: MessageEvent) => { + if (event.data.type === 'client-unregister') this.handleUnregisterEvent(); + else if (event.data.type === 'client-update') this.handleConfigurationUpdateEvent(event.data); + else if (event.data.type === 'send-request') this.handleSendRequestEvent(event.data); + else if (event.data.type === 'cancel-request') this.handleCancelRequestEvent(event.data); + else if (event.data.type === 'client-disconnect') this.handleDisconnectEvent(); + else if (event.data.type === 'client-pong') this.handlePongEvent(); + }, + { signal: this.listenerAbortController.signal }, + ); + } + + /** + * Handle PubNub client unregister event. + * + * During unregister handling, the following changes will happen: + * - remove from the clients hash map ({@link PubNubClientsManager|clients manager}) + * - reset long-poll request (remove channels/groups that have been used only by this client) + * - stop backup heartbeat timer + */ + private handleUnregisterEvent() { + this.invalidate(); + this.dispatchEvent(new PubNubClientUnregisterEvent(this)); + } + + /** + * Update client's configuration. + * + * During configuration update handling, the following changes may happen (depending on the changed data): + * - reset long-poll request (remove channels/groups that have been used only by this client from active request) on + * `userID` change. + * - heartbeat will be sent immediately on `userID` change (to announce new user presence). **Note:** proper flow will + * be `unsubscribeAll` and then, with changed `userID` subscribe back, but the code will handle hard reset as well. + * - _backup_ heartbeat timer reschedule in on `heartbeatInterval` change. + * + * @param event - Object with up-to-date client settings, which should be reflected in SharedWorker's state for the + * registered client. + */ + private handleConfigurationUpdateEvent(event: UpdateEvent) { + const { userId, accessToken: authKey, preProcessedToken: token, heartbeatInterval, workerLogLevel } = event; + + this.logger.minLogLevel = workerLogLevel; + this.logger.debug(() => ({ + messageType: 'object', + message: { userId, authKey, token, heartbeatInterval, workerLogLevel }, + details: 'Update client configuration with parameters:', + })); + + // Check whether authentication information has been changed or not. + // Important: If changed, this should be notified before a potential identity change event. + if (!!authKey || !!this.accessToken) { + const accessToken = authKey ? new AccessToken(authKey, (token ?? {}).token, (token ?? {}).expiration) : undefined; + + // Check whether the access token really changed or not. + if ( + !!accessToken !== !!this.accessToken || + (!!accessToken && this.accessToken && !accessToken.equalTo(this.accessToken, true)) + ) { + const oldValue = this._accessToken; + this._accessToken = accessToken; + + // Make sure that all ongoing subscribe (usually should be only one at a time) requests use proper + // `accessToken`. + Object.values(this.requests) + .filter( + (request) => + (!request.completed && request instanceof SubscribeRequest) || request instanceof HeartbeatRequest, + ) + .forEach((request) => (request.accessToken = accessToken)); + + this.dispatchEvent(new PubNubClientAuthChangeEvent(this, accessToken, oldValue)); + } + } + + // Check whether PubNub client identity has been changed or not. + if (this.userId !== userId) { + const oldValue = this.userId; + this.userId = userId; + + // Make sure that all ongoing subscribe (usually should be only one at a time) requests use proper `userId`. + // **Note:** Core PubNub client module docs have a warning saying that `userId` should be changed only after + // unsubscribe/disconnect to properly update the user's presence. + Object.values(this.requests) + .filter( + (request) => + (!request.completed && request instanceof SubscribeRequest) || request instanceof HeartbeatRequest, + ) + .forEach((request) => (request.userId = userId)); + + this.dispatchEvent(new PubNubClientIdentityChangeEvent(this, oldValue, userId)); + } + + if (this._heartbeatInterval !== heartbeatInterval) { + const oldValue = this._heartbeatInterval; + this._heartbeatInterval = heartbeatInterval; + + this.dispatchEvent(new PubNubClientHeartbeatIntervalChangeEvent(this, heartbeatInterval, oldValue)); + } + } + + /** + * Handle requests send request from the core PubNub client module. + * + * @param data - Object with received request details. + */ + private handleSendRequestEvent(data: SendRequestEvent) { + let request: BasePubNubRequest; + + // Setup client's authentication token from request (if it hasn't been set yet) + if (!this._accessToken && !!data.request.queryParameters?.auth && !!data.preProcessedToken) { + const auth = data.request.queryParameters.auth as string; + this._accessToken = new AccessToken(auth, data.preProcessedToken.token, data.preProcessedToken.expiration); + } + + if (data.request.path.startsWith('/v2/subscribe')) { + if ( + SubscribeRequest.useCachedState(data.request) && + (this.cachedSubscriptionChannelGroups.length || this.cachedSubscriptionChannels.length) + ) { + request = SubscribeRequest.fromCachedState( + data.request, + this.subKey, + this.cachedSubscriptionChannelGroups, + this.cachedSubscriptionChannels, + this.cachedSubscriptionState, + this.accessToken, + ); + } else { + request = SubscribeRequest.fromTransportRequest(data.request, this.subKey, this.accessToken); + + // Update the cached client's subscription state. + this.cachedSubscriptionChannelGroups = [...request.channelGroups]; + this.cachedSubscriptionChannels = [...request.channels]; + if ((request as SubscribeRequest).state) + this.cachedSubscriptionState = { ...(request as SubscribeRequest).state }; + else this.cachedSubscriptionState = undefined; + } + } else if (data.request.path.endsWith('/heartbeat')) + request = HeartbeatRequest.fromTransportRequest(data.request, this.subKey, this.accessToken); + else request = LeaveRequest.fromTransportRequest(data.request, this.subKey, this.accessToken); + + request.client = this; + this.requests[request.request.identifier] = request; + + if (!this._origin) this._origin = request.origin; + + // Set client state cleanup on request processing completion (with any outcome). + this.listenRequestCompletion(request); + + // Notify request managers about new client-provided request. + this.dispatchEvent(this.eventWithRequest(request)); + } + + /** + * Handle on-demand request cancellation. + * + * **Note:** Cancellation will dispatch the event handled in `listenRequestCompletion` and remove target request from + * the PubNub client requests' list. + * + * @param data - Object with canceled request information. + */ + private handleCancelRequestEvent(data: CancelRequestEvent) { + if (!this.requests[data.identifier]) return; + const request = this.requests[data.identifier]; + request.cancel('Cancel request'); + } + + /** + * Handle PubNub client disconnect event. + * + * **Note:** On disconnect, the core {@link PubNubClient|PubNub} client module will terminate `client`-provided + * subscribe requests ({@link handleCancelRequestEvent} will be called). + * + * During disconnection handling, the following changes will happen: + * - reset subscription state ({@link SubscribeRequestsManager|subscription requests manager}) + * - stop backup heartbeat timer + * - reset heartbeat state ({@link HeartbeatRequestsManager|heartbeat requests manager}) + */ + private handleDisconnectEvent() { + this.dispatchEvent(new PubNubClientDisconnectEvent(this)); + } + + /** + * Handle ping-pong response from the core PubNub client module. + */ + private handlePongEvent() { + this._lastPongEvent = Date.now() / 1000; + } + + /** + * Listen for any request outcome to clean + * + * @param request - Request for which processing outcome should be observed. + */ + private listenRequestCompletion(request: BasePubNubRequest) { + const ac = new AbortController(); + const callback = (evt: Event) => { + delete this.requests[request.identifier]; + ac.abort(); + + if (evt instanceof RequestSuccessEvent) this.postEvent((evt as RequestSuccessEvent).response); + else if (evt instanceof RequestErrorEvent) this.postEvent((evt as RequestErrorEvent).error); + else if (evt instanceof RequestCancelEvent) { + this.postEvent(this.requestCancelError(request)); + + // Notify specifically about the `subscribe` request cancellation. + if (!this._invalidated && request instanceof SubscribeRequest) + this.dispatchEvent(new PubNubClientCancelSubscribeEvent(request.client, request)); + } + }; + + request.addEventListener(PubNubSharedWorkerRequestEvents.Success, callback, { signal: ac.signal, once: true }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Error, callback, { signal: ac.signal, once: true }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Canceled, callback, { signal: ac.signal, once: true }); + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Requests ----------------------- + // -------------------------------------------------------- + // region Requests + + /** + * Cancel any active `client`-provided requests. + * + * **Note:** Cancellation will dispatch the event handled in `listenRequestCompletion` and remove `request` from the + * PubNub client requests' list. + */ + private cancelRequests() { + Object.values(this.requests).forEach((request) => request.cancel()); + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Wrap `request` into corresponding event for dispatching. + * + * @param request - Request which should be used to identify event type and stored in it. + */ + private eventWithRequest(request: BasePubNubRequest) { + let event: CustomEvent; + + if (request instanceof SubscribeRequest) event = new PubNubClientSendSubscribeEvent(this, request); + else if (request instanceof HeartbeatRequest) event = new PubNubClientSendHeartbeatEvent(this, request); + else event = new PubNubClientSendLeaveEvent(this, request as LeaveRequest); + + return event; + } + + /** + * Create request cancellation response. + * + * @param request - Reference on client-provided request for which payload should be prepared. + * @returns Object which will be treated as cancel response on core PubNub client module side. + */ + private requestCancelError(request: BasePubNubRequest): RequestSendingError { + return { + type: 'request-process-error', + clientIdentifier: this.identifier, + identifier: request.request.identifier, + url: request.asFetchRequest.url, + error: { name: 'AbortError', type: 'ABORTED', message: 'Request aborted' }, + }; + } + // endregion +} diff --git a/src/transport/subscription-worker/components/pubnub-clients-manager.ts b/src/transport/subscription-worker/components/pubnub-clients-manager.ts new file mode 100644 index 000000000..e2695c1cb --- /dev/null +++ b/src/transport/subscription-worker/components/pubnub-clients-manager.ts @@ -0,0 +1,281 @@ +import { + PubNubClientManagerRegisterEvent, + PubNubClientManagerUnregisterEvent, +} from './custom-events/client-manager-event'; +import { PubNubClientEvent, PubNubClientUnregisterEvent } from './custom-events/client-event'; +import { RegisterEvent } from '../subscription-worker-types'; +import { PubNubClient } from './pubnub-client'; + +/** + * Registered {@link PubNubClient|PubNub} client instances manager. + * + * Manager responsible for keeping track and interaction with registered {@link PubNubClient|PubNub}. + */ +export class PubNubClientsManager extends EventTarget { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Map of started `PING` timeouts per subscription key. + */ + private timeouts: { + [subKey: string]: { timeout?: ReturnType; interval: number; unsubscribeOffline: boolean }; + } = {}; + + /** + * Map of previously created {@link PubNubClient|PubNub} clients. + */ + private clients: Record = {}; + + /** + * Map of previously created {@link PubNubClient|PubNub} clients to the corresponding subscription key. + */ + private clientBySubscribeKey: Record = {}; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + + /** + * Create {@link PubNubClient|PubNub} clients manager. + * + * @param sharedWorkerIdentifier - Unique `Subscription` worker identifier that will work with clients. + */ + constructor(private readonly sharedWorkerIdentifier: string) { + super(); + } + // endregion + + // -------------------------------------------------------- + // ----------------- Client registration ------------------ + // -------------------------------------------------------- + // region Client registration + + /** + * Create {@link PubNubClient|PubNub} client. + * + * Function called in response to the `client-register` from the core {@link PubNubClient|PubNub} client module. + * + * @param event - Registration event with base {@link PubNubClient|PubNub} client information. + * @param port - Message port for two-way communication with core {@link PubNubClient|PubNub} client module. + * @returns New {@link PubNubClient|PubNub} client or existing one from the cache. + */ + createClient(event: RegisterEvent, port: MessagePort) { + if (this.clients[event.clientIdentifier]) return this.clients[event.clientIdentifier]; + + const client = new PubNubClient( + event.clientIdentifier, + event.subscriptionKey, + event.userId, + port, + event.workerLogLevel, + event.heartbeatInterval, + ); + + this.registerClient(client); + + // Start offline PubNub clients checks (ping-pong). + if (event.workerOfflineClientsCheckInterval) { + this.startClientTimeoutCheck( + event.subscriptionKey, + event.workerOfflineClientsCheckInterval, + event.workerUnsubscribeOfflineClients ?? false, + ); + } + + return client; + } + + /** + * Store {@link PubNubClient|PubNub} client in manager's internal state. + * + * @param client - Freshly created {@link PubNubClient|PubNub} client which should be registered. + */ + private registerClient(client: PubNubClient) { + this.clients[client.identifier] = { client, abortController: new AbortController() }; + + // Associate client with subscription key. + if (!this.clientBySubscribeKey[client.subKey]) this.clientBySubscribeKey[client.subKey] = [client]; + else this.clientBySubscribeKey[client.subKey].push(client); + + this.forEachClient(client.subKey, (subKeyClient) => + subKeyClient.logger.debug( + `'${client.identifier}' client registered with '${this.sharedWorkerIdentifier}' shared worker (${ + this.clientBySubscribeKey[client.subKey].length + } active clients).`, + ), + ); + + this.subscribeOnClientEvents(client); + this.dispatchEvent(new PubNubClientManagerRegisterEvent(client)); + } + + /** + * Remove {@link PubNubClient|PubNub} client from manager's internal state. + * + * @param client - Previously created {@link PubNubClient|PubNub} client which should be removed. + * @param [withLeave=false] - Whether `leave` request should be sent or not. + * @param [onClientInvalidation=false] - Whether client removal caused by its invalidation (event from the + * {@link PubNubClient|PubNub} client) or as result of timeout check. + */ + private unregisterClient(client: PubNubClient, withLeave = false, onClientInvalidation = false) { + if (!this.clients[client.identifier]) return; + + // Make sure to detach all listeners for this `client`. + if (this.clients[client.identifier].abortController) this.clients[client.identifier].abortController.abort(); + delete this.clients[client.identifier]; + + const clientsBySubscribeKey = this.clientBySubscribeKey[client.subKey]; + if (clientsBySubscribeKey) { + const clientIdx = clientsBySubscribeKey.indexOf(client); + clientsBySubscribeKey.splice(clientIdx, 1); + + if (clientsBySubscribeKey.length === 0) { + delete this.clientBySubscribeKey[client.subKey]; + this.stopClientTimeoutCheck(client); + } + } + + this.forEachClient(client.subKey, (subKeyClient) => + subKeyClient.logger.debug( + `'${this.sharedWorkerIdentifier}' shared worker unregistered '${client.identifier}' client (${ + this.clientBySubscribeKey[client.subKey].length + } active clients).`, + ), + ); + + if (!onClientInvalidation) client.invalidate(); + + this.dispatchEvent(new PubNubClientManagerUnregisterEvent(client, withLeave)); + } + // endregion + + // -------------------------------------------------------- + // ----------------- Availability check ------------------- + // -------------------------------------------------------- + // region Availability check + + /** + * Start timer for _timeout_ {@link PubNubClient|PubNub} client checks. + * + * @param subKey - Subscription key to get list of {@link PubNubClient|PubNub} clients that should be checked. + * @param interval - Interval at which _timeout_ check should be done. + * @param unsubscribeOffline - Whether _timeout_ (or _offline_) {@link PubNubClient|PubNub} clients should send + * `leave` request before invalidation or not. + */ + private startClientTimeoutCheck(subKey: string, interval: number, unsubscribeOffline: boolean) { + if (this.timeouts[subKey]) return; + + this.forEachClient(subKey, (client) => + client.logger.debug(`Setup PubNub client ping for every ${interval} seconds.`), + ); + + this.timeouts[subKey] = { + interval, + unsubscribeOffline, + timeout: setTimeout(() => this.handleTimeoutCheck(subKey), interval * 500 - 1), + }; + } + + /** + * Stop _timeout_ (or _offline_) {@link PubNubClient|PubNub} clients pinging. + * + * **Note:** This method is used only when all clients for a specific subscription key have been unregistered. + * + * @param client - {@link PubNubClient|PubNub} client with which the last client related by subscription key has been + * removed. + */ + private stopClientTimeoutCheck(client: PubNubClient) { + if (!this.timeouts[client.subKey]) return; + + if (this.timeouts[client.subKey].timeout) clearTimeout(this.timeouts[client.subKey].timeout); + delete this.timeouts[client.subKey]; + } + + /** + * Handle periodic {@link PubNubClient|PubNub} client timeout checks. + * + * @param subKey - Subscription key to get list of {@link PubNubClient|PubNub} clients that should be checked. + */ + private handleTimeoutCheck(subKey: string) { + if (!this.timeouts[subKey]) return; + + const interval = this.timeouts[subKey].interval; + [...this.clientBySubscribeKey[subKey]].forEach((client) => { + // Handle potential SharedWorker timers throttling and early eviction of the PubNub core client. + // If timer fired later than specified interval - it has been throttled and shouldn't unregister client. + if (client.lastPingRequest && Date.now() / 1000 - client.lastPingRequest - 0.2 > interval * 0.5) { + client.logger.warn('PubNub clients timeout timer fired after throttling past due time.'); + client.lastPingRequest = undefined; + } + + if ( + client.lastPingRequest && + (!client.lastPongEvent || Math.abs(client.lastPongEvent - client.lastPingRequest) > interval) + ) { + this.unregisterClient(client, this.timeouts[subKey].unsubscribeOffline); + + // Notify other clients with same subscription key that one of them became inactive. + this.forEachClient(subKey, (subKeyClient) => { + if (subKeyClient.identifier !== client.identifier) + subKeyClient.logger.debug(`'${client.identifier}' client is inactive. Invalidating...`); + }); + } + + if (this.clients[client.identifier]) { + client.lastPingRequest = Date.now() / 1000; + client.postEvent({ type: 'shared-worker-ping' }); + } + }); + + // Restart PubNub clients timeout check timer. + if (this.timeouts[subKey]) + this.timeouts[subKey].timeout = setTimeout(() => this.handleTimeoutCheck(subKey), interval * 500); + } + // endregion + + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + + /** + * Listen for {@link PubNubClient|PubNub} client events that affect aggregated subscribe/heartbeat requests. + * + * @param client - {@link PubNubClient|PubNub} client for which event should be listened. + */ + private subscribeOnClientEvents(client: PubNubClient) { + client.addEventListener( + PubNubClientEvent.Unregister, + () => + this.unregisterClient( + client, + this.timeouts[client.subKey] ? this.timeouts[client.subKey].unsubscribeOffline : false, + true, + ), + { signal: this.clients[client.identifier].abortController.signal, once: true }, + ); + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Call callback function for all {@link PubNubClient|PubNub} clients that have similar `subscribeKey`. + * + * @param subKey - Subscription key for which list of clients should be retrieved. + * @param callback - Function that will be called for each client list entry. + */ + private forEachClient(subKey: string, callback: (client: PubNubClient) => void) { + if (!this.clientBySubscribeKey[subKey]) return; + this.clientBySubscribeKey[subKey].forEach(callback); + } + // endregion +} diff --git a/src/transport/subscription-worker/components/request.ts b/src/transport/subscription-worker/components/request.ts new file mode 100644 index 000000000..cbce4f544 --- /dev/null +++ b/src/transport/subscription-worker/components/request.ts @@ -0,0 +1,780 @@ +import { + RequestErrorEvent, + RequestStartEvent, + RequestCancelEvent, + RequestSuccessEvent, + PubNubSharedWorkerRequestEvents, +} from './custom-events/request-processing-event'; +import { RequestSendingError, RequestSendingResult, RequestSendingSuccess } from '../subscription-worker-types'; +import { TransportRequest } from '../../../core/types/transport-request'; +import { Query } from '../../../core/types/api'; +import { PubNubClient } from './pubnub-client'; +import { AccessToken } from './access-token'; + +/** + * Base shared worker request implementation. + * + * In the `SharedWorker` context, this base class is used both for `client`-provided (they won't be used for actual + * request) and those that are created by `SharedWorker` code (`service` request, which will be used in actual + * requests). + * + * **Note:** The term `service` request in inline documentation will mean request created by `SharedWorker` and used to + * call PubNub REST API. + */ +export class BasePubNubRequest extends EventTarget { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Starter request processing timeout timer. + */ + private _fetchTimeoutTimer: ReturnType | undefined; + + /** + * Map of attached to the service request `client`-provided requests by their request identifiers. + * + * **Context:** `service`-provided requests only. + */ + private dependents: Record = {}; + + /** + * Controller, which is used to cancel ongoing `service`-provided request by signaling {@link fetch}. + */ + private _fetchAbortController?: AbortController; + + /** + * Service request (aggregated/modified) which will actually be used to call the REST API endpoint. + * + * This is used only by `client`-provided requests to be notified on service request (aggregated/modified) processing + * stages. + * + * **Context:** `client`-provided requests only. + */ + private _serviceRequest?: BasePubNubRequest; + + /** + * Controller, which is used to clean up any event listeners added by `client`-provided request on `service`-provided + * request. + * + * **Context:** `client`-provided requests only. + */ + private abortController?: AbortController; + + /** + * Whether the request already received a service response or an error. + * + * **Important:** Any interaction with completed requests except requesting properties is prohibited. + */ + private _completed: boolean = false; + + /** + * Whether request has been cancelled or not. + * + * **Important:** Any interaction with canceled requests except requesting properties is prohibited. + */ + private _canceled: boolean = false; + + /** + * Access token with permissions to access provided `channels`and `channelGroups` on behalf of `userId`. + */ + private _accessToken?: AccessToken; + + /** + * Reference to {@link PubNubClient|PubNub} client instance which created this request. + * + * **Context:** `client`-provided requests only. + */ + private _client?: PubNubClient; + + /** + * Unique user identifier from the name of which request will be made. + */ + private _userId: string; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + + /** + * Create request object. + * + * @param request - Transport request. + * @param subscribeKey - Subscribe REST API access key. + * @param userId - Unique user identifier from the name of which request will be made. + * @param channels - List of channels used in request. + * @param channelGroups - List of channel groups used in request. + * @param [accessToken] - Access token with permissions to access provided `channels` and `channelGroups` on behalf of + * `userId`. + */ + constructor( + readonly request: TransportRequest, + readonly subscribeKey: string, + userId: string, + readonly channels: string[], + readonly channelGroups: string[], + accessToken?: AccessToken, + ) { + super(); + + this._accessToken = accessToken; + this._userId = userId; + } + // endregion + + // -------------------------------------------------------- + // ---------------------- Properties ---------------------- + // -------------------------------------------------------- + // region Properties + + /** + * Get the request's unique identifier. + * + * @returns Request's unique identifier. + */ + get identifier() { + return this.request.identifier; + } + + /** + * Retrieve the origin that is used to access PubNub REST API. + * + * @returns Origin, which is used to access PubNub REST API. + */ + get origin() { + return this.request.origin; + } + + /** + * Retrieve the unique user identifier from the name of which request will be made. + * + * @returns Unique user identifier from the name of which request will be made. + */ + get userId() { + return this._userId; + } + + /** + * Update the unique user identifier from the name of which request will be made. + * + * @param value - New unique user identifier. + */ + set userId(value: string) { + this._userId = value; + + // Patch underlying transport request query parameters to use new value. + this.request.queryParameters!.uuid = value; + } + + /** + * Retrieve access token with permissions to access provided `channels` and `channelGroups`. + * + * @returns Access token with permissions for {@link userId} or `undefined` if not set. + */ + get accessToken() { + return this._accessToken; + } + + /** + * Update the access token which should be used to access provided `channels` and `channelGroups` by the user with + * {@link userId}. + * + * @param [value] - Access token with permissions for {@link userId}. + */ + set accessToken(value: AccessToken | undefined) { + this._accessToken = value; + + // Patch underlying transport request query parameters to use new value. + if (value) this.request.queryParameters!.auth = value.toString(); + else delete this.request.queryParameters!.auth; + } + + /** + * Retrieve {@link PubNubClient|PubNub} client associates with request. + * + * **Context:** `client`-provided requests only. + * + * @returns Reference to the {@link PubNubClient|PubNub} client that is sending the request. + */ + get client() { + return this._client!; + } + + /** + * Associate request with PubNub client. + * + * **Context:** `client`-provided requests only. + * + * @param value - {@link PubNubClient|PubNub} client that created request in `SharedWorker` context. + */ + set client(value: PubNubClient) { + this._client = value; + } + + /** + * Retrieve whether the request already received a service response or an error. + * + * @returns `true` if request already completed processing (not with {@link cancel}). + */ + get completed() { + return this._completed; + } + + /** + * Retrieve whether the request can be cancelled or not. + * + * @returns `true` if there is a possibility and meaning to be able to cancel the request. + */ + get cancellable() { + return this.request.cancellable; + } + + /** + * Retrieve whether the request has been canceled prior to completion or not. + * + * @returns `true` if the request didn't complete processing. + */ + get canceled() { + return this._canceled; + } + + /** + * Update controller, which is used to cancel ongoing `service`-provided requests by signaling {@link fetch}. + * + * **Context:** `service`-provided requests only. + * + * @param value - Controller that has been used to signal {@link fetch} for request cancellation. + */ + set fetchAbortController(value: AbortController) { + // There is no point in completed request `fetch` abort controller set. + if (this.completed || this.canceled) return; + + // Fetch abort controller can't be set for `client`-provided requests. + if (!this.isServiceRequest) { + console.error('Unexpected attempt to set fetch abort controller on client-provided request.'); + return; + } + + if (this._fetchAbortController) { + console.error('Only one abort controller can be set for service-provided requests.'); + return; + } + + this._fetchAbortController = value; + } + + /** + * Retrieve `service`-provided fetch request abort controller. + * + * **Context:** `service`-provided requests only. + * + * @returns `service`-provided fetch request abort controller. + */ + get fetchAbortController() { + return this._fetchAbortController!; + } + + /** + * Represent transport request as {@link fetch} {@link Request}. + * + * @returns Ready-to-use {@link Request} instance. + */ + get asFetchRequest(): Request { + const queryParameters = this.request.queryParameters; + const headers: Record = {}; + let query = ''; + + if (this.request.headers) for (const [key, value] of Object.entries(this.request.headers)) headers[key] = value; + + if (queryParameters && Object.keys(queryParameters).length !== 0) + query = `?${this.queryStringFromObject(queryParameters)}`; + + return new Request(`${this.origin}${this.request.path}${query}`, { + method: this.request.method, + headers: Object.keys(headers).length ? headers : undefined, + redirect: 'follow', + }); + } + + /** + * Retrieve the service (aggregated/modified) request, which will actually be used to call the REST API endpoint. + * + * **Context:** `client`-provided requests only. + * + * @returns Service (aggregated/modified) request, which will actually be used to call the REST API endpoint. + */ + get serviceRequest() { + return this._serviceRequest; + } + + /** + * Link request processing results to the service (aggregated/modified) request. + * + * **Context:** `client`-provided requests only. + * + * @param value - Service (aggregated/modified) request for which process progress should be observed. + */ + set serviceRequest(value: BasePubNubRequest | undefined) { + // This function shouldn't be called even unintentionally, on the `service`-provided requests. + if (this.isServiceRequest) { + console.error('Unexpected attempt to set service-provided request on service-provided request.'); + return; + } + + const previousServiceRequest = this.serviceRequest; + this._serviceRequest = value; + + // Detach from the previous service request if it has been changed (to a new one or unset). + if (previousServiceRequest && (!value || previousServiceRequest.identifier !== value.identifier)) + previousServiceRequest.detachRequest(this); + + // There is no need to set attach to service request if either of them is already completed, or canceled. + if (this.completed || this.canceled || (value && (value.completed || value.canceled))) { + this._serviceRequest = undefined; + return; + } + if (previousServiceRequest && value && previousServiceRequest.identifier === value.identifier) return; + + // Attach the request to the service request processing results. + if (value) value.attachRequest(this); + } + + /** + * Retrieve whether the receiver is a `service`-provided request or not. + * + * @returns `true` if the request has been created by the `SharedWorker`. + */ + get isServiceRequest() { + return !this.client; + } + // endregion + + // -------------------------------------------------------- + // ---------------------- Dependency ---------------------- + // -------------------------------------------------------- + // region Dependency + + /** + * Retrieve a list of `client`-provided requests that have been attached to the `service`-provided request. + * + * **Context:** `service`-provided requests only. + * + * @returns List of attached `client`-provided requests. + */ + dependentRequests(): T[] { + // Return an empty list for `client`-provided requests. + if (!this.isServiceRequest) return []; + + return Object.values(this.dependents) as T[]; + } + + /** + * Attach the `client`-provided request to the receiver (`service`-provided request) to receive a response from the + * PubNub REST API. + * + * **Context:** `service`-provided requests only. + * + * @param request - `client`-provided request that should be attached to the receiver (`service`-provided request). + */ + private attachRequest(request: BasePubNubRequest) { + // Request attachments works only on service requests. + if (!this.isServiceRequest || this.dependents[request.identifier]) { + if (!this.isServiceRequest) console.error('Unexpected attempt to attach requests using client-provided request.'); + + return; + } + + this.dependents[request.identifier] = request; + this.addEventListenersForRequest(request); + } + + /** + * Detach the `client`-provided request from the receiver (`service`-provided request) to ignore any response from the + * PubNub REST API. + * + * **Context:** `service`-provided requests only. + * + * @param request - `client`-provided request that should be attached to the receiver (`service`-provided request). + */ + private detachRequest(request: BasePubNubRequest) { + // Request detachments works only on service requests. + if (!this.isServiceRequest || !this.dependents[request.identifier]) { + if (!this.isServiceRequest) console.error('Unexpected attempt to detach requests using client-provided request.'); + return; + } + + delete this.dependents[request.identifier]; + request.removeEventListenersFromRequest(); + + // Because `service`-provided requests are created in response to the `client`-provided one we need to cancel the + // receiver if there are no more attached `client`-provided requests. + // This ensures that there will be no abandoned/dangling `service`-provided request in `SharedWorker` structures. + if (Object.keys(this.dependents).length === 0) this.cancel('Cancel request'); + } + // endregion + + // -------------------------------------------------------- + // ------------------ Request processing ------------------ + // -------------------------------------------------------- + // region Request processing + + /** + * Notify listeners that ongoing request processing has been cancelled. + * + * **Note:** The current implementation doesn't let {@link PubNubClient|PubNub} directly call + * {@link cancel}, and it can be called from `SharedWorker` code logic. + * + * **Important:** Previously attached `client`-provided requests should be re-attached to another `service`-provided + * request or properly cancelled with {@link PubNubClient|PubNub} notification of the core PubNub client module. + * + * @param [reason] - Reason because of which the request has been cancelled. The request manager uses this to specify + * whether the `service`-provided request has been cancelled on-demand or because of timeout. + * @param [notifyDependent] - Whether dependent requests should receive cancellation error or not. + * @returns List of detached `client`-provided requests. + */ + cancel(reason?: string, notifyDependent: boolean = false) { + // There is no point in completed request cancellation. + if (this.completed || this.canceled) { + return []; + } + + const dependentRequests = this.dependentRequests(); + if (this.isServiceRequest) { + // Detach request if not interested in receiving request cancellation error (because of timeout). + // When switching between aggregated `service`-provided requests there is no need in handling cancellation of + // outdated request. + if (!notifyDependent) dependentRequests.forEach((request) => (request.serviceRequest = undefined)); + + if (this._fetchAbortController) { + this._fetchAbortController.abort(reason); + this._fetchAbortController = undefined; + } + } else this.serviceRequest = undefined; + + this._canceled = true; + this.stopRequestTimeoutTimer(); + this.dispatchEvent(new RequestCancelEvent(this)); + + return dependentRequests; + } + + /** + * Create and return running request processing timeout timer. + * + * @returns Promise with timout timer resolution. + */ + requestTimeoutTimer() { + return new Promise((_, reject) => { + this._fetchTimeoutTimer = setTimeout(() => { + reject(new Error('Request timeout')); + this.cancel('Cancel because of timeout', true); + }, this.request.timeout * 1000); + }); + } + + /** + * Stop request processing timeout timer without error. + */ + stopRequestTimeoutTimer() { + if (!this._fetchTimeoutTimer) return; + + clearTimeout(this._fetchTimeoutTimer); + this._fetchTimeoutTimer = undefined; + } + + /** + * Handle request processing started by the request manager (actual sending). + */ + handleProcessingStarted() { + // Log out request processing start (will be made only for client-provided request). + this.logRequestStart(this); + + this.dispatchEvent(new RequestStartEvent(this)); + } + + /** + * Handle request processing successfully completed by request manager (actual sending). + * + * @param fetchRequest - Reference to the actual request that has been used with {@link fetch}. + * @param response - PubNub service response which is ready to be sent to the core PubNub client module. + */ + handleProcessingSuccess(fetchRequest: Request, response: RequestSendingSuccess) { + this.addRequestInformationForResult(this, fetchRequest, response); + this.logRequestSuccess(this, response); + this._completed = true; + + this.stopRequestTimeoutTimer(); + this.dispatchEvent(new RequestSuccessEvent(this, fetchRequest, response)); + } + + /** + * Handle request processing failed by request manager (actual sending). + * + * @param fetchRequest - Reference to the actual request that has been used with {@link fetch}. + * @param error - Request processing error description. + */ + handleProcessingError(fetchRequest: Request, error: RequestSendingError) { + this.addRequestInformationForResult(this, fetchRequest, error); + this.logRequestError(this, error); + this._completed = true; + + this.stopRequestTimeoutTimer(); + this.dispatchEvent(new RequestErrorEvent(this, fetchRequest, error)); + } + // endregion + + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + + /** + * Add `service`-provided request processing progress listeners for `client`-provided requests. + * + * **Context:** `service`-provided requests only. + * + * @param request - `client`-provided request that would like to observe `service`-provided request progress. + */ + addEventListenersForRequest(request: BasePubNubRequest) { + if (!this.isServiceRequest) { + console.error('Unexpected attempt to add listeners using a client-provided request.'); + return; + } + + request.abortController = new AbortController(); + + this.addEventListener( + PubNubSharedWorkerRequestEvents.Started, + (event) => { + if (!(event instanceof RequestStartEvent)) return; + + request.logRequestStart(event.request); + request.dispatchEvent(event.clone(request)); + }, + { signal: request.abortController.signal, once: true }, + ); + this.addEventListener( + PubNubSharedWorkerRequestEvents.Success, + (event) => { + if (!(event instanceof RequestSuccessEvent)) return; + + request.removeEventListenersFromRequest(); + request.addRequestInformationForResult(event.request, event.fetchRequest, event.response); + request.logRequestSuccess(event.request, event.response); + request._completed = true; + request.dispatchEvent(event.clone(request)); + }, + { signal: request.abortController.signal, once: true }, + ); + + this.addEventListener( + PubNubSharedWorkerRequestEvents.Error, + (event) => { + if (!(event instanceof RequestErrorEvent)) return; + + request.removeEventListenersFromRequest(); + request.addRequestInformationForResult(event.request, event.fetchRequest, event.error); + request.logRequestError(event.request, event.error); + request._completed = true; + request.dispatchEvent(event.clone(request)); + }, + { signal: request.abortController.signal, once: true }, + ); + } + + /** + * Remove listeners added to the `service` request. + * + * **Context:** `client`-provided requests only. + */ + removeEventListenersFromRequest() { + // Only client-provided requests add listeners. + if (this.isServiceRequest || !this.abortController) { + if (this.isServiceRequest) + console.error('Unexpected attempt to remove listeners using a client-provided request.'); + return; + } + + this.abortController.abort(); + this.abortController = undefined; + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Check whether the request contains specified channels in the URI path and channel groups in the request query or + * not. + * + * @param channels - List of channels for which any entry should be checked in the request. + * @param channelGroups - List of channel groups for which any entry should be checked in the request. + * @returns `true` if receiver has at least one entry from provided `channels` or `channelGroups` in own URI. + */ + hasAnyChannelsOrGroups(channels: string[], channelGroups: string[]) { + return ( + this.channels.some((channel) => channels.includes(channel)) || + this.channelGroups.some((channelGroup) => channelGroups.includes(channelGroup)) + ); + } + + /** + * Append request-specific information to the processing result. + * + * @param fetchRequest - Reference to the actual request that has been used with {@link fetch}. + * @param request - Reference to the client- or service-provided request with information for response. + * @param result - Request processing result that should be modified. + */ + private addRequestInformationForResult( + request: BasePubNubRequest, + fetchRequest: Request, + result: RequestSendingResult, + ) { + if (this.isServiceRequest) return; + + result.clientIdentifier = this.client.identifier; + result.identifier = this.identifier; + result.url = fetchRequest.url; + } + + /** + * Log to the core PubNub client module information about request processing start. + * + * @param request - Reference to the client- or service-provided request information about which should be logged. + */ + private logRequestStart(request: BasePubNubRequest) { + if (this.isServiceRequest) return; + + this.client.logger.debug(() => ({ messageType: 'network-request', message: request.request })); + } + + /** + * Log to the core PubNub client module information about request processing successful completion. + * + * @param request - Reference to the client- or service-provided request information about which should be logged. + * @param response - Reference to the PubNub service response. + */ + private logRequestSuccess(request: BasePubNubRequest, response: RequestSendingSuccess) { + if (this.isServiceRequest) return; + + this.client.logger.debug(() => { + const { status, headers, body } = response.response; + const fetchRequest = request.asFetchRequest; + const _headers: Record = {}; + + // Copy Headers object content into plain Record. + Object.entries(headers).forEach(([key, value]) => (_headers[key] = value)); + + return { messageType: 'network-response', message: { status, url: fetchRequest.url, headers, body } }; + }); + } + + /** + * Log to the core PubNub client module information about request processing error. + * + * @param request - Reference to the client- or service-provided request information about which should be logged. + * @param error - Request processing error information. + */ + private logRequestError(request: BasePubNubRequest, error: RequestSendingError) { + if (this.isServiceRequest) return; + + if ((error.error ? error.error.message : 'Unknown').toLowerCase().includes('timeout')) { + this.client.logger.debug(() => ({ + messageType: 'network-request', + message: request.request, + details: 'Timeout', + canceled: true, + })); + } else { + this.client.logger.warn(() => { + const { details, canceled } = this.errorDetailsFromSendingError(error); + let logDetails = details; + + if (canceled) logDetails = 'Aborted'; + else if (details.toLowerCase().includes('network')) logDetails = 'Network error'; + + return { + messageType: 'network-request', + message: request.request, + details: logDetails, + canceled: canceled, + failed: !canceled, + }; + }); + } + } + + /** + * Retrieve error details from the error response object. + * + * @param error - Request fetch error object. + * @reruns Object with error details and whether it has been canceled or not. + */ + private errorDetailsFromSendingError(error: RequestSendingError): { details: string; canceled: boolean } { + const canceled = error.error ? error.error.type === 'TIMEOUT' || error.error.type === 'ABORTED' : false; + let details = error.error ? error.error.message : 'Unknown'; + if (error.response) { + const contentType = error.response.headers['content-type']; + + if ( + error.response.body && + contentType && + (contentType.indexOf('javascript') !== -1 || contentType.indexOf('json') !== -1) + ) { + try { + const serviceResponse = JSON.parse(new TextDecoder().decode(error.response.body)); + if ('message' in serviceResponse) details = serviceResponse.message; + else if ('error' in serviceResponse) { + if (typeof serviceResponse.error === 'string') details = serviceResponse.error; + else if (typeof serviceResponse.error === 'object' && 'message' in serviceResponse.error) + details = serviceResponse.error.message; + } + } catch (_) {} + } + + if (details === 'Unknown') { + if (error.response.status >= 500) details = 'Internal Server Error'; + else if (error.response.status == 400) details = 'Bad request'; + else if (error.response.status == 403) details = 'Access denied'; + else details = `${error.response.status}`; + } + } + + return { details, canceled }; + } + + /** + * Stringify request query key/value pairs. + * + * @param query - Request query object. + * @returns Stringified query object. + */ + private queryStringFromObject = (query: Query) => { + return Object.keys(query) + .map((key) => { + const queryValue = query[key]; + if (!Array.isArray(queryValue)) return `${key}=${this.encodeString(queryValue)}`; + + return queryValue.map((value) => `${key}=${this.encodeString(value)}`).join('&'); + }) + .join('&'); + }; + + /** + * Percent-encode input string. + * + * **Note:** Encode content in accordance of the `PubNub` service requirements. + * + * @param input - Source string or number for encoding. + * @returns Percent-encoded string. + */ + protected encodeString(input: string | number) { + return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); + } + // endregion +} diff --git a/src/transport/subscription-worker/components/requests-manager.ts b/src/transport/subscription-worker/components/requests-manager.ts new file mode 100644 index 000000000..95aeedf0a --- /dev/null +++ b/src/transport/subscription-worker/components/requests-manager.ts @@ -0,0 +1,154 @@ +import { RequestSendingError, RequestSendingSuccess } from '../subscription-worker-types'; +import { BasePubNubRequest } from './request'; + +/** + * SharedWorker's requests manager. + * + * Manager responsible for storing client-provided request for the time while enqueue / dequeue service request which + * is actually sent to the PubNub service. + */ +export class RequestsManager extends EventTarget { + // -------------------------------------------------------- + // ------------------ Request processing ------------------ + // -------------------------------------------------------- + // region Request processing + + /** + * Begin service request processing. + * + * @param request - Reference to the service request which should be sent. + * @param success - Request success completion handler. + * @param failure - Request failure handler. + * @param responsePreprocess - Raw response pre-processing function which is used before calling handling callbacks. + */ + sendRequest( + request: BasePubNubRequest, + success: (fetchRequest: Request, response: RequestSendingSuccess) => void, + failure: (fetchRequest: Request, errorResponse: RequestSendingError) => void, + responsePreprocess?: (response: [Response, ArrayBuffer]) => [Response, ArrayBuffer], + ) { + request.handleProcessingStarted(); + + if (request.cancellable) request.fetchAbortController = new AbortController(); + const fetchRequest = request.asFetchRequest; + + (async () => { + Promise.race([ + fetch(fetchRequest, { + ...(request.fetchAbortController ? { signal: request.fetchAbortController.signal } : {}), + keepalive: true, + }), + request.requestTimeoutTimer(), + ]) + .then((response): Promise<[Response, ArrayBuffer]> | [Response, ArrayBuffer] => + response.arrayBuffer().then((buffer) => [response, buffer]), + ) + .then((response) => (responsePreprocess ? responsePreprocess(response) : response)) + .then((response) => { + if (response[0].status >= 400) failure(fetchRequest, this.requestProcessingError(undefined, response)); + else success(fetchRequest, this.requestProcessingSuccess(response)); + }) + .catch((error) => { + let fetchError = error; + + if (typeof error === 'string') { + const errorMessage = error.toLowerCase(); + fetchError = new Error(error); + + if (!errorMessage.includes('timeout') && errorMessage.includes('cancel')) fetchError.name = 'AbortError'; + } + + request.stopRequestTimeoutTimer(); + failure(fetchRequest, this.requestProcessingError(fetchError)); + }); + })(); + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Create processing success event from service response. + * + * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each + * specific PubNub client state. + * + * @param res - Service response for used REST API endpoint along with response body. + * + * @returns Request processing success event object. + */ + private requestProcessingSuccess(res: [Response, ArrayBuffer]): RequestSendingSuccess { + const [response, body] = res; + const responseBody = body.byteLength > 0 ? body : undefined; + const contentLength = parseInt(response.headers.get('Content-Length') ?? '0', 10); + const contentType = response.headers.get('Content-Type')!; + const headers: Record = {}; + + // Copy Headers object content into plain Record. + response.headers.forEach((value, key) => (headers[key.toLowerCase()] = value.toLowerCase())); + + return { + type: 'request-process-success', + clientIdentifier: '', + identifier: '', + url: '', + response: { contentLength, contentType, headers, status: response.status, body: responseBody }, + }; + } + + /** + * Create processing error event from service response. + * + * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each + * specific PubNub client state. + * + * @param [error] - Client-side request processing error (for example network issues). + * @param [response] - Service error response (for example permissions error or malformed + * payload) along with service body. + * @returns Request processing error event object. + */ + private requestProcessingError(error?: unknown, response?: [Response, ArrayBuffer]): RequestSendingError { + // Use service response as error information source. + if (response) return { ...this.requestProcessingSuccess(response), type: 'request-process-error' }; + + let type: NonNullable['type'] = 'NETWORK_ISSUE'; + let message = 'Unknown error'; + let name = 'Error'; + + if (error && error instanceof Error) { + message = error.message; + name = error.name; + } + + const errorMessage = message.toLowerCase(); + if (errorMessage.includes('timeout')) type = 'TIMEOUT'; + else if (name === 'AbortError' || errorMessage.includes('aborted') || errorMessage.includes('cancel')) { + message = 'Request aborted'; + type = 'ABORTED'; + } + + return { + type: 'request-process-error', + clientIdentifier: '', + identifier: '', + url: '', + error: { name, type, message }, + }; + } + + /** + * Percent-encode input string. + * + * **Note:** Encode content in accordance of the `PubNub` service requirements. + * + * @param input - Source string or number for encoding. + * @returns Percent-encoded string. + */ + protected encodeString(input: string | number) { + return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); + } + // endregion +} diff --git a/src/transport/subscription-worker/components/subscribe-request.ts b/src/transport/subscription-worker/components/subscribe-request.ts new file mode 100644 index 000000000..c91ca6661 --- /dev/null +++ b/src/transport/subscription-worker/components/subscribe-request.ts @@ -0,0 +1,424 @@ +import { TransportRequest } from '../../../core/types/transport-request'; +import uuidGenerator from '../../../core/components/uuid'; +import { Payload } from '../../../core/types/api'; +import { BasePubNubRequest } from './request'; +import { AccessToken } from './access-token'; + +export class SubscribeRequest extends BasePubNubRequest { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Global subscription request creation date tracking. + * + * Tracking is required to handle about rapid requests receive and need to know which of them were earlier. + */ + private static lastCreationDate = 0; + + /** + * Presence state associated with `userID` on {@link SubscribeRequest.channels|channels} and + * {@link SubscribeRequest.channelGroups|channelGroups}. + */ + readonly state: Record | undefined; + + /** + * Request creation timestamp. + */ + private readonly _creationDate = Date.now(); + + /** + * Timetoken region which should be used to patch timetoken origin in initial response. + */ + public timetokenRegionOverride: string = '0'; + + /** + * Timetoken which should be used to patch timetoken in initial response. + */ + public timetokenOverride?: string; + + /** + * Subscription loop timetoken. + */ + private _timetoken: string; + + /** + * Subscription loop timetoken's region. + */ + private _region?: string; + + /** + * Whether request requires client's cached subscription state reset or not. + */ + private _requireCachedStateReset: boolean; + + /** + * Real-time events filtering expression. + */ + private readonly filterExpression?: string; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + + /** + * Create subscribe request from received _transparent_ transport request. + * + * @param request - Object with subscribe transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with read permissions on + * {@link SubscribeRequest.channels|channels} and {@link SubscribeRequest.channelGroups|channelGroups}. + * @returns Initialized and ready to use subscribe request. + */ + static fromTransportRequest(request: TransportRequest, subscriptionKey: string, accessToken?: AccessToken) { + return new SubscribeRequest(request, subscriptionKey, accessToken); + } + + /** + * Create subscribe request from previously cached data. + * + * @param request - Object with subscribe transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [cachedChannelGroups] - Previously cached list of channel groups for subscription. + * @param [cachedChannels] - Previously cached list of channels for subscription. + * @param [cachedState] - Previously cached user's presence state for channels and groups. + * @param [accessToken] - Access token with read permissions on + * {@link PubNubSharedWorkerRequest.channels|channels} and + * {@link PubNubSharedWorkerRequest.channelGroups|channelGroups}. + * @retusns Initialized and ready to use subscribe request. + */ + static fromCachedState( + request: TransportRequest, + subscriptionKey: string, + cachedChannelGroups: string[], + cachedChannels: string[], + cachedState?: Record, + accessToken?: AccessToken, + ): SubscribeRequest { + return new SubscribeRequest( + request, + subscriptionKey, + accessToken, + cachedChannelGroups, + cachedChannels, + cachedState, + ); + } + + /** + * Create aggregated subscribe request. + * + * @param requests - List of subscribe requests for same the user. + * @param [accessToken] - Access token with permissions to announce presence on + * {@link SubscribeRequest.channels|channels} and {@link SubscribeRequest.channelGroups|channelGroups}. + * @param timetokenOverride - Timetoken which should be used to patch timetoken in initial response. + * @param timetokenRegionOverride - Timetoken origin which should be used to patch timetoken origin in initial + * response. + * @returns Aggregated subscribe request which will be sent. + */ + static fromRequests( + requests: SubscribeRequest[], + accessToken?: AccessToken, + timetokenOverride?: string, + timetokenRegionOverride?: string, + ) { + const baseRequest = requests[Math.floor(Math.random() * requests.length)]; + const aggregatedRequest = { ...baseRequest.request }; + let state: Record = {}; + const channelGroups = new Set(); + const channels = new Set(); + + for (const request of requests) { + if (request.state) state = { ...state, ...request.state }; + request.channelGroups.forEach(channelGroups.add, channelGroups); + request.channels.forEach(channels.add, channels); + } + + // Update request channels list (if required). + if (channels.size || channelGroups.size) { + const pathComponents = aggregatedRequest.path.split('/'); + pathComponents[4] = channels.size ? [...channels].sort().join(',') : ','; + aggregatedRequest.path = pathComponents.join('/'); + } + + // Update request channel groups list (if required). + if (channelGroups.size) aggregatedRequest.queryParameters!['channel-group'] = [...channelGroups].sort().join(','); + + // Update request `state` (if required). + if (Object.keys(state).length) aggregatedRequest.queryParameters!.state = JSON.stringify(state); + else delete aggregatedRequest.queryParameters!.state; + + if (accessToken) aggregatedRequest.queryParameters!.auth = accessToken.toString(); + aggregatedRequest.identifier = uuidGenerator.createUUID(); + + // Create service request and link to its result other requests used in aggregation. + const request = new SubscribeRequest(aggregatedRequest, baseRequest.subscribeKey, accessToken); + for (const clientRequest of requests) clientRequest.serviceRequest = request; + + if (request.isInitialSubscribe && timetokenOverride && timetokenOverride !== '0') { + request.timetokenOverride = timetokenOverride; + if (timetokenRegionOverride) request.timetokenRegionOverride = timetokenRegionOverride; + } + + return request; + } + + /** + * Create subscribe request from received _transparent_ transport request. + * + * @param request - Object with subscribe transport request. + * @param subscriptionKey - Subscribe REST API access key. + * @param [accessToken] - Access token with read permissions on {@link SubscribeRequest.channels|channels} and + * {@link SubscribeRequest.channelGroups|channelGroups}. + * @param [cachedChannels] - Previously cached list of channels for subscription. + * @param [cachedChannelGroups] - Previously cached list of channel groups for subscription. + * @param [cachedState] - Previously cached user's presence state for channels and groups. + */ + private constructor( + request: TransportRequest, + subscriptionKey: string, + accessToken?: AccessToken, + cachedChannelGroups?: string[], + cachedChannels?: string[], + cachedState?: Record, + ) { + // Retrieve information about request's origin (who initiated it). + const requireCachedStateReset = !!request.queryParameters && 'on-demand' in request.queryParameters; + delete request.queryParameters!['on-demand']; + + super( + request, + subscriptionKey, + request.queryParameters!.uuid as string, + cachedChannels ?? SubscribeRequest.channelsFromRequest(request), + cachedChannelGroups ?? SubscribeRequest.channelGroupsFromRequest(request), + accessToken, + ); + + // Shift on millisecond creation timestamp for two sequential requests. + if (this._creationDate <= SubscribeRequest.lastCreationDate) { + SubscribeRequest.lastCreationDate++; + this._creationDate = SubscribeRequest.lastCreationDate; + } else SubscribeRequest.lastCreationDate = this._creationDate; + + this._requireCachedStateReset = requireCachedStateReset; + + if (request.queryParameters!['filter-expr']) + this.filterExpression = request.queryParameters!['filter-expr'] as string; + this._timetoken = (request.queryParameters!.tt ?? '0') as string; + if (request.queryParameters!.tr) this._region = request.queryParameters!.tr as string; + if (cachedState) this.state = cachedState; + + // Clean up `state` from objects which is not used with request (if needed). + if (this.state || !request.queryParameters!.state || (request.queryParameters!.state as string).length === 0) + return; + + const state = JSON.parse(request.queryParameters!.state as string) as Record; + for (const objectName of Object.keys(state)) + if (!this.channels.includes(objectName) && !this.channelGroups.includes(objectName)) delete state[objectName]; + + this.state = state; + } + // endregion + + // -------------------------------------------------------- + // ---------------------- Properties ---------------------- + // -------------------------------------------------------- + // region Properties + + /** + * Retrieve `subscribe` request creation timestamp. + * + * @returns `Subscribe` request creation timestamp. + */ + get creationDate() { + return this._creationDate; + } + + /** + * Represent subscribe request as identifier. + * + * Generated identifier will be identical for requests created for the same user. + */ + get asIdentifier() { + const auth = this.accessToken ? this.accessToken.asIdentifier : undefined; + const id = `${this.userId}-${this.subscribeKey}${auth ? `-${auth}` : ''}`; + return this.filterExpression ? `${id}-${this.filterExpression}` : id; + } + + /** + * Retrieve whether this is initial subscribe request or not. + * + * @returns `true` if subscribe REST API called with missing or `tt=0` query parameter. + */ + get isInitialSubscribe() { + return this._timetoken === '0'; + } + + /** + * Retrieve subscription loop timetoken. + * + * @returns Subscription loop timetoken. + */ + get timetoken() { + return this._timetoken; + } + + /** + * Update subscription loop timetoken. + * + * @param value - New timetoken that should be used in PubNub REST API calls. + */ + set timetoken(value: string) { + this._timetoken = value; + + // Update value for transport request object. + this.request.queryParameters!.tt = value; + } + + /** + * Retrieve subscription loop timetoken's region. + * + * @returns Subscription loop timetoken's region. + */ + get region() { + return this._region; + } + + /** + * Update subscription loop timetoken's region. + * + * @param value - New timetoken's region that should be used in PubNub REST API calls. + */ + set region(value: string | undefined) { + this._region = value; + + // Update value for transport request object. + if (value) this.request.queryParameters!.tr = value; + else delete this.request.queryParameters!.tr; + } + + /** + * Retrieve whether the request requires the client's cached subscription state reset or not. + * + * @returns `true` if a subscribe request has been created on user request (`subscribe()` call) or not. + */ + get requireCachedStateReset() { + return this._requireCachedStateReset; + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Check whether client's subscription state cache can be used for new request or not. + * + * @param request - Transport request from the core PubNub client module with request origin information. + * @returns `true` if request created not by user (subscription loop). + */ + static useCachedState(request: TransportRequest) { + return !!request.queryParameters && !('on-demand' in request.queryParameters); + } + + /** + * Reset the inner state of the `subscribe` request object to the one that `initial` requests. + */ + resetToInitialRequest() { + this._requireCachedStateReset = true; + this._timetoken = '0'; + this._region = undefined; + + delete this.request.queryParameters!.tt; + } + + /** + * Check whether received is a subset of another `subscribe` request. + * + * If the receiver is a subset of another means: + * - list of channels of another `subscribe` request includes all channels from the receiver, + * - list of channel groups of another `subscribe` request includes all channel groups from the receiver, + * - receiver's timetoken equal to `0` or another request `timetoken`. + * + * @param request - Request that should be checked to be a superset of received. + * @retuns `true` in case if the receiver is a subset of another `subscribe` request. + */ + isSubsetOf(request: SubscribeRequest): boolean { + if (request.channelGroups.length && !this.includesStrings(request.channelGroups, this.channelGroups)) return false; + if (request.channels.length && !this.includesStrings(request.channels, this.channels)) return false; + + return this.timetoken === '0' || this.timetoken === request.timetoken || request.timetoken === '0'; + } + + /** + * Serialize request for easier representation in logs. + * + * @returns Stringified `subscribe` request. + */ + toString() { + return `SubscribeRequest { clientIdentifier: ${ + this.client ? this.client.identifier : 'service request' + }, requestIdentifier: ${this.identifier}, serviceRequestIdentified: ${ + this.client ? (this.serviceRequest ? this.serviceRequest.identifier : "'not set'") : "'is service request" + }, channels: [${ + this.channels.length ? this.channels.map((channel) => `'${channel}'`).join(', ') : '' + }], channelGroups: [${ + this.channelGroups.length ? this.channelGroups.map((group) => `'${group}'`).join(', ') : '' + }], timetoken: ${this.timetoken}, region: ${this.region}, reset: ${ + this._requireCachedStateReset ? "'reset'" : "'do not reset'" + } }`; + } + + /** + * Serialize request to "typed" JSON string. + * + * @returns "Typed" JSON string. + */ + toJSON() { + return this.toString(); + } + + /** + * Extract list of channels for subscription from request URI path. + * + * @param request - Transport request from which should be extracted list of channels for presence announcement. + * + * @returns List of channel names (not percent-decoded) for which `subscribe` has been called. + */ + private static channelsFromRequest(request: TransportRequest): string[] { + const channels = request.path.split('/')[4]; + return channels === ',' ? [] : channels.split(',').filter((name) => name.length > 0); + } + + /** + * Extract list of channel groups for subscription from request query. + * + * @param request - Transport request from which should be extracted list of channel groups for presence announcement. + * + * @returns List of channel group names (not percent-decoded) for which `subscribe` has been called. + */ + private static channelGroupsFromRequest(request: TransportRequest): string[] { + if (!request.queryParameters || !request.queryParameters['channel-group']) return []; + const group = request.queryParameters['channel-group'] as string; + return group.length === 0 ? [] : group.split(',').filter((name) => name.length > 0); + } + + /** + * Check whether {@link main} array contains all entries from {@link sub} array. + * + * @param main - Main array with which `intersection` with {@link sub} should be checked. + * @param sub - Sub-array whose values should be checked in {@link main}. + * + * @returns `true` if all entries from {@link sub} is present in {@link main}. + */ + private includesStrings(main: string[], sub: string[]) { + const set = new Set(main); + return sub.every(set.has, set); + } + // endregion +} diff --git a/src/transport/subscription-worker/components/subscribe-requests-manager.ts b/src/transport/subscription-worker/components/subscribe-requests-manager.ts new file mode 100644 index 000000000..73873972d --- /dev/null +++ b/src/transport/subscription-worker/components/subscribe-requests-manager.ts @@ -0,0 +1,617 @@ +import { + PubNubClientEvent, + PubNubClientSendLeaveEvent, + PubNubClientAuthChangeEvent, + PubNubClientSendSubscribeEvent, + PubNubClientIdentityChangeEvent, + PubNubClientCancelSubscribeEvent, +} from './custom-events/client-event'; +import { + PubNubClientsManagerEvent, + PubNubClientManagerRegisterEvent, + PubNubClientManagerUnregisterEvent, +} from './custom-events/client-manager-event'; +import { SubscriptionStateChangeEvent, SubscriptionStateEvent } from './custom-events/subscription-state-event'; +import { SubscriptionState, SubscriptionStateChange } from './subscription-state'; +import { PubNubClientsManager } from './pubnub-clients-manager'; +import { SubscribeRequest } from './subscribe-request'; +import { RequestsManager } from './requests-manager'; +import { PubNubClient } from './pubnub-client'; +import { LeaveRequest } from './leave-request'; +import { leaveRequest } from './helpers'; + +/** + * Aggregation timer timeout. + * + * Timeout used by the timer to postpone enqueued `subscribe` requests processing and let other clients for the same + * subscribe key send next subscribe loop request (to make aggregation more efficient). + */ +const aggregationTimeout = 50; + +/** + * Sent {@link SubscribeRequest|subscribe} requests manager. + * + * Manager responsible for requests enqueue for batch processing and aggregated `service`-provided requests scheduling. + */ +export class SubscribeRequestsManager extends RequestsManager { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Service response binary data decoder. + */ + private static textDecoder = new TextDecoder(); + + /** + * Stringified to binary data encoder. + */ + private static textEncoder = new TextEncoder(); + + /** + * Map of change aggregation identifiers to the requests which should be processed at once. + * + * `requests` key contains a map of {@link PubNubClient|PubNub} client identifiers to requests created by it (usually + * there is only one at a time). + */ + private requestsChangeAggregationQueue: { + [key: string]: { timeout: ReturnType; changes: Set }; + } = {}; + + /** + * Map of client identifiers to {@link AbortController} instances which is used to detach added listeners when + * {@link PubNubClient|PubNub} client unregisters. + */ + private readonly clientAbortControllers: Record = {}; + + /** + * Map of unique user identifier (composed from multiple request object properties) to the aggregated subscription + * {@link SubscriptionState|state}. + */ + private readonly subscriptionStates: Record = {}; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructors --------------------- + // -------------------------------------------------------- + // region Constructors + + /** + * Create a {@link SubscribeRequest|subscribe} requests manager. + * + * @param clientsManager - Reference to the {@link PubNubClient|PubNub} clients manager as an events source for new + * clients for which {@link SubscribeRequest|subscribe} request sending events should be listened. + */ + constructor(private readonly clientsManager: PubNubClientsManager) { + super(); + this.addEventListenersForClientsManager(clientsManager); + } + // endregion + + // -------------------------------------------------------- + // ----------------- Changes aggregation ------------------ + // -------------------------------------------------------- + // region Changes aggregation + + /** + * Retrieve {@link SubscribeRequest|requests} changes aggregation queue for specific {@link PubNubClient|PubNub} + * client. + * + * @param client - Reference to {@link PubNubClient|PubNub} client for which {@link SubscribeRequest|subscribe} + * requests queue should be retrieved. + * @returns Tuple with aggregation key and aggregated changes of client's {@link SubscribeRequest|subscribe} requests + * that are enqueued for aggregation/removal. + */ + private requestsChangeAggregationQueueForClient( + client: PubNubClient, + ): [string | undefined, Set] { + for (const aggregationKey of Object.keys(this.requestsChangeAggregationQueue)) { + const { changes } = this.requestsChangeAggregationQueue[aggregationKey]; + if (Array.from(changes).some((change) => change.clientIdentifier === client.identifier)) + return [aggregationKey, changes]; + } + + return [undefined, new Set()]; + } + + /** + * Move {@link PubNubClient|PubNub} client to new subscription set. + * + * This function used when PubNub client changed its identity (`userId`) or auth (`access token`) and can't be + * aggregated with previous requests. + * + * **Note:** Previous `service`-provided `subscribe` request won't be canceled. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which should be moved to new state. + */ + private moveClient(client: PubNubClient) { + // Retrieve a list of client's requests that have been enqueued for further aggregation. + const [queueIdentifier, enqueuedChanges] = this.requestsChangeAggregationQueueForClient(client); + // Retrieve list of client's requests from active subscription state. + let state = this.subscriptionStateForClient(client); + const request = state?.requestForClient(client); + + // Check whether PubNub client has any activity prior removal or not. + if (!state && !enqueuedChanges.size) return; + + // Make sure that client will be removed from its previous subscription state. + if (state) state.invalidateClient(client); + + // Requests aggregation identifier. + let identifier = request?.asIdentifier; + if (!identifier && enqueuedChanges.size) { + const [change] = enqueuedChanges; + identifier = change.request.asIdentifier; + } + + if (!identifier) return; + + if (request) { + // Unset `service`-provided request because we can't receive a response with new `userId`. + request.serviceRequest = undefined; + + state!.processChanges([new SubscriptionStateChange(client.identifier, request, true, false, true)]); + + state = this.subscriptionStateForIdentifier(identifier); + // Force state refresh (because we are putting into new subscription set). + request.resetToInitialRequest(); + state!.processChanges([new SubscriptionStateChange(client.identifier, request, false, false)]); + } + + // Check whether there is enqueued request changes which should be removed from previous queue and added to the new + // one. + if (!enqueuedChanges.size || !this.requestsChangeAggregationQueue[queueIdentifier!]) return; + + // Start the changes aggregation timer if required (this also prepares the queue for `identifier`). + this.startAggregationTimer(identifier); + + // Remove from previous aggregation queue. + const oldChangesQueue = this.requestsChangeAggregationQueue[queueIdentifier!].changes; + SubscriptionStateChange.squashedChanges([...enqueuedChanges]) + .filter((change) => change.clientIdentifier !== client.identifier || change.remove) + .forEach(oldChangesQueue.delete, oldChangesQueue); + + // Add previously scheduled for aggregation requests to the new subscription set target. + const { changes } = this.requestsChangeAggregationQueue[identifier]; + SubscriptionStateChange.squashedChanges([...enqueuedChanges]) + .filter( + (change) => + change.clientIdentifier === client.identifier && + !change.request.completed && + change.request.canceled && + !change.remove, + ) + .forEach(changes.add, changes); + } + + /** + * Remove unregistered/disconnected {@link PubNubClient|PubNub} client from manager's {@link SubscriptionState|state}. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which should be removed from + * {@link SubscriptionState|state}. + * @param useChangeAggregation - Whether {@link PubNubClient|client} removal should be processed using an aggregation + * queue or change should be done on-the-fly by removing from both the aggregation queue and subscription state. + * @param sendLeave - Whether the {@link PubNubClient|client} should send a presence `leave` request for _free_ + * channels and groups or not. + * @param [invalidated=false] - Whether the {@link PubNubClient|PubNub} client and its request were removed as part of + * client invalidation (unregister) or not. + */ + private removeClient(client: PubNubClient, useChangeAggregation: boolean, sendLeave: boolean, invalidated = false) { + // Retrieve a list of client's requests that have been enqueued for further aggregation. + const [queueIdentifier, enqueuedChanges] = this.requestsChangeAggregationQueueForClient(client); + // Retrieve list of client's requests from active subscription state. + const state = this.subscriptionStateForClient(client); + const request = state?.requestForClient(client, invalidated); + + // Check whether PubNub client has any activity prior removal or not. + if (!state && !enqueuedChanges.size) return; + + const identifier = (state && state.identifier) ?? queueIdentifier!; + + // Remove the client's subscription requests from the active aggregation queue. + if (enqueuedChanges.size && this.requestsChangeAggregationQueue[identifier]) { + const { changes } = this.requestsChangeAggregationQueue[identifier]; + enqueuedChanges.forEach(changes.delete, changes); + + this.stopAggregationTimerIfEmptyQueue(identifier); + } + + if (!request) return; + + // Detach `client`-provided request to avoid unexpected response processing. + request.serviceRequest = undefined; + + if (useChangeAggregation) { + // Start the changes aggregation timer if required (this also prepares the queue for `identifier`). + this.startAggregationTimer(identifier); + + // Enqueue requests into the aggregated state change queue (delayed). + this.enqueueForAggregation(client, request, true, sendLeave, invalidated); + } else if (state) + state.processChanges([new SubscriptionStateChange(client.identifier, request, true, sendLeave, invalidated)]); + } + + /** + * Enqueue {@link SubscribeRequest|subscribe} requests for aggregation after small delay. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which created + * {@link SubscribeRequest|subscribe} request. + * @param enqueuedRequest - {@link SubscribeRequest|Subscribe} request which should be placed into the queue. + * @param removing - Whether requests enqueued for removal or not. + * @param sendLeave - Whether on remove it should leave "free" channels and groups or not. + * @param [clientInvalidate=false] - Whether the `subscription` state change was caused by the + * {@link PubNubClient|PubNub} client invalidation (unregister) or not. + */ + private enqueueForAggregation( + client: PubNubClient, + enqueuedRequest: SubscribeRequest, + removing: boolean, + sendLeave: boolean, + clientInvalidate = false, + ) { + const identifier = enqueuedRequest.asIdentifier; + // Start the changes aggregation timer if required (this also prepares the queue for `identifier`). + this.startAggregationTimer(identifier); + + // Enqueue requests into the aggregated state change queue. + const { changes } = this.requestsChangeAggregationQueue[identifier]; + changes.add(new SubscriptionStateChange(client.identifier, enqueuedRequest, removing, sendLeave, clientInvalidate)); + } + + /** + * Start requests change aggregation timer. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + private startAggregationTimer(identifier: string) { + if (this.requestsChangeAggregationQueue[identifier]) return; + + this.requestsChangeAggregationQueue[identifier] = { + timeout: setTimeout(() => this.handleDelayedAggregation(identifier), aggregationTimeout), + changes: new Set(), + }; + } + + /** + * Stop request changes aggregation timer if there is no changes left in queue. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + private stopAggregationTimerIfEmptyQueue(identifier: string) { + const queue = this.requestsChangeAggregationQueue[identifier]; + if (!queue) return; + + if (queue.changes.size === 0) { + if (queue.timeout) clearTimeout(queue.timeout); + delete this.requestsChangeAggregationQueue[identifier]; + } + } + + /** + * Handle delayed {@link SubscribeRequest|subscribe} requests aggregation. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + private handleDelayedAggregation(identifier: string) { + if (!this.requestsChangeAggregationQueue[identifier]) return; + + const state = this.subscriptionStateForIdentifier(identifier); + + // Squash self-excluding change entries. + const changes = [...this.requestsChangeAggregationQueue[identifier].changes]; + delete this.requestsChangeAggregationQueue[identifier]; + + // Apply final changes to the subscription state. + state.processChanges(changes); + } + + /** + * Retrieve existing or create new `subscription` {@link SubscriptionState|state} object for id. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + * @returns Existing or create new `subscription` {@link SubscriptionState|state} object for id. + */ + private subscriptionStateForIdentifier(identifier: string) { + let state = this.subscriptionStates[identifier]; + + if (!state) { + state = this.subscriptionStates[identifier] = new SubscriptionState(identifier); + // Make sure to receive updates from subscription state. + this.addListenerForSubscriptionStateEvents(state); + } + + return state; + } + // endregion + + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + + /** + * Listen for {@link PubNubClient|PubNub} clients {@link PubNubClientsManager|manager} events that affect aggregated + * subscribe/heartbeat requests. + * + * @param clientsManager - Clients {@link PubNubClientsManager|manager} for which change in + * {@link PubNubClient|clients} should be tracked. + */ + private addEventListenersForClientsManager(clientsManager: PubNubClientsManager) { + clientsManager.addEventListener(PubNubClientsManagerEvent.Registered, (evt) => { + const { client } = evt as PubNubClientManagerRegisterEvent; + + // Keep track of the client's listener abort controller. + const abortController = new AbortController(); + this.clientAbortControllers[client.identifier] = abortController; + + client.addEventListener( + PubNubClientEvent.IdentityChange, + (event) => { + if (!(event instanceof PubNubClientIdentityChangeEvent)) return; + // Make changes into state only if `userId` actually changed. + if ( + !!event.oldUserId !== !!event.newUserId || + (event.oldUserId && event.newUserId && event.newUserId !== event.oldUserId) + ) + this.moveClient(client); + }, + { + signal: abortController.signal, + }, + ); + client.addEventListener( + PubNubClientEvent.AuthChange, + (event) => { + if (!(event instanceof PubNubClientAuthChangeEvent)) return; + // Check whether the client should be moved to another state because of a permissions change or whether the + // same token with the same permissions should be used for the next requests. + if ( + !!event.oldAuth !== !!event.newAuth || + (event.oldAuth && event.newAuth && !event.oldAuth.equalTo(event.newAuth)) + ) + this.moveClient(client); + else if (event.oldAuth && event.newAuth && event.oldAuth.equalTo(event.newAuth)) + this.subscriptionStateForClient(client)?.updateClientAccessToken(event.newAuth); + }, + { + signal: abortController.signal, + }, + ); + client.addEventListener( + PubNubClientEvent.SendSubscribeRequest, + (event) => { + if (!(event instanceof PubNubClientSendSubscribeEvent)) return; + this.enqueueForAggregation(event.client, event.request, false, false); + }, + { signal: abortController.signal }, + ); + client.addEventListener( + PubNubClientEvent.CancelSubscribeRequest, + (event) => { + if (!(event instanceof PubNubClientCancelSubscribeEvent)) return; + this.enqueueForAggregation(event.client, event.request, true, false); + }, + { signal: abortController.signal }, + ); + client.addEventListener( + PubNubClientEvent.SendLeaveRequest, + (event) => { + if (!(event instanceof PubNubClientSendLeaveEvent)) return; + const request = this.patchedLeaveRequest(event.request); + if (!request) return; + + this.sendRequest( + request, + (fetchRequest, response) => request.handleProcessingSuccess(fetchRequest, response), + (fetchRequest, errorResponse) => request.handleProcessingError(fetchRequest, errorResponse), + ); + }, + { signal: abortController.signal }, + ); + }); + clientsManager.addEventListener(PubNubClientsManagerEvent.Unregistered, (event) => { + const { client, withLeave } = event as PubNubClientManagerUnregisterEvent; + + // Remove all listeners added for the client. + const abortController = this.clientAbortControllers[client.identifier]; + delete this.clientAbortControllers[client.identifier]; + if (abortController) abortController.abort(); + + // Update manager's state. + this.removeClient(client, false, withLeave, true); + }); + } + + /** + * Listen for subscription {@link SubscriptionState|state} events. + * + * @param state - Reference to the subscription object for which listeners should be added. + */ + private addListenerForSubscriptionStateEvents(state: SubscriptionState) { + const abortController = new AbortController(); + + state.addEventListener( + SubscriptionStateEvent.Changed, + (event) => { + const { requestsWithInitialResponse, canceledRequests, newRequests, leaveRequest } = + event as SubscriptionStateChangeEvent; + + // Cancel outdated ongoing `service`-provided subscribe requests. + canceledRequests.forEach((request) => request.cancel('Cancel request')); + + // Schedule new `service`-provided subscribe requests processing. + newRequests.forEach((request) => { + this.sendRequest( + request, + (fetchRequest, response) => request.handleProcessingSuccess(fetchRequest, response), + (fetchRequest, error) => request.handleProcessingError(fetchRequest, error), + request.isInitialSubscribe && request.timetokenOverride !== '0' + ? (response) => + this.patchInitialSubscribeResponse( + response, + request.timetokenOverride, + request.timetokenRegionOverride, + ) + : undefined, + ); + }); + + requestsWithInitialResponse.forEach((response) => { + const { request, timetoken, region } = response; + request.handleProcessingStarted(); + this.makeResponseOnHandshakeRequest(request, timetoken, region); + }); + + if (leaveRequest) { + this.sendRequest( + leaveRequest, + (fetchRequest, response) => leaveRequest.handleProcessingSuccess(fetchRequest, response), + (fetchRequest, error) => leaveRequest.handleProcessingError(fetchRequest, error), + ); + } + }, + { signal: abortController.signal }, + ); + state.addEventListener( + SubscriptionStateEvent.Invalidated, + () => { + delete this.subscriptionStates[state.identifier]; + abortController.abort(); + }, + { + signal: abortController.signal, + once: true, + }, + ); + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Retrieve subscription {@link SubscriptionState|state} with which specific client is working. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client for which subscription + * {@link SubscriptionState|state} should be found. + * @returns Reference to the subscription {@link SubscriptionState|state} if the client has ongoing + * {@link SubscribeRequest|requests}. + */ + private subscriptionStateForClient(client: PubNubClient) { + return Object.values(this.subscriptionStates).find((state) => state.hasStateForClient(client)); + } + + /** + * Create `service`-provided `leave` request from a `client`-provided {@link LeaveRequest|request} with channels and + * groups for removal. + * + * @param request - Original `client`-provided `leave` {@link LeaveRequest|request}. + * @returns `service`-provided `leave` request. + */ + private patchedLeaveRequest(request: LeaveRequest) { + const subscriptionState = this.subscriptionStateForClient(request.client); + // Something is wrong. Client doesn't have any active subscriptions. + if (!subscriptionState) { + request.cancel(); + return; + } + + // Filter list from channels and groups which is still in use. + const clientStateForLeave = subscriptionState.uniqueStateForClient( + request.client, + request.channels, + request.channelGroups, + ); + + const serviceRequest = leaveRequest( + request.client, + clientStateForLeave.channels, + clientStateForLeave.channelGroups, + ); + if (serviceRequest) request.serviceRequest = serviceRequest; + + return serviceRequest; + } + + /** + * Return "response" from PubNub service with initial timetoken data. + * + * @param request - Client-provided handshake/initial request for which response should be provided. + * @param timetoken - Timetoken from currently active service request. + * @param region - Region from currently active service request. + */ + private makeResponseOnHandshakeRequest(request: SubscribeRequest, timetoken: string, region: string) { + const body = new TextEncoder().encode(`{"t":{"t":"${timetoken}","r":${region ?? '0'}},"m":[]}`); + + request.handleProcessingSuccess(request.asFetchRequest, { + type: 'request-process-success', + clientIdentifier: '', + identifier: '', + url: '', + response: { + contentType: 'text/javascript; charset="UTF-8"', + contentLength: body.length, + headers: { 'content-type': 'text/javascript; charset="UTF-8"', 'content-length': `${body.length}` }, + status: 200, + body, + }, + }); + } + + /** + * Patch `service`-provided subscribe response with new timetoken and region. + * + * @param serverResponse - Original service response for patching. + * @param timetoken - Original timetoken override value. + * @param region - Original timetoken region override value. + * @returns Patched subscribe REST API response. + */ + private patchInitialSubscribeResponse( + serverResponse: [Response, ArrayBuffer], + timetoken?: string, + region?: string, + ): [Response, ArrayBuffer] { + if (timetoken === undefined || timetoken === '0' || serverResponse[0].status >= 400) return serverResponse; + + let json: { t: { t: string; r: number }; m: Record[] }; + const response = serverResponse[0]; + let decidedResponse = response; + let body = serverResponse[1]; + + try { + json = JSON.parse(SubscribeRequestsManager.textDecoder.decode(body)); + } catch (error) { + console.error(`Subscribe response parse error: ${error}`); + return serverResponse; + } + + // Replace server-provided timetoken. + json.t.t = timetoken; + if (region) json.t.r = parseInt(region, 10); + + try { + body = SubscribeRequestsManager.textEncoder.encode(JSON.stringify(json)).buffer; + + if (body.byteLength) { + const headers = new Headers(response.headers); + headers.set('Content-Length', `${body.byteLength}`); + + // Create a new response with the original response options and modified headers + decidedResponse = new Response(body, { + status: response.status, + statusText: response.statusText, + headers: headers, + }); + } + } catch (error) { + console.error(`Subscribe serialization error: ${error}`); + return serverResponse; + } + + return body.byteLength > 0 ? [decidedResponse, body] : serverResponse; + } + // endregion +} diff --git a/src/transport/subscription-worker/components/subscription-state.ts b/src/transport/subscription-worker/components/subscription-state.ts new file mode 100644 index 000000000..d0fa02657 --- /dev/null +++ b/src/transport/subscription-worker/components/subscription-state.ts @@ -0,0 +1,838 @@ +import { PubNubSharedWorkerRequestEvents } from './custom-events/request-processing-event'; +import { + SubscriptionStateChangeEvent, + SubscriptionStateInvalidateEvent, +} from './custom-events/subscription-state-event'; +import { SubscribeRequest } from './subscribe-request'; +import { Payload } from '../../../core/types/api'; +import { PubNubClient } from './pubnub-client'; +import { LeaveRequest } from './leave-request'; +import { AccessToken } from './access-token'; +import { leaveRequest } from './helpers'; + +export class SubscriptionStateChange { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Timestamp when batched changes has been modified before. + */ + private static previousChangeTimestamp = 0; + + /** + * Timestamp when subscription change has been enqueued. + */ + private readonly _timestamp: number; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructor ---------------------- + // -------------------------------------------------------- + // region Constructor + + /** + * Squash changes to exclude repetitive removal and addition of the same requests in a single change transaction. + * + * @param changes - List of changes that should be analyzed and squashed if possible. + * @returns List of changes that doesn't have self-excluding change requests. + */ + static squashedChanges(changes: SubscriptionStateChange[]) { + if (!changes.length || changes.length === 1) return changes; + + // Sort changes in order in which they have been created (original `changes` is Set). + const sortedChanges = changes.sort((lhc, rhc) => lhc.timestamp - rhc.timestamp); + + // Remove changes which first add and then remove same request (removes both addition and removal change entry). + const requestAddChange = sortedChanges.filter((change) => !change.remove); + requestAddChange.forEach((addChange) => { + for (let idx = 0; idx < requestAddChange.length; idx++) { + const change = requestAddChange[idx]; + if (!change.remove || change.request.identifier !== addChange.request.identifier) continue; + sortedChanges.splice(idx, 1); + sortedChanges.splice(sortedChanges.indexOf(addChange), 1); + break; + } + }); + + // Filter out old `add` change entries for the same client. + const addChangePerClient: Record = {}; + requestAddChange.forEach((change) => { + if (addChangePerClient[change.clientIdentifier]) { + const changeIdx = sortedChanges.indexOf(change); + if (changeIdx >= 0) sortedChanges.splice(changeIdx, 1); + } + addChangePerClient[change.clientIdentifier] = change; + }); + + return sortedChanges; + } + + /** + * Create subscription state batched change entry. + * + * @param clientIdentifier - Identifier of the {@link PubNubClient|PubNub} client that provided data for subscription + * state change. + * @param request - Request that should be used during batched subscription state modification. + * @param remove - Whether provided {@link request} should be removed from `subscription` state or not. + * @param sendLeave - Whether the {@link PubNubClient|client} should send a presence `leave` request for _free_ + * channels and groups or not. + * @param [clientInvalidate=false] - Whether the `subscription` state change was caused by the + * {@link PubNubClient|PubNub} client invalidation (unregister) or not. + */ + constructor( + public readonly clientIdentifier: string, + public readonly request: SubscribeRequest, + public readonly remove: boolean, + public readonly sendLeave: boolean, + public readonly clientInvalidate = false, + ) { + this._timestamp = this.timestampForChange(); + } + // endregion + + // -------------------------------------------------------- + // --------------------- Properties ----------------------- + // -------------------------------------------------------- + // region Properties + + /** + * Retrieve subscription change enqueue timestamp. + * + * @returns Subscription change enqueue timestamp. + */ + get timestamp() { + return this._timestamp; + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Serialize object for easier representation in logs. + * + * @returns Stringified `subscription` state object. + */ + toString() { + return `SubscriptionStateChange { timestamp: ${this.timestamp}, client: ${ + this.clientIdentifier + }, request: ${this.request.toString()}, remove: ${this.remove ? "'remove'" : "'do not remove'"}, sendLeave: ${ + this.sendLeave ? "'send'" : "'do not send'" + } }`; + } + + /** + * Serialize the object to a "typed" JSON string. + * + * @returns "Typed" JSON string. + */ + toJSON() { + return this.toString(); + } + + /** + * Retrieve timestamp when change has been added to the batch. + * + * Non-repetitive timestamp required for proper changes sorting and identification of requests which has been removed + * and added during single batch. + * + * @returns Non-repetitive timestamp even for burst changes. + */ + private timestampForChange() { + const timestamp = Date.now(); + + if (timestamp <= SubscriptionStateChange.previousChangeTimestamp) { + SubscriptionStateChange.previousChangeTimestamp++; + } else SubscriptionStateChange.previousChangeTimestamp = timestamp; + + return SubscriptionStateChange.previousChangeTimestamp; + } + // endregion +} + +/** + * Aggregated subscription state. + * + * State object responsible for keeping in sync and optimization of `client`-provided {@link SubscribeRequest|requests} + * by attaching them to already existing or new aggregated `service`-provided {@link SubscribeRequest|requests} to + * reduce number of concurrent connections. + */ +export class SubscriptionState extends EventTarget { + // -------------------------------------------------------- + // ---------------------- Information --------------------- + // -------------------------------------------------------- + // region Information + + /** + * Map of `client`-provided request identifiers to the subscription state listener abort controller. + */ + private requestListenersAbort: Record = {}; + + /** + * Map of {@link PubNubClient|client} identifiers to their portion of data which affects subscription state. + * + * **Note:** This information is removed only with the {@link SubscriptionState.removeClient|removeClient} function + * call. + */ + private clientsState: Record< + string, + { channels: Set; channelGroups: Set; state?: Record } + > = {}; + + /** + * Map of {@link PubNubClient|client} to its {@link SubscribeRequest|request} that already received response/error + * or has been canceled. + */ + private lastCompletedRequest: Record = {}; + + /** + * List of identifiers of the {@link PubNubClient|PubNub} clients that should be invalidated when it will be + * possible. + */ + private clientsForInvalidation: string[] = []; + + /** + * Map of {@link PubNubClient|client} to its {@link SubscribeRequest|request} which is pending for + * `service`-provided {@link SubscribeRequest|request} processing results. + */ + private requests: Record = {}; + + /** + * Aggregated/modified {@link SubscribeRequest|subscribe} requests which is used to call PubNub REST API. + * + * **Note:** There could be multiple requests to handle the situation when similar {@link PubNubClient|PubNub} clients + * have subscriptions but with different timetokens (if requests have intersecting lists of channels and groups they + * can be merged in the future if a response on a similar channel will be received and the same `timetoken` will be + * used for continuation). + */ + private serviceRequests: SubscribeRequest[] = []; + + /** + * Cached list of channel groups used with recent aggregation service requests. + * + * **Note:** Set required to have the ability to identify which channel groups have been added/removed with recent + * {@link SubscriptionStateChange|changes} list processing. + */ + private channelGroups: Set = new Set(); + + /** + * Cached list of channels used with recent aggregation service requests. + * + * **Note:** Set required to have the ability to identify which channels have been added/removed with recent + * {@link SubscriptionStateChange|changes} list processing. + */ + private channels: Set = new Set(); + + /** + * Reference to the most suitable access token to access {@link SubscriptionState#channels|channels} and + * {@link SubscriptionState#channelGroups|channelGroups}. + */ + private accessToken?: AccessToken; + // endregion + + // -------------------------------------------------------- + // --------------------- Constructor ---------------------- + // -------------------------------------------------------- + // region Constructor + + /** + * Create subscription state management object. + * + * @param identifier - Similar {@link SubscribeRequest|subscribe} requests aggregation identifier. + */ + constructor(public readonly identifier: string) { + super(); + } + // endregion + + // -------------------------------------------------------- + // ---------------------- Accessors ----------------------- + // -------------------------------------------------------- + // region Accessors + + /** + * Check whether subscription state contain state for specific {@link PubNubClient|PubNub} client. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client for which state should be checked. + * @returns `true` if there is state related to the {@link PubNubClient|client}. + */ + hasStateForClient(client: PubNubClient) { + return !!this.clientsState[client.identifier]; + } + + /** + * Retrieve portion of subscription state which is unique for the {@link PubNubClient|client}. + * + * Function will return list of channels and groups which has been introduced by the client into the state (no other + * clients have them). + * + * @param client - Reference to the {@link PubNubClient|PubNub} client for which unique elements should be retrieved + * from the state. + * @param channels - List of client's channels from subscription state. + * @param channelGroups - List of client's channel groups from subscription state. + * @returns State with channels and channel groups unique for the {@link PubNubClient|client}. + */ + uniqueStateForClient( + client: PubNubClient, + channels: string[], + channelGroups: string[], + ): { + channels: string[]; + channelGroups: string[]; + } { + let uniqueChannelGroups = [...channelGroups]; + let uniqueChannels = [...channels]; + + Object.entries(this.clientsState).forEach(([identifier, state]) => { + if (identifier === client.identifier) return; + uniqueChannelGroups = uniqueChannelGroups.filter((channelGroup) => !state.channelGroups.has(channelGroup)); + uniqueChannels = uniqueChannels.filter((channel) => !state.channels.has(channel)); + }); + + return { channels: uniqueChannels, channelGroups: uniqueChannelGroups }; + } + + /** + * Retrieve ongoing `client`-provided {@link SubscribeRequest|subscribe} request for the {@link PubNubClient|client}. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client for which requests should be retrieved. + * @param [invalidated=false] - Whether receiving request for invalidated (unregistered) {@link PubNubClient|PubNub} + * client. + * @returns A `client`-provided {@link SubscribeRequest|subscribe} request if it has been sent by + * {@link PubNubClient|client}. + */ + requestForClient(client: PubNubClient, invalidated = false): SubscribeRequest | undefined { + return this.requests[client.identifier] ?? (invalidated ? this.lastCompletedRequest[client.identifier] : undefined); + } + // endregion + + // -------------------------------------------------------- + // --------------------- Aggregation ---------------------- + // -------------------------------------------------------- + // region Aggregation + + /** + * Update access token for the client which should be used with next subscribe request. + * + * @param accessToken - Access token for next subscribe REST API call. + */ + updateClientAccessToken(accessToken: AccessToken) { + if (!this.accessToken || accessToken.isNewerThan(this.accessToken)) this.accessToken = accessToken; + } + + /** + * Mark specific client as suitable for state invalidation when it will be appropriate. + * + * @param client - Reference to the {@link PubNubClient|PubNub} client which should be invalidated when will be + * possible. + */ + invalidateClient(client: PubNubClient) { + if (this.clientsForInvalidation.includes(client.identifier)) return; + this.clientsForInvalidation.push(client.identifier); + } + + /** + * Process batched subscription state change. + * + * @param changes - List of {@link SubscriptionStateChange|changes} made from requests received from the core + * {@link PubNubClient|PubNub} client modules. + */ + processChanges(changes: SubscriptionStateChange[]) { + if (changes.length) changes = SubscriptionStateChange.squashedChanges(changes); + + if (!changes.length) return; + + let stateRefreshRequired = this.channelGroups.size === 0 && this.channels.size === 0; + if (!stateRefreshRequired) + stateRefreshRequired = changes.some((change) => change.remove || change.request.requireCachedStateReset); + + // Update list of PubNub client requests. + const appliedRequests = this.applyChanges(changes); + + let stateChanges: ReturnType; + if (stateRefreshRequired) stateChanges = this.refreshInternalState(); + + // Identify and dispatch subscription state change event with service requests for cancellation and start. + this.handleSubscriptionStateChange( + changes, + stateChanges, + appliedRequests.initial, + appliedRequests.continuation, + appliedRequests.removed, + ); + + // Check whether subscription state for all registered clients has been removed or not. + if (!Object.keys(this.clientsState).length) this.dispatchEvent(new SubscriptionStateInvalidateEvent()); + } + + /** + * Make changes to the internal state. + * + * Categorize changes by grouping requests (into `initial`, `continuation`, and `removed` groups) and update internal + * state to reflect those changes (add/remove `client`-provided requests). + * + * @param changes - Final subscription state changes list. + * @returns Subscribe request separated by different subscription loop stages. + */ + private applyChanges(changes: SubscriptionStateChange[]): { + initial: SubscribeRequest[]; + continuation: SubscribeRequest[]; + removed: SubscribeRequest[]; + } { + const continuationRequests: SubscribeRequest[] = []; + const initialRequests: SubscribeRequest[] = []; + const removedRequests: SubscribeRequest[] = []; + + changes.forEach((change) => { + const { remove, request, clientIdentifier, clientInvalidate } = change; + + if (!remove) { + if (request.isInitialSubscribe) initialRequests.push(request); + else continuationRequests.push(request); + + this.requests[clientIdentifier] = request; + this.addListenersForRequestEvents(request); + } + + if (remove && (!!this.requests[clientIdentifier] || !!this.lastCompletedRequest[clientIdentifier])) { + if (clientInvalidate) { + delete this.lastCompletedRequest[clientIdentifier]; + delete this.clientsState[clientIdentifier]; + } + + delete this.requests[clientIdentifier]; + removedRequests.push(request); + } + }); + + return { initial: initialRequests, continuation: continuationRequests, removed: removedRequests }; + } + + /** + * Process changes in subscription state. + * + * @param changes - Final subscription state changes list. + * @param stateChanges - Changes to the subscribed channels and groups in aggregated requests. + * @param initialRequests - List of `client`-provided handshake {@link SubscribeRequest|subscribe} requests. + * @param continuationRequests - List of `client`-provided subscription loop continuation + * {@link SubscribeRequest|subscribe} requests. + * @param removedRequests - List of `client`-provided {@link SubscribeRequest|subscribe} requests that should be + * removed from the state. + */ + private handleSubscriptionStateChange( + changes: SubscriptionStateChange[], + stateChanges: ReturnType, + initialRequests: SubscribeRequest[], + continuationRequests: SubscribeRequest[], + removedRequests: SubscribeRequest[], + ) { + // Retrieve list of active (not completed or canceled) `service`-provided requests. + const serviceRequests = this.serviceRequests.filter((request) => !request.completed && !request.canceled); + const requestsWithInitialResponse: { request: SubscribeRequest; timetoken: string; region: string }[] = []; + const newContinuationServiceRequests: SubscribeRequest[] = []; + const newInitialServiceRequests: SubscribeRequest[] = []; + const cancelledServiceRequests: SubscribeRequest[] = []; + let serviceLeaveRequest: LeaveRequest | undefined; + + // Identify token override for initial requests. + let timetokenOverrideRefreshTimestamp: number | undefined; + let decidedTimetokenRegionOverride: string | undefined; + let decidedTimetokenOverride: string | undefined; + + const cancelServiceRequest = (serviceRequest: SubscribeRequest) => { + cancelledServiceRequests.push(serviceRequest); + + const rest = serviceRequest + .dependentRequests() + .filter((dependantRequest) => !removedRequests.includes(dependantRequest)); + + if (rest.length === 0) return; + + rest.forEach((dependantRequest) => (dependantRequest.serviceRequest = undefined)); + (serviceRequest.isInitialSubscribe ? initialRequests : continuationRequests).push(...rest); + }; + + // -------------------------------------------------- + // Identify ongoing `service`-provided requests which should be canceled because channels/channel groups has been + // added/removed. + // + + if (stateChanges) { + if (stateChanges.channels.added || stateChanges.channelGroups.added) { + for (const serviceRequest of serviceRequests) cancelServiceRequest(serviceRequest); + serviceRequests.length = 0; + } else if (stateChanges.channels.removed || stateChanges.channelGroups.removed) { + const channelGroups = stateChanges.channelGroups.removed ?? []; + const channels = stateChanges.channels.removed ?? []; + + for (let serviceRequestIdx = serviceRequests.length - 1; serviceRequestIdx >= 0; serviceRequestIdx--) { + const serviceRequest = serviceRequests[serviceRequestIdx]; + if (!serviceRequest.hasAnyChannelsOrGroups(channels, channelGroups)) continue; + + cancelServiceRequest(serviceRequest); + serviceRequests.splice(serviceRequestIdx, 1); + } + } + } + + continuationRequests = this.squashSameClientRequests(continuationRequests); + initialRequests = this.squashSameClientRequests(initialRequests); + + // -------------------------------------------------- + // Searching for optimal timetoken, which should be used for `service`-provided request (will override response with + // new timetoken to make it possible to aggregate on next subscription loop with already ongoing `service`-provided + // long-poll request). + // + + (initialRequests.length ? continuationRequests : []).forEach((request) => { + let shouldSetPreviousTimetoken = !decidedTimetokenOverride; + if (!shouldSetPreviousTimetoken && request.timetoken !== '0') { + if (decidedTimetokenOverride === '0') shouldSetPreviousTimetoken = true; + else if (request.timetoken < decidedTimetokenOverride!) + shouldSetPreviousTimetoken = request.creationDate > timetokenOverrideRefreshTimestamp!; + } + + if (shouldSetPreviousTimetoken) { + timetokenOverrideRefreshTimestamp = request.creationDate; + decidedTimetokenOverride = request.timetoken; + decidedTimetokenRegionOverride = request.region; + } + }); + + // -------------------------------------------------- + // Try to attach `initial` and `continuation` `client`-provided requests to ongoing `service`-provided requests. + // + + // Separate continuation requests by next subscription loop timetoken. + // This prevents possibility that some subscribe requests will be aggregated into one with much newer timetoken and + // miss messages as result. + const continuationByTimetoken: Record = {}; + continuationRequests.forEach((request) => { + if (!continuationByTimetoken[request.timetoken]) continuationByTimetoken[request.timetoken] = [request]; + else continuationByTimetoken[request.timetoken].push(request); + }); + + this.attachToServiceRequest(serviceRequests, initialRequests); + for (let initialRequestIdx = initialRequests.length - 1; initialRequestIdx >= 0; initialRequestIdx--) { + const request = initialRequests[initialRequestIdx]; + + serviceRequests.forEach((serviceRequest) => { + if (!request.isSubsetOf(serviceRequest) || serviceRequest.isInitialSubscribe) return; + + const { region, timetoken } = serviceRequest; + requestsWithInitialResponse.push({ request, timetoken, region: region! }); + initialRequests.splice(initialRequestIdx, 1); + }); + } + if (initialRequests.length) { + let aggregationRequests: SubscribeRequest[]; + + if (continuationRequests.length) { + decidedTimetokenOverride = Object.keys(continuationByTimetoken).sort().pop()!; + const requests = continuationByTimetoken[decidedTimetokenOverride]; + decidedTimetokenRegionOverride = requests[0].region!; + delete continuationByTimetoken[decidedTimetokenOverride]; + + requests.forEach((request) => request.resetToInitialRequest()); + aggregationRequests = [...initialRequests, ...requests]; + } else aggregationRequests = initialRequests; + + // Create handshake service request (if possible) + this.createAggregatedRequest( + aggregationRequests, + newInitialServiceRequests, + decidedTimetokenOverride, + decidedTimetokenRegionOverride, + ); + } + + // Handle case when `initial` requests are supersets of continuation requests. + Object.values(continuationByTimetoken).forEach((requestsByTimetoken) => { + // Set `initial` `service`-provided requests as service requests for those continuation `client`-provided requests + // that are a _subset_ of them. + this.attachToServiceRequest(newInitialServiceRequests, requestsByTimetoken); + // Set `ongoing` `service`-provided requests as service requests for those continuation `client`-provided requests + // that are a _subset_ of them (if any still available). + this.attachToServiceRequest(serviceRequests, requestsByTimetoken); + + // Create continuation `service`-provided request (if possible). + this.createAggregatedRequest(requestsByTimetoken, newContinuationServiceRequests); + }); + + // -------------------------------------------------- + // Identify channels and groups for which presence `leave` should be generated. + // + + const channelGroupsForLeave = new Set(); + const channelsForLeave = new Set(); + + if ( + stateChanges && + removedRequests.length && + (stateChanges.channels.removed || stateChanges.channelGroups.removed) + ) { + const channelGroups = stateChanges.channelGroups.removed ?? []; + const channels = stateChanges.channels.removed ?? []; + const client = removedRequests[0].client; + + changes + .filter((change) => change.remove && change.sendLeave) + .forEach((change) => { + const { channels: requestChannels, channelGroups: requestChannelsGroups } = change.request; + channelGroups.forEach((group) => requestChannelsGroups.includes(group) && channelGroupsForLeave.add(group)); + channels.forEach((channel) => requestChannels.includes(channel) && channelsForLeave.add(channel)); + }); + serviceLeaveRequest = leaveRequest(client, [...channelsForLeave], [...channelGroupsForLeave]); + } + + if ( + requestsWithInitialResponse.length || + newInitialServiceRequests.length || + newContinuationServiceRequests.length || + cancelledServiceRequests.length || + serviceLeaveRequest + ) { + this.dispatchEvent( + new SubscriptionStateChangeEvent( + requestsWithInitialResponse, + [...newInitialServiceRequests, ...newContinuationServiceRequests], + cancelledServiceRequests, + serviceLeaveRequest, + ), + ); + } + } + + /** + * Refresh the internal subscription's state. + */ + private refreshInternalState() { + const channelGroups = new Set(); + const channels = new Set(); + + // Aggregate channels and groups from active requests. + Object.entries(this.requests).forEach(([clientIdentifier, request]) => { + const clientState = (this.clientsState[clientIdentifier] ??= { channels: new Set(), channelGroups: new Set() }); + + request.channelGroups.forEach(clientState.channelGroups.add, clientState.channelGroups); + request.channels.forEach(clientState.channels.add, clientState.channels); + + request.channelGroups.forEach(channelGroups.add, channelGroups); + request.channels.forEach(channels.add, channels); + }); + + const changes = this.subscriptionStateChanges(channels, channelGroups); + + // Update state information. + this.channelGroups = channelGroups; + this.channels = channels; + + // Identify most suitable access token. + const sortedTokens = Object.values(this.requests) + .flat() + .filter((request) => !!request.accessToken) + .map((request) => request.accessToken!) + .sort(AccessToken.compare); + if (sortedTokens && sortedTokens.length > 0) this.accessToken = sortedTokens.pop(); + + return changes; + } + // endregion + + // -------------------------------------------------------- + // ------------------- Event Handlers --------------------- + // -------------------------------------------------------- + // region Event handlers + + private addListenersForRequestEvents(request: SubscribeRequest) { + const abortController = (this.requestListenersAbort[request.identifier] = new AbortController()); + + const cleanUpCallback = () => { + this.removeListenersFromRequestEvents(request); + if (!request.isServiceRequest) { + if (this.requests[request.client.identifier]) { + this.lastCompletedRequest[request.client.identifier] = request; + delete this.requests[request.client.identifier]; + + const clientIdx = this.clientsForInvalidation.indexOf(request.client.identifier); + if (clientIdx > 0) { + this.clientsForInvalidation.splice(clientIdx, 1); + delete this.lastCompletedRequest[request.client.identifier]; + delete this.clientsState[request.client.identifier]; + + // Check whether subscription state for all registered clients has been removed or not. + if (!Object.keys(this.clientsState).length) this.dispatchEvent(new SubscriptionStateInvalidateEvent()); + } + } + + return; + } + + const requestIdx = this.serviceRequests.indexOf(request); + if (requestIdx >= 0) this.serviceRequests.splice(requestIdx, 1); + }; + + request.addEventListener(PubNubSharedWorkerRequestEvents.Success, cleanUpCallback, { + signal: abortController.signal, + once: true, + }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Error, cleanUpCallback, { + signal: abortController.signal, + once: true, + }); + request.addEventListener(PubNubSharedWorkerRequestEvents.Canceled, cleanUpCallback, { + signal: abortController.signal, + once: true, + }); + } + + private removeListenersFromRequestEvents(request: SubscribeRequest) { + if (!this.requestListenersAbort[request.request.identifier]) return; + + this.requestListenersAbort[request.request.identifier].abort(); + delete this.requestListenersAbort[request.request.identifier]; + } + // endregion + + // -------------------------------------------------------- + // ----------------------- Helpers ------------------------ + // -------------------------------------------------------- + // region Helpers + + /** + * Identify changes to the channels and groups. + * + * @param channels - Set with channels which has been left after client requests list has been changed. + * @param channelGroups - Set with channel groups which has been left after client requests list has been changed. + * @returns Objects with names of channels and groups which has been added and removed from the current subscription + * state. + */ + private subscriptionStateChanges( + channels: Set, + channelGroups: Set, + ): + | { + channelGroups: { removed?: string[]; added?: string[] }; + channels: { removed?: string[]; added?: string[] }; + } + | undefined { + const stateIsEmpty = this.channelGroups.size === 0 && this.channels.size === 0; + const changes = { channelGroups: {}, channels: {} }; + const removedChannelGroups: string[] = []; + const addedChannelGroups: string[] = []; + const removedChannels: string[] = []; + const addedChannels: string[] = []; + + for (const group of channelGroups) if (!this.channelGroups.has(group)) addedChannelGroups.push(group); + for (const channel of channels) if (!this.channels.has(channel)) addedChannels.push(channel); + + if (!stateIsEmpty) { + for (const group of this.channelGroups) if (!channelGroups.has(group)) removedChannelGroups.push(group); + for (const channel of this.channels) if (!channels.has(channel)) removedChannels.push(channel); + } + + if (addedChannels.length || removedChannels.length) { + changes.channels = { + ...(addedChannels.length ? { added: addedChannels } : {}), + ...(removedChannels.length ? { removed: removedChannels } : {}), + }; + } + + if (addedChannelGroups.length || removedChannelGroups.length) { + changes.channelGroups = { + ...(addedChannelGroups.length ? { added: addedChannelGroups } : {}), + ...(removedChannelGroups.length ? { removed: removedChannelGroups } : {}), + }; + } + + return Object.keys(changes.channelGroups).length === 0 && Object.keys(changes.channels).length === 0 + ? undefined + : changes; + } + + /** + * Squash list of provided requests to represent latest request for each client. + * + * @param requests - List with potentially repetitive or multiple {@link SubscribeRequest|subscribe} requests for the + * same {@link PubNubClient|PubNub} client. + * @returns List of latest {@link SubscribeRequest|subscribe} requests for corresponding {@link PubNubClient|PubNub} + * clients. + */ + private squashSameClientRequests(requests: SubscribeRequest[]) { + if (!requests.length || requests.length === 1) return requests; + + // Sort requests in order in which they have been created. + const sortedRequests = requests.sort((lhr, rhr) => lhr.creationDate - rhr.creationDate); + return Object.values( + sortedRequests.reduce( + (acc, value) => { + acc[value.client.identifier] = value; + return acc; + }, + {} as Record, + ), + ); + } + + /** + * Attach `client`-provided requests to the compatible ongoing `service`-provided requests. + * + * @param serviceRequests - List of ongoing `service`-provided subscribe requests. + * @param requests - List of `client`-provided requests that should try to hook for service response using existing + * ongoing `service`-provided requests. + */ + private attachToServiceRequest(serviceRequests: SubscribeRequest[], requests: SubscribeRequest[]) { + if (!serviceRequests.length || !requests.length) return; + + [...requests].forEach((request) => { + for (const serviceRequest of serviceRequests) { + // Check whether continuation request is actually a subset of the `service`-provided request or not. + // Note: Second condition handled in the function which calls `attachToServiceRequest`. + if ( + !!request.serviceRequest || + !request.isSubsetOf(serviceRequest) || + (request.isInitialSubscribe && !serviceRequest.isInitialSubscribe) + ) + continue; + + // Attach to the matching `service`-provided request. + request.serviceRequest = serviceRequest; + + // There is no need to aggregate attached request. + const requestIdx = requests.indexOf(request); + requests.splice(requestIdx, 1); + break; + } + }); + } + + /** + * Create aggregated `service`-provided {@link SubscribeRequest|subscribe} request. + * + * @param requests - List of `client`-provided {@link SubscribeRequest|subscribe} requests which should be sent with + * as single `service`-provided request. + * @param serviceRequests - List with created `service`-provided {@link SubscribeRequest|subscribe} requests. + * @param timetokenOverride - Timetoken that should replace the initial response timetoken. + * @param regionOverride - Timetoken region that should replace the initial response timetoken region. + */ + private createAggregatedRequest( + requests: SubscribeRequest[], + serviceRequests: SubscribeRequest[], + timetokenOverride?: string, + regionOverride?: string, + ) { + if (requests.length === 0) return; + + const serviceRequest = SubscribeRequest.fromRequests(requests, this.accessToken, timetokenOverride, regionOverride); + this.addListenersForRequestEvents(serviceRequest); + + requests.forEach((request) => (request.serviceRequest = serviceRequest)); + this.serviceRequests.push(serviceRequest); + serviceRequests.push(serviceRequest); + } + // endregion +} diff --git a/src/transport/subscription-worker/subscription-worker-middleware.ts b/src/transport/subscription-worker/subscription-worker-middleware.ts index ababfe0ed..09a6090c8 100644 --- a/src/transport/subscription-worker/subscription-worker-middleware.ts +++ b/src/transport/subscription-worker/subscription-worker-middleware.ts @@ -9,17 +9,17 @@ import { CancellationController, TransportRequest } from '../../core/types/transport-request'; import { TransportResponse } from '../../core/types/transport-response'; +import * as PubNubSubscriptionWorker from './subscription-worker-types'; import { LoggerManager } from '../../core/components/logger-manager'; +import { LogLevel, LogMessage } from '../../core/interfaces/logger'; import { TokenManager } from '../../core/components/token_manager'; -import * as PubNubSubscriptionWorker from './subscription-worker'; +import { RequestSendingError } from './subscription-worker-types'; import { PubNubAPIError } from '../../errors/pubnub-api-error'; import StatusCategory from '../../core/constants/categories'; import { Transport } from '../../core/interfaces/transport'; import * as PAM from '../../core/types/api/access-manager'; -import { LogMessage } from '../../core/interfaces/logger'; import { Status, StatusEvent } from '../../core/types/api'; import PNOperations from '../../core/constants/operations'; -import { RequestSendingError } from './subscription-worker'; // -------------------------------------------------------- // ------------------------ Types ------------------------- @@ -63,9 +63,9 @@ type PubNubMiddlewareConfiguration = { workerUnsubscribeOfflineClients: boolean; /** - * Whether verbose logging should be enabled for `Subscription` worker should print debug messages or not. + * Minimum messages log level which should be passed to the `Subscription` worker logger. */ - workerLogVerbosity: boolean; + workerLogLevel: LogLevel; /** * Whether heartbeat request success should be announced or not. @@ -180,6 +180,7 @@ export class SubscriptionWorkerMiddleware implements Transport { clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, userId: this.configuration.userId, + workerLogLevel: this.configuration.workerLogLevel, }); } @@ -197,6 +198,7 @@ export class SubscriptionWorkerMiddleware implements Transport { clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, userId: this.configuration.userId, + workerLogLevel: this.configuration.workerLogLevel, }); } @@ -212,6 +214,7 @@ export class SubscriptionWorkerMiddleware implements Transport { clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, userId: this.configuration.userId, + workerLogLevel: this.configuration.workerLogLevel, }; // Trigger request processing by Service Worker. @@ -223,6 +226,18 @@ export class SubscriptionWorkerMiddleware implements Transport { .then(() => this.scheduleEventPost(updateEvent)); } + /** + * Disconnect client and terminate ongoing long-poll requests (if needed). + */ + disconnect() { + this.scheduleEventPost({ + type: 'client-disconnect', + clientIdentifier: this.configuration.clientIdentifier, + subscriptionKey: this.configuration.subscriptionKey, + workerLogLevel: this.configuration.workerLogLevel, + }); + } + /** * Terminate all ongoing long-poll requests. */ @@ -231,6 +246,7 @@ export class SubscriptionWorkerMiddleware implements Transport { type: 'client-unregister', clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, + workerLogLevel: this.configuration.workerLogLevel, }); } @@ -247,6 +263,7 @@ export class SubscriptionWorkerMiddleware implements Transport { clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, request: req, + workerLogLevel: this.configuration.workerLogLevel, }; if (req.cancellable) { @@ -257,6 +274,7 @@ export class SubscriptionWorkerMiddleware implements Transport { clientIdentifier: this.configuration.clientIdentifier, subscriptionKey: this.configuration.subscriptionKey, identifier: req.identifier, + workerLogLevel: this.configuration.workerLogLevel, }; // Cancel active request with specified identifier. @@ -375,12 +393,21 @@ export class SubscriptionWorkerMiddleware implements Transport { heartbeatInterval: this.configuration.heartbeatInterval, workerOfflineClientsCheckInterval: this.configuration.workerOfflineClientsCheckInterval, workerUnsubscribeOfflineClients: this.configuration.workerUnsubscribeOfflineClients, - workerLogVerbosity: this.configuration.workerLogVerbosity, + workerLogLevel: this.configuration.workerLogLevel, }, true, ); this.subscriptionWorker.port.onmessage = (event) => this.handleWorkerEvent(event); + + if (this.shouldAnnounceNewerSharedWorkerVersionAvailability()) + localStorage.setItem('PNSubscriptionSharedWorkerVersion', this.configuration.sdkVersion); + + window.addEventListener('storage', (event) => { + if (event.key !== 'PNSubscriptionSharedWorkerVersion' || !event.newValue) return; + if (this._emitStatus && this.isNewerSharedWorkerVersion(event.newValue)) + this._emitStatus({ error: false, category: StatusCategory.PNSharedWorkerUpdatedCategory }); + }); } private handleWorkerEvent(event: MessageEvent) { @@ -422,7 +449,12 @@ export class SubscriptionWorkerMiddleware implements Transport { } else if (data.type === 'shared-worker-ping') { const { subscriptionKey, clientIdentifier } = this.configuration; - this.scheduleEventPost({ type: 'client-pong', subscriptionKey, clientIdentifier }); + this.scheduleEventPost({ + type: 'client-pong', + subscriptionKey, + clientIdentifier, + workerLogLevel: this.configuration.workerLogLevel, + }); } else if (data.type === 'request-process-success' || data.type === 'request-process-error') { if (this.callbacks!.has(data.identifier)) { const { resolve, reject } = this.callbacks!.get(data.identifier)!; @@ -560,4 +592,30 @@ export class SubscriptionWorkerMiddleware implements Transport { return new PubNubAPIError(message, category, 0, new Error(message)); } + + /** + * Check whether current subscription `SharedWorker` version should be announced or not. + * + * @returns `true` if local storage is empty (only newer version will add value) or stored version is smaller than + * current. + */ + private shouldAnnounceNewerSharedWorkerVersionAvailability() { + const version = localStorage.getItem('PNSubscriptionSharedWorkerVersion'); + if (!version) return true; + + return !this.isNewerSharedWorkerVersion(version); + } + + /** + * Check whether current subscription `SharedWorker` version should be announced or not. + * + * @param version - Stored (received on init or event) version of subscription shared worker. + * @returns `true` if provided `version` is newer than current client version. + */ + private isNewerSharedWorkerVersion(version: string) { + const [currentMajor, currentMinor, currentPatch] = this.configuration.sdkVersion.split('.').map(Number); + const [storedMajor, storedMinor, storedPatch] = version.split('.').map(Number); + + return storedMajor > currentMajor || storedMinor > currentMinor || storedPatch > currentPatch; + } } diff --git a/src/transport/subscription-worker/subscription-worker-types.ts b/src/transport/subscription-worker/subscription-worker-types.ts new file mode 100644 index 000000000..ad8c83bfd --- /dev/null +++ b/src/transport/subscription-worker/subscription-worker-types.ts @@ -0,0 +1,341 @@ +// -------------------------------------------------------- +// ------------------------ Types ------------------------- +// -------------------------------------------------------- +// region Types +// region Client-side + +import { TransportRequest } from '../../core/types/transport-request'; +import { LogLevel, LogMessage } from '../../core/interfaces/logger'; +import { Payload } from '../../core/types/api'; + +/** + * Basic information for client and request group identification. + */ +type BasicEvent = { + /** + * Unique PubNub SDK client identifier for which setup is done. + */ + clientIdentifier: string; + + /** + * Subscribe REST API access key. + */ + subscriptionKey: string; + + /** + * Minimum messages log level which should be passed to the Subscription worker logger. + */ + workerLogLevel: LogLevel; + + /** + * Interval at which Shared Worker should check whether PubNub instances which used it still active or not. + */ + workerOfflineClientsCheckInterval?: number; + + /** + * Whether `leave` request should be sent for _offline_ PubNub client or not. + */ + workerUnsubscribeOfflineClients?: boolean; +}; + +/** + * PubNub client registration event. + */ +export type RegisterEvent = BasicEvent & { + type: 'client-register'; + + /** + * Unique identifier of the user for which PubNub SDK client has been created. + */ + userId: string; + + /** + * How often the client will announce itself to server. The value is in seconds. + * + * @default `not set` + */ + heartbeatInterval?: number; + + /** + * Specific PubNub client instance communication port. + */ + port?: MessagePort; +}; + +/** + * PubNub client update event. + */ +export type UpdateEvent = BasicEvent & { + type: 'client-update'; + + /** + * `userId` currently used by the client. + */ + userId: string; + + /** + * How often the client will announce itself to server. The value is in seconds. + * + * @default `not set` + */ + heartbeatInterval?: number; + + /** + * Access token which is used to access provided list of channels and channel groups. + * + * **Note:** Value can be missing, but it shouldn't reset it in the state. + */ + accessToken?: string; + + /** + * Pre-processed access token (If set). + * + * **Note:** Value can be missing, but it shouldn't reset it in the state. + */ + preProcessedToken?: { token: string; expiration: number }; +}; + +/** + * Send HTTP request event. + * + * Request from Web Worker to schedule {@link Request} using provided {@link SendRequestSignal#request|request} data. + */ +export type SendRequestEvent = BasicEvent & { + type: 'send-request'; + + /** + * Instruction to construct actual {@link Request}. + */ + request: TransportRequest; + + /** + * Pre-processed access token (If set). + * + * **Note:** Value can be missing, but it shouldn't reset it in the state. + */ + preProcessedToken?: { token: string; expiration: number }; +}; + +/** + * Cancel HTTP request event. + */ +export type CancelRequestEvent = BasicEvent & { + type: 'cancel-request'; + + /** + * Identifier of request which should be cancelled. + */ + identifier: string; +}; + +/** + * Client response on PING request. + */ +export type PongEvent = BasicEvent & { + type: 'client-pong'; +}; + +/** + * PubNub client disconnection event. + * + * On disconnection will be cleared subscription/heartbeat state and active backup heartbeat timer. + */ +export type DisconnectEvent = BasicEvent & { + type: 'client-disconnect'; +}; + +/** + * PubNub client remove registration event. + * + * On registration removal ongoing long-long poll request will be cancelled. + */ +export type UnRegisterEvent = BasicEvent & { + type: 'client-unregister'; +}; + +/** + * List of known events from the PubNub Core. + */ +export type ClientEvent = + | RegisterEvent + | UpdateEvent + | PongEvent + | SendRequestEvent + | CancelRequestEvent + | DisconnectEvent + | UnRegisterEvent; +// endregion + +// region Subscription Worker +/** + * Shared subscription worker connected event. + * + * Event signal shared worker client that worker can be used. + */ +export type SharedWorkerConnected = { + type: 'shared-worker-connected'; +}; + +/** + * Request processing error. + * + * Object may include either service error response or client-side processing error object. + */ +export type RequestSendingError = { + type: 'request-process-error'; + + /** + * Receiving PubNub client unique identifier. + */ + clientIdentifier: string; + + /** + * Failed request identifier. + */ + identifier: string; + + /** + * Url which has been used to perform request. + */ + url: string; + + /** + * Service error response. + */ + response?: RequestSendingSuccess['response']; + + /** + * Client side request processing error. + */ + error?: { + /** + * Name of error object which has been received. + */ + name: string; + + /** + * Available client-side errors. + */ + type: 'NETWORK_ISSUE' | 'ABORTED' | 'TIMEOUT'; + + /** + * Triggered error message. + */ + message: string; + }; +}; + +/** + * Request processing success. + */ +export type RequestSendingSuccess = { + type: 'request-process-success'; + + /** + * Receiving PubNub client unique identifier. + */ + clientIdentifier: string; + + /** + * Processed request identifier. + */ + identifier: string; + + /** + * Url which has been used to perform request. + */ + url: string; + + /** + * Service success response. + */ + response: { + /** + * Received {@link RequestSendingSuccess#response.body|body} content type. + */ + contentType: string; + + /** + * Received {@link RequestSendingSuccess#response.body|body} content length. + */ + contentLength: number; + + /** + * Response headers key / value pairs. + */ + headers: Record; + + /** + * Response status code. + */ + status: number; + + /** + * Service response. + */ + body?: ArrayBuffer; + }; +}; + +/** + * Request processing results. + */ +export type RequestSendingResult = RequestSendingError | RequestSendingSuccess; + +/** + * Send message to debug console. + */ +export type SharedWorkerConsoleLog = { + type: 'shared-worker-console-log'; + + /** + * Message which should be printed into the console. + */ + message: Payload; +}; +/** + * Send message to debug console. + */ +export type SharedWorkerConsoleDir = { + type: 'shared-worker-console-dir'; + + /** + * Message which should be printed into the console before {@link data}. + */ + message?: string; + + /** + * Data which should be printed into the console. + */ + data: Payload; +}; + +/** + * Shared worker console output request. + */ +export type SharedWorkerConsole = SharedWorkerConsoleLog | SharedWorkerConsoleDir; + +/** + * Shared worker client ping request. + * + * Ping used to discover disconnected PubNub instances. + */ +export type SharedWorkerPing = { + type: 'shared-worker-ping'; +}; + +/** + * List of known events from the PubNub Subscription Service Worker. + */ +export type SubscriptionWorkerEvent = + | SharedWorkerConnected + | SharedWorkerConsole + | SharedWorkerPing + | RequestSendingResult; + +/** + * Logger's message type definition. + */ +export type ClientLogMessage = Omit; + +// endregion diff --git a/src/transport/subscription-worker/subscription-worker.ts b/src/transport/subscription-worker/subscription-worker.ts index 4aeecbc8c..7331fca34 100644 --- a/src/transport/subscription-worker/subscription-worker.ts +++ b/src/transport/subscription-worker/subscription-worker.ts @@ -8,544 +8,11 @@ * @internal */ -import { TransportMethod, TransportRequest } from '../../core/types/transport-request'; -import { TransportResponse } from '../../core/types/transport-response'; +import { SubscribeRequestsManager } from './components/subscribe-requests-manager'; +import { HeartbeatRequestsManager } from './components/heartbeat-requests-manager'; +import { PubNubClientsManager } from './components/pubnub-clients-manager'; +import { ClientEvent } from './subscription-worker-types'; import uuidGenerator from '../../core/components/uuid'; -import { Payload, Query } from '../../core/types/api'; - -// -------------------------------------------------------- -// ------------------------ Types ------------------------- -// -------------------------------------------------------- -// region Types -// region Client-side - -/** - * Basic information for client and request group identification. - */ -type BasicEvent = { - /** - * Unique PubNub SDK client identifier for which setup is done. - */ - clientIdentifier: string; - - /** - * Subscribe REST API access key. - */ - subscriptionKey: string; - - /** - * Interval at which Shared Worker should check whether PubNub instances which used it still active or not. - */ - workerOfflineClientsCheckInterval?: number; - - /** - * Whether `leave` request should be sent for _offline_ PubNub client or not. - */ - workerUnsubscribeOfflineClients?: boolean; - - /** - * Whether verbose logging should be enabled for `Subscription` worker should print debug messages or not. - */ - workerLogVerbosity?: boolean; -}; - -/** - * PubNub client registration event. - */ -export type RegisterEvent = BasicEvent & { - type: 'client-register'; - - /** - * Unique identifier of the user for which PubNub SDK client has been created. - */ - userId: string; - - /** - * How often the client will announce itself to server. The value is in seconds. - * - * @default `not set` - */ - heartbeatInterval?: number; - - /** - * Specific PubNub client instance communication port. - */ - port?: MessagePort; -}; - -/** - * PubNub client update event. - */ -export type UpdateEvent = BasicEvent & { - type: 'client-update'; - - /** - * `userId` currently used by the client. - */ - userId: string; - - /** - * How often the client will announce itself to server. The value is in seconds. - * - * @default `not set` - */ - heartbeatInterval?: number; - - /** - * Access token which is used to access provided list of channels and channel groups. - * - * **Note:** Value can be missing, but it shouldn't reset it in the state. - */ - accessToken?: string; - - /** - * Pre-processed access token (If set). - * - * **Note:** Value can be missing, but it shouldn't reset it in the state. - */ - preProcessedToken?: PubNubClientState['accessToken']; -}; - -/** - * Send HTTP request event. - * - * Request from Web Worker to schedule {@link Request} using provided {@link SendRequestSignal#request|request} data. - */ -export type SendRequestEvent = BasicEvent & { - type: 'send-request'; - - /** - * Instruction to construct actual {@link Request}. - */ - request: TransportRequest; - - /** - * Pre-processed access token (If set). - */ - preProcessedToken?: PubNubClientState['accessToken']; -}; - -/** - * Cancel HTTP request event. - */ -export type CancelRequestEvent = BasicEvent & { - type: 'cancel-request'; - - /** - * Identifier of request which should be cancelled. - */ - identifier: string; -}; - -/** - * Client response on PING request. - */ -export type PongEvent = BasicEvent & { - type: 'client-pong'; -}; - -/** - * PubNub client remove registration event. - * - * On registration removal ongoing long-long poll request will be cancelled. - */ -export type UnRegisterEvent = BasicEvent & { - type: 'client-unregister'; -}; - -/** - * List of known events from the PubNub Core. - */ -export type ClientEvent = - | RegisterEvent - | UpdateEvent - | PongEvent - | SendRequestEvent - | CancelRequestEvent - | UnRegisterEvent; -// endregion - -// region Subscription Worker -/** - * Shared subscription worker connected event. - * - * Event signal shared worker client that worker can be used. - */ -export type SharedWorkerConnected = { - type: 'shared-worker-connected'; -}; - -/** - * Request processing error. - * - * Object may include either service error response or client-side processing error object. - */ -export type RequestSendingError = { - type: 'request-process-error'; - - /** - * Receiving PubNub client unique identifier. - */ - clientIdentifier: string; - - /** - * Failed request identifier. - */ - identifier: string; - - /** - * Url which has been used to perform request. - */ - url: string; - - /** - * Service error response. - */ - response?: RequestSendingSuccess['response']; - - /** - * Client side request processing error. - */ - error?: { - /** - * Name of error object which has been received. - */ - name: string; - - /** - * Available client-side errors. - */ - type: 'NETWORK_ISSUE' | 'ABORTED' | 'TIMEOUT'; - - /** - * Triggered error message. - */ - message: string; - }; -}; - -/** - * Request processing success. - */ -export type RequestSendingSuccess = { - type: 'request-process-success'; - - /** - * Receiving PubNub client unique identifier. - */ - clientIdentifier: string; - - /** - * Processed request identifier. - */ - identifier: string; - - /** - * Url which has been used to perform request. - */ - url: string; - - /** - * Service success response. - */ - response: { - /** - * Received {@link RequestSendingSuccess#response.body|body} content type. - */ - contentType: string; - - /** - * Received {@link RequestSendingSuccess#response.body|body} content length. - */ - contentLength: number; - - /** - * Response headers key / value pairs. - */ - headers: Record; - - /** - * Response status code. - */ - status: number; - - /** - * Service response. - */ - body?: ArrayBuffer; - }; -}; - -/** - * Request processing results. - */ -export type RequestSendingResult = RequestSendingError | RequestSendingSuccess; - -/** - * Send message to debug console. - */ -export type SharedWorkerConsoleLog = { - type: 'shared-worker-console-log'; - - /** - * Message which should be printed into the console. - */ - message: Payload; -}; -/** - * Send message to debug console. - */ -export type SharedWorkerConsoleDir = { - type: 'shared-worker-console-dir'; - - /** - * Message which should be printed into the console before {@link data}. - */ - message?: string; - - /** - * Data which should be printed into the console. - */ - data: Payload; -}; - -/** - * Shared worker console output request. - */ -export type SharedWorkerConsole = SharedWorkerConsoleLog | SharedWorkerConsoleDir; - -/** - * Shared worker client ping request. - * - * Ping used to discover disconnected PubNub instances. - */ -export type SharedWorkerPing = { - type: 'shared-worker-ping'; -}; - -/** - * List of known events from the PubNub Subscription Service Worker. - */ -export type SubscriptionWorkerEvent = - | SharedWorkerConnected - | SharedWorkerConsole - | SharedWorkerPing - | RequestSendingResult; - -/** - * PubNub client state representation in Shared Worker. - */ -type PubNubClientState = { - /** - * Unique PubNub client identifier. - */ - clientIdentifier: string; - - /** - * Subscribe REST API access key. - */ - subscriptionKey: string; - - /** - * Unique identifier of the user currently configured for the PubNub client. - */ - userId: string; - - /** - * Authorization key or access token which is used to access provided list of - * {@link subscription.channels|channels} and {@link subscription.channelGroups|channelGroups}. - */ - authKey?: string; - - /** - * Aggregateable {@link authKey} representation. - * - * Representation based on information stored in `resources`, `patterns`, and `authorized_uuid`. - */ - accessToken?: { - token: string; - expiration: number; - }; - - /** - * Origin which is used to access PubNub REST API. - */ - origin?: string; - - /** - * PubNub JS SDK identification string. - */ - pnsdk?: string; - - /** - * How often the client will announce itself to server. The value is in seconds. - * - * @default `not set` - */ - heartbeatInterval?: number; - - /** - * Whether instance registered for the first time or not. - */ - newlyRegistered: boolean; - - /** - * Interval at which Shared Worker should check whether PubNub instances which used it still active or not. - */ - offlineClientsCheckInterval?: number; - - /** - * Whether `leave` request should be sent for _offline_ PubNub client or not. - */ - unsubscribeOfflineClients?: boolean; - - /** - * Whether client should log Shared Worker logs or not. - */ - workerLogVerbosity?: boolean; - - /** - * Last time when PING request has been sent. - */ - lastPingRequest?: number; - - /** - * Last time when PubNub client respond with PONG event. - */ - lastPongEvent?: number; - - /** - * Current subscription session information. - * - * **Note:** Information updated each time when PubNub client instance schedule `subscribe` or - * `unsubscribe` requests. - */ - subscription?: { - /** - * Date time when subscription object has been updated. - */ - refreshTimestamp: number; - - /** - * Subscription REST API uri path. - * - * **Note:** Keeping it for faster check whether client state should be updated or not. - */ - path: string; - - /** - * Channel groups list representation from request query parameters. - * - * **Note:** Keeping it for faster check whether client state should be updated or not. - */ - channelGroupQuery: string; - - /** - * List of channels used in current subscription session. - */ - channels: string[]; - - /** - * List of channel groups used in current subscription session. - */ - channelGroups: string[]; - - /** - * Timetoken which used has been used with previous subscription session loop. - */ - previousTimetoken: string; - - /** - * Timetoken which used in current subscription session loop. - */ - timetoken: string; - - /** - * Timetoken region which used in current subscription session loop. - */ - region?: string; - - /** - * List of channel and / or channel group names for which state has been assigned. - * - * Information used during client information update to identify entries which should be removed. - */ - objectsWithState: string[]; - - /** - * Subscribe request which has been emitted by PubNub client. - * - * Value will be reset when current request processing completed or client "disconnected" (not interested in - * real-time updates). - */ - request?: TransportRequest; - - /** - * Identifier of subscribe request which has been actually sent by Service Worker. - * - * **Note:** Value not set if client not interested in any real-time updates. - */ - serviceRequestId?: string; - - /** - * Real-time events filtering expression. - */ - filterExpression?: string; - }; - - heartbeat?: { - /** - * Previous heartbeat send event. - */ - heartbeatEvent?: SendRequestEvent; - - /** - * List of channels for which user's presence has been announced by the PubNub client. - */ - channels: string[]; - - /** - * List of channel groups for which user's presence has been announced by the PubNub client. - */ - channelGroups: string[]; - - /** - * Presence state associated with user at specified list of channels and groups. - * - * Per-channel/group state associated with specific user. - */ - presenceState?: Record; - - /** - * Backup presence heartbeat loop managed by the `SharedWorker`. - */ - loop?: { - /** - * Heartbeat timer. - * - * Timer which is started with first heartbeat request and repeat inside SharedWorker to bypass browser's - * timers throttling. - * - * **Note:** Timer will be restarted each time when core client request to send a request (still "alive"). - */ - timer: ReturnType; - - /** - * Interval which has been used for the timer. - */ - heartbeatInterval: number; - - /** - * Timestamp when time has been started. - * - * **Note:** Information needed to compute active timer restart with new interval value. - */ - startTimestamp: number; - }; - }; -}; -// endregion -// endregion // -------------------------------------------------------- // ------------------- Service Worker --------------------- @@ -554,128 +21,15 @@ type PubNubClientState = { declare const self: SharedWorkerGlobalScope; -/** - * Aggregation timer timeout. - * - * Timeout used by the timer to postpone `handleSendSubscribeRequestEvent` function call and let other clients for - * same subscribe key send next subscribe loop request (to make aggregation more efficient). - */ -const subscribeAggregationTimeout = 50; - -/** - * Map of clients aggregation keys to the started aggregation timeout timers with client and event information. - */ -const aggregationTimers: Map = new Map(); - -// region State -/** - * Per-subscription key map of "offline" clients detection timeouts. - */ -const pingTimeouts: { [subscriptionKey: string]: number | undefined } = {}; - /** * Unique shared worker instance identifier. */ const sharedWorkerIdentifier = uuidGenerator.createUUID(); -/** - * Map of identifiers, scheduled by the Service Worker, to their abort controllers. - * - * **Note:** Because of message-based nature of interaction it will be impossible to pass actual {@link AbortController} - * to the transport provider code. - */ -const abortControllers: Map = new Map(); - -/** - * Map of PubNub client identifiers to their state in the current Service Worker. - */ -const pubNubClients: Record = {}; - -/** - * Per-subscription key list of PubNub client state. - */ -const pubNubClientsBySubscriptionKey: { [subscriptionKey: string]: PubNubClientState[] | undefined } = {}; - -/** - * Per-subscription key map of heartbeat request configurations recently used for user. - */ -const serviceHeartbeatRequests: { - [subscriptionKey: string]: - | { - [userId: string]: - | { - createdByActualRequest: boolean; - channels: string[]; - channelGroups: string[]; - timestamp: number; - clientIdentifier?: string; - response?: [Response, ArrayBuffer]; - } - | undefined; - } - | undefined; -} = {}; - -/** - * Per-subscription key presence state associated with unique user identifiers with which {@link pubNubClients|clients} - * scheduled subscription request. - */ -const presenceState: { - [subscriptionKey: string]: { [userId: string]: Record | undefined } | undefined; -} = {}; - -/** - * Per-subscription key map of client identifiers to the Shared Worker {@link MessagePort}. - * - * Shared Worker {@link MessagePort} represent specific PubNub client which connected to the Shared Worker. - */ -const sharedWorkerClients: { - [subscriptionKey: string]: { [clientId: string]: MessagePort | undefined } | undefined; -} = {}; - -/** - * List of ongoing subscription requests. - * - * **Node:** Identifiers differ from request identifiers received in {@link SendRequestEvent} object. - */ -const serviceRequests: { - [requestId: string]: { - /** - * Unique active request identifier. - */ - requestId: string; +const clientsManager = new PubNubClientsManager(sharedWorkerIdentifier); +const subscriptionRequestsManager = new SubscribeRequestsManager(clientsManager); +const heartbeatRequestsManager = new HeartbeatRequestsManager(clientsManager); - /** - * Timetoken which is used for subscription loop. - */ - timetoken: string; - - /** - * Timetoken region which is used for subscription loop. - */ - region?: string; - - /** - * Timetoken override which is used after initial subscription to catch up on previous messages. - */ - timetokenOverride?: string; - - /** - * Timetoken region override which is used after initial subscription to catch up on previous messages. - */ - regionOverride?: string; - - /** - * List of channels used in current subscription session. - */ - channels: string[]; - - /** - * List of channel groups used in current subscription session. - */ - channelGroups: string[]; - }; -} = {}; // endregion // -------------------------------------------------------- @@ -691,2136 +45,15 @@ const serviceRequests: { * @param event - Remote `SharedWorker` client connection event. */ self.onconnect = (event) => { - consoleLog('New PubNub Client connected to the Subscription Shared Worker.'); - event.ports.forEach((receiver) => { receiver.start(); receiver.onmessage = (event: MessageEvent) => { - // Ignoring unknown event payloads. - if (!validateEventPayload(event)) return; - const data = event.data as ClientEvent; - - if (data.type === 'client-register') { - // Appending information about messaging port for responses. - data.port = receiver; - registerClientIfRequired(data); - - consoleLog(`Client '${data.clientIdentifier}' registered with '${sharedWorkerIdentifier}' shared worker`); - } else if (data.type === 'client-update') updateClientInformation(data); - else if (data.type === 'client-unregister') unRegisterClient(data); - else if (data.type === 'client-pong') handleClientPong(data); - else if (data.type === 'send-request') { - if (data.request.path.startsWith('/v2/subscribe')) { - const changedSubscription = updateClientSubscribeStateIfRequired(data); - - const client = pubNubClients[data.clientIdentifier]; - if (client) { - // Check whether there are more clients which may schedule next subscription loop and they need to be - // aggregated or not. - const timerIdentifier = aggregateTimerId(client); - let enqueuedClients: [PubNubClientState, SendRequestEvent][] = []; - - if (aggregationTimers.has(timerIdentifier)) enqueuedClients = aggregationTimers.get(timerIdentifier)![0]; - enqueuedClients.push([client, data]); - - // Clear existing aggregation timer if subscription list changed. - if (aggregationTimers.has(timerIdentifier) && changedSubscription) { - clearTimeout(aggregationTimers.get(timerIdentifier)![1]); - aggregationTimers.delete(timerIdentifier); - } - - // Check whether we need to start new aggregation timer or not. - if (!aggregationTimers.has(timerIdentifier)) { - const aggregationTimer = setTimeout(() => { - handleSendSubscribeRequestEventForClients(enqueuedClients, data); - aggregationTimers.delete(timerIdentifier); - }, subscribeAggregationTimeout); - - aggregationTimers.set(timerIdentifier, [enqueuedClients, aggregationTimer]); - } - } - } else if (data.request.path.endsWith('/heartbeat')) { - updateClientHeartbeatState(data); - handleHeartbeatRequestEvent(data); - } else handleSendLeaveRequestEvent(data); - } else if (data.type === 'cancel-request') handleCancelRequestEvent(data); + if (data.type === 'client-register') clientsManager.createClient(data, receiver); }; receiver.postMessage({ type: 'shared-worker-connected' }); }); }; - -/** - * Handle aggregated clients request to send subscription request. - * - * @param clients - List of aggregated clients which would like to send subscription requests. - * @param event - Subscription event details. - */ -const handleSendSubscribeRequestEventForClients = ( - clients: [PubNubClientState, SendRequestEvent][], - event: SendRequestEvent, -) => { - const requestOrId = subscribeTransportRequestFromEvent(event); - const client = pubNubClients[event.clientIdentifier]; - - if (!client) return; - - // Getting rest of aggregated clients. - clients = clients.filter((aggregatedClient) => aggregatedClient[0].clientIdentifier !== client.clientIdentifier); - handleSendSubscribeRequestForClient(client, event, requestOrId, true); - clients.forEach(([aggregatedClient, clientEvent]) => - handleSendSubscribeRequestForClient(aggregatedClient, clientEvent, requestOrId, false), - ); -}; - -/** - * Handle subscribe request by single client. - * - * @param client - Client which processes `request`. - * @param event - Subscription event details. - * @param requestOrId - New aggregated request object or its identifier (if already scheduled). - * @param requestOrigin - Whether `client` is the one who triggered subscribe request or not. - */ -const handleSendSubscribeRequestForClient = ( - client: PubNubClientState, - event: SendRequestEvent, - requestOrId: ReturnType, - requestOrigin: boolean, -) => { - let isInitialSubscribe = false; - if (!requestOrigin && typeof requestOrId !== 'string') requestOrId = requestOrId.identifier; - - if (client.subscription) isInitialSubscribe = client.subscription.timetoken === '0'; - - if (typeof requestOrId === 'string') { - const scheduledRequest = serviceRequests[requestOrId]; - - if (client) { - if (client.subscription) { - // Updating client timetoken information. - client.subscription.refreshTimestamp = Date.now(); - client.subscription.timetoken = scheduledRequest.timetoken; - client.subscription.region = scheduledRequest.region; - client.subscription.serviceRequestId = requestOrId; - } - - if (!isInitialSubscribe) return; - - const body = new TextEncoder().encode( - `{"t":{"t":"${scheduledRequest.timetoken}","r":${scheduledRequest.region ?? '0'}},"m":[]}`, - ); - const headers = new Headers({ - 'Content-Type': 'text/javascript; charset="UTF-8"', - 'Content-Length': `${body.length}`, - }); - const response = new Response(body, { status: 200, headers }); - const result = requestProcessingSuccess([response, body]); - result.url = `${event.request.origin}${event.request.path}`; - result.clientIdentifier = event.clientIdentifier; - result.identifier = event.request.identifier; - - publishClientEvent(client, result); - } - - return; - } - - if (event.request.cancellable) abortControllers.set(requestOrId.identifier, new AbortController()); - const scheduledRequest = serviceRequests[requestOrId.identifier]; - const { timetokenOverride, regionOverride } = scheduledRequest; - const expectingInitialSubscribeResponse = scheduledRequest.timetoken === '0'; - - consoleLog(`'${Object.keys(serviceRequests).length}' subscription request currently active.`); - - // Notify about request processing start. - for (const client of clientsForRequest(requestOrId.identifier)) - consoleLog({ messageType: 'network-request', message: requestOrId as unknown as Payload }, client); - - sendRequest( - requestOrId, - () => clientsForRequest(requestOrId.identifier), - (clients, fetchRequest, response) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, response, event.request); - - // Clean up scheduled request and client references to it. - markRequestCompleted(clients, requestOrId.identifier); - }, - (clients, fetchRequest, error) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, null, event.request, requestProcessingError(error)); - - // Clean up scheduled request and client references to it. - markRequestCompleted(clients, requestOrId.identifier); - }, - (response) => { - let serverResponse = response; - if (expectingInitialSubscribeResponse && timetokenOverride && timetokenOverride !== '0') - serverResponse = patchInitialSubscribeResponse(serverResponse, timetokenOverride, regionOverride); - - return serverResponse; - }, - ); -}; - -const patchInitialSubscribeResponse = ( - serverResponse: [Response, ArrayBuffer], - timetoken?: string, - region?: string, -): [Response, ArrayBuffer] => { - if (timetoken === undefined || timetoken === '0' || serverResponse[0].status >= 400) { - return serverResponse; - } - - let json: { t: { t: string; r: number }; m: Record[] }; - const response = serverResponse[0]; - let decidedResponse = response; - let body = serverResponse[1]; - - try { - json = JSON.parse(new TextDecoder().decode(body)); - } catch (error) { - consoleLog(`Subscribe response parse error: ${error}`); - return serverResponse; - } - - // Replace server-provided timetoken. - json.t.t = timetoken; - if (region) json.t.r = parseInt(region, 10); - - try { - body = new TextEncoder().encode(JSON.stringify(json)).buffer; - if (body.byteLength) { - const headers = new Headers(response.headers); - headers.set('Content-Length', `${body.byteLength}`); - - // Create a new response with the original response options and modified headers - decidedResponse = new Response(body, { - status: response.status, - statusText: response.statusText, - headers: headers, - }); - } - } catch (error) { - consoleLog(`Subscribe serialization error: ${error}`); - return serverResponse; - } - - return body.byteLength > 0 ? [decidedResponse, body] : serverResponse; -}; - -/** - * Handle client heartbeat request. - * - * @param event - Heartbeat event details. - * @param [actualRequest] - Whether handling actual request from the core-part of the client and not backup heartbeat in - * the `SharedWorker`. - * @param [outOfOrder] - Whether handling request which is sent on irregular basis (setting update). - */ -const handleHeartbeatRequestEvent = (event: SendRequestEvent, actualRequest = true, outOfOrder = false) => { - const client = pubNubClients[event.clientIdentifier]; - const request = heartbeatTransportRequestFromEvent(event, actualRequest, outOfOrder); - - if (!client) return; - const heartbeatRequestKey = `${client.userId}_${clientAggregateAuthKey(client) ?? ''}`; - const hbRequestsBySubscriptionKey = serviceHeartbeatRequests[client.subscriptionKey]; - const hbRequests = (hbRequestsBySubscriptionKey ?? {})[heartbeatRequestKey]; - - if (!request) { - let message = `Previous heartbeat request has been sent less than ${ - client.heartbeatInterval - } seconds ago. Skipping...`; - if (!client.heartbeat || (client.heartbeat.channels.length === 0 && client.heartbeat.channelGroups.length === 0)) - message = `${client.clientIdentifier} doesn't have subscriptions to non-presence channels. Skipping...`; - consoleLog(message, client); - - let response: Response | undefined; - let body: ArrayBuffer | undefined; - - // Pulling out previous response. - if (hbRequests && hbRequests.response) [response, body] = hbRequests.response; - - if (!response) { - body = new TextEncoder().encode('{ "status": 200, "message": "OK", "service": "Presence" }').buffer; - const headers = new Headers({ - 'Content-Type': 'text/javascript; charset="UTF-8"', - 'Content-Length': `${body.byteLength}`, - }); - - response = new Response(body, { status: 200, headers }); - } - - const result = requestProcessingSuccess([response, body!]); - result.url = `${event.request.origin}${event.request.path}`; - result.clientIdentifier = event.clientIdentifier; - result.identifier = event.request.identifier; - - publishClientEvent(client, result); - return; - } - - consoleLog(`Started heartbeat request.`, client); - - // Notify about request processing start. - for (const client of clientsForSendHeartbeatRequestEvent(event)) - consoleLog({ messageType: 'network-request', message: request as unknown as Payload }, client); - - sendRequest( - request, - () => [client], - (clients, fetchRequest, response) => { - if (hbRequests) hbRequests.response = response; - - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, response, event.request); - - // Stop heartbeat timer on client error status codes. - if (response[0].status >= 400 && response[0].status < 500) stopHeartbeatTimer(client); - }, - (clients, fetchRequest, error) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, null, event.request, requestProcessingError(error)); - }, - ); - - // Start "backup" heartbeat timer. - if (!outOfOrder) startHeartbeatTimer(client); -}; - -/** - * Handle client request to leave request. - * - * @param data - Leave event details. - * @param [invalidatedClient] - Specific client to handle leave request. - * @param [invalidatedClientServiceRequestId] - Identifier of the service request ID for which the invalidated - * client waited for a subscribe response. - */ -const handleSendLeaveRequestEvent = ( - data: SendRequestEvent, - invalidatedClient?: PubNubClientState, - invalidatedClientServiceRequestId?: string, -) => { - const client = invalidatedClient ?? pubNubClients[data.clientIdentifier]; - const request = leaveTransportRequestFromEvent(data, invalidatedClient); - - if (!client) return; - - // Clean up client subscription information if there is no more channels / groups to use. - const { subscription, heartbeat } = client; - const serviceRequestId = invalidatedClientServiceRequestId ?? subscription?.serviceRequestId; - if (subscription && subscription.channels.length === 0 && subscription.channelGroups.length === 0) { - subscription.channelGroupQuery = ''; - subscription.path = ''; - subscription.previousTimetoken = '0'; - subscription.refreshTimestamp = Date.now(); - subscription.timetoken = '0'; - delete subscription.region; - delete subscription.serviceRequestId; - delete subscription.request; - } - - if (serviceHeartbeatRequests[client.subscriptionKey]) { - if (heartbeat && heartbeat.channels.length === 0 && heartbeat.channelGroups.length === 0) { - const hbRequestsBySubscriptionKey = (serviceHeartbeatRequests[client.subscriptionKey] ??= {}); - const heartbeatRequestKey = `${client.userId}_${clientAggregateAuthKey(client) ?? ''}`; - - if ( - hbRequestsBySubscriptionKey[heartbeatRequestKey] && - hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier === client.clientIdentifier - ) - delete hbRequestsBySubscriptionKey[heartbeatRequestKey]!.clientIdentifier; - - delete heartbeat.heartbeatEvent; - stopHeartbeatTimer(client); - } - } - - if (!request) { - const body = new TextEncoder().encode('{"status": 200, "action": "leave", "message": "OK", "service":"Presence"}'); - const headers = new Headers({ - 'Content-Type': 'text/javascript; charset="UTF-8"', - 'Content-Length': `${body.length}`, - }); - const response = new Response(body, { status: 200, headers }); - const result = requestProcessingSuccess([response, body]); - result.url = `${data.request.origin}${data.request.path}`; - result.clientIdentifier = data.clientIdentifier; - result.identifier = data.request.identifier; - - publishClientEvent(client, result); - return; - } - - consoleLog(`Started leave request.`, client); - - // Notify about request processing start. - for (const client of clientsForSendLeaveRequestEvent(data, invalidatedClient)) - consoleLog({ messageType: 'network-request', message: request as unknown as Payload }, client); - - sendRequest( - request, - () => [client], - (clients, fetchRequest, response) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, response, data.request); - }, - (clients, fetchRequest, error) => { - // Notify each PubNub client which awaited for response. - notifyRequestProcessingResult(clients, fetchRequest, null, data.request, requestProcessingError(error)); - }, - ); - - // Check whether there were active subscription with channels from this client or not. - if (serviceRequestId === undefined) return; - - // Update ongoing clients - const clients = clientsForRequest(serviceRequestId); - clients.forEach((client) => { - if (client && client.subscription) delete client.subscription.serviceRequestId; - }); - cancelRequest(serviceRequestId); - restartSubscribeRequestForClients(clients); -}; - -/** - * Handle cancel request event. - * - * Try cancel request if there is no other observers. - * - * @param event - Request cancellation event details. - */ -const handleCancelRequestEvent = (event: CancelRequestEvent) => { - const client = pubNubClients[event.clientIdentifier]; - if (!client || !client.subscription) return; - - const serviceRequestId = client.subscription.serviceRequestId; - if (!client || !serviceRequestId) return; - - // Unset awaited requests. - delete client.subscription.serviceRequestId; - if (client.subscription.request && client.subscription.request.identifier === event.identifier) { - delete client.subscription.request; - } - - cancelRequest(serviceRequestId); -}; -// endregion - -// -------------------------------------------------------- -// --------------------- Subscription --------------------- -// -------------------------------------------------------- -// region Subscription - -/** - * Try restart subscribe request for the list of clients. - * - * Subscribe restart will use previous timetoken information to schedule new subscription loop. - * - * **Note:** This function mimics behaviour when SharedWorker receives request from PubNub SDK. - * - * @param clients List of PubNub client states for which new aggregated request should be sent. - */ -const restartSubscribeRequestForClients = (clients: PubNubClientState[]) => { - let clientWithRequest: PubNubClientState | undefined; - let request: TransportRequest | undefined; - - for (const client of clients) { - if (client.subscription && client.subscription.request) { - request = client.subscription.request; - clientWithRequest = client; - break; - } - } - if (!request || !clientWithRequest) return; - - const sendRequest: SendRequestEvent = { - type: 'send-request', - clientIdentifier: clientWithRequest.clientIdentifier, - subscriptionKey: clientWithRequest.subscriptionKey, - request, - }; - - handleSendSubscribeRequestEventForClients([[clientWithRequest, sendRequest]], sendRequest); -}; -// endregion - -// -------------------------------------------------------- -// ------------------------ Common ------------------------ -// -------------------------------------------------------- -// region Common - -/** - * Process transport request. - * - * @param request - Transport request with required information for {@link Request} creation. - * @param getClients - Request completion PubNub client observers getter. - * @param success - Request success completion handler. - * @param failure - Request failure handler. - * @param responsePreProcess - Raw response pre-processing function which is used before calling handling callbacks. - */ -const sendRequest = ( - request: TransportRequest, - getClients: () => PubNubClientState[], - success: (clients: PubNubClientState[], fetchRequest: Request, response: [Response, ArrayBuffer]) => void, - failure: (clients: PubNubClientState[], fetchRequest: Request, error: unknown) => void, - responsePreProcess?: (response: [Response, ArrayBuffer]) => [Response, ArrayBuffer], -) => { - (async () => { - const fetchRequest = requestFromTransportRequest(request); - - Promise.race([ - fetch(fetchRequest, { - signal: abortControllers.get(request.identifier)?.signal, - keepalive: true, - }), - requestTimeoutTimer(request.identifier, request.timeout), - ]) - .then((response): Promise<[Response, ArrayBuffer]> | [Response, ArrayBuffer] => - response.arrayBuffer().then((buffer) => [response, buffer]), - ) - .then((response) => (responsePreProcess ? responsePreProcess(response) : response)) - .then((response) => { - const clients = getClients(); - if (clients.length === 0) return; - - success(clients, fetchRequest, response); - }) - .catch((error) => { - const clients = getClients(); - if (clients.length === 0) return; - - let fetchError = error; - - if (typeof error === 'string') { - const errorMessage = error.toLowerCase(); - fetchError = new Error(error); - - if (!errorMessage.includes('timeout') && errorMessage.includes('cancel')) fetchError.name = 'AbortError'; - } - - failure(clients, fetchRequest, fetchError); - }); - })(); -}; - -/** - * Cancel (abort) service request by ID. - * - * @param requestId - Unique identifier of request which should be cancelled. - */ -const cancelRequest = (requestId: string) => { - if (clientsForRequest(requestId).length === 0) { - const controller = abortControllers.get(requestId); - abortControllers.delete(requestId); - - // Clean up scheduled requests. - delete serviceRequests[requestId]; - - // Abort request if possible. - if (controller) controller.abort('Cancel request'); - } -}; - -/** - * Create request timeout timer. - * - * **Note:** Native Fetch API doesn't support `timeout` out-of-box and {@link Promise} used to emulate it. - * - * @param requestId - Unique identifier of request which will time out after {@link requestTimeout} seconds. - * @param requestTimeout - Number of seconds after which request with specified identifier will time out. - * - * @returns Promise which rejects after time out will fire. - */ -const requestTimeoutTimer = (requestId: string, requestTimeout: number) => - new Promise((_, reject) => { - const timeoutId = setTimeout(() => { - // Clean up. - abortControllers.delete(requestId); - clearTimeout(timeoutId); - - reject(new Error('Request timeout')); - }, requestTimeout * 1000); - }); - -/** - * Retrieve list of PubNub clients which is pending for service worker request completion. - * - * @param identifier - Identifier of the subscription request which has been scheduled by the Service Worker. - * - * @returns List of PubNub client state objects for Service Worker. - */ -const clientsForRequest = (identifier: string) => { - return Object.values(pubNubClients).filter( - (client): client is PubNubClientState => - client !== undefined && client.subscription !== undefined && client.subscription.serviceRequestId === identifier, - ); -}; - -/** - * Clean up PubNub client states from ongoing request. - * - * Reset requested and scheduled request information to make PubNub client "free" for next requests. - * - * @param clients - List of PubNub clients which awaited for scheduled request completion. - * @param requestId - Unique subscribe request identifier for which {@link clients} has been provided. - */ -const markRequestCompleted = (clients: PubNubClientState[], requestId: string) => { - delete serviceRequests[requestId]; - - clients.forEach((client) => { - if (client.subscription) { - delete client.subscription.request; - delete client.subscription.serviceRequestId; - } - }); -}; - -/** - * Creates a Request object from a given {@link TransportRequest} object. - * - * @param req - The {@link TransportRequest} object containing request information. - * - * @returns `Request` object generated from the {@link TransportRequest} object or `undefined` if no request - * should be sent. - */ -const requestFromTransportRequest = (req: TransportRequest): Request => { - let headers: Record | undefined = undefined; - const queryParameters = req.queryParameters; - let path = req.path; - - if (req.headers) { - headers = {}; - for (const [key, value] of Object.entries(req.headers)) headers[key] = value; - } - - if (queryParameters && Object.keys(queryParameters).length !== 0) - path = `${path}?${queryStringFromObject(queryParameters)}`; - - return new Request(`${req.origin!}${path}`, { - method: req.method, - headers, - redirect: 'follow', - }); -}; - -/** - * Construct transport request from send subscription request event. - * - * Update transport request to aggregate channels and groups if possible. - * - * @param event - Client's send subscription event request. - * - * @returns Final transport request or identifier from active request which will provide response to required - * channels and groups. - */ -const subscribeTransportRequestFromEvent = (event: SendRequestEvent): TransportRequest | string => { - const client = pubNubClients[event.clientIdentifier]!; - const subscription = client.subscription!; - const clients = clientsForSendSubscribeRequestEvent(subscription.timetoken, event); - const serviceRequestId = uuidGenerator.createUUID(); - const request = { ...event.request }; - let previousSubscribeTimetokenRefreshTimestamp: number | undefined; - let previousSubscribeTimetoken: string | undefined; - let previousSubscribeRegion: string | undefined; - - if (clients.length > 1) { - const activeRequestId = activeSubscriptionForEvent(clients, event); - - // Return identifier of the ongoing request. - if (activeRequestId) { - const scheduledRequest = serviceRequests[activeRequestId]; - const { channels, channelGroups } = client.subscription ?? { channels: [], channelGroups: [] }; - if ( - (channels.length > 0 ? includesStrings(scheduledRequest.channels, channels) : true) && - (channelGroups.length > 0 ? includesStrings(scheduledRequest.channelGroups, channelGroups) : true) - ) { - return activeRequestId; - } - } - - const state = (presenceState[client.subscriptionKey] ?? {})[client.userId]; - const aggregatedState: Record = {}; - const channelGroups = new Set(subscription.channelGroups); - const channels = new Set(subscription.channels); - - if (state && subscription.objectsWithState.length) { - subscription.objectsWithState.forEach((name) => { - const objectState = state[name]; - if (objectState) aggregatedState[name] = objectState; - }); - } - - for (const _client of clients) { - const { subscription: _subscription } = _client; - // Skip clients which doesn't have active subscription request. - if (!_subscription) continue; - - // Keep track of timetoken from previous call to use it for catchup after initial subscribe. - if (_subscription.timetoken) { - let shouldSetPreviousTimetoken = !previousSubscribeTimetoken; - if (!shouldSetPreviousTimetoken && _subscription.timetoken !== '0') { - if (previousSubscribeTimetoken === '0') shouldSetPreviousTimetoken = true; - else if (_subscription.timetoken < previousSubscribeTimetoken!) - shouldSetPreviousTimetoken = _subscription.refreshTimestamp > previousSubscribeTimetokenRefreshTimestamp!; - } - - if (shouldSetPreviousTimetoken) { - previousSubscribeTimetokenRefreshTimestamp = _subscription.refreshTimestamp; - previousSubscribeTimetoken = _subscription.timetoken; - previousSubscribeRegion = _subscription.region; - } - } - - _subscription.channelGroups.forEach(channelGroups.add, channelGroups); - _subscription.channels.forEach(channels.add, channels); - - const activeServiceRequestId = _subscription.serviceRequestId; - _subscription.serviceRequestId = serviceRequestId; - - // Set awaited service worker request identifier. - if (activeServiceRequestId && serviceRequests[activeServiceRequestId]) { - cancelRequest(activeServiceRequestId); - } - - if (!state) continue; - - _subscription.objectsWithState.forEach((name) => { - const objectState = state[name]; - - if (objectState && !aggregatedState[name]) aggregatedState[name] = objectState; - }); - } - - const serviceRequest = (serviceRequests[serviceRequestId] ??= { - requestId: serviceRequestId, - timetoken: (request.queryParameters!.tt as string) ?? '0', - channelGroups: [], - channels: [], - }); - - // Update request channels list (if required). - if (channels.size) { - serviceRequest.channels = Array.from(channels).sort(); - const pathComponents = request.path.split('/'); - pathComponents[4] = serviceRequest.channels.join(','); - request.path = pathComponents.join('/'); - } - - // Update request channel groups list (if required). - if (channelGroups.size) { - serviceRequest.channelGroups = Array.from(channelGroups).sort(); - request.queryParameters!['channel-group'] = serviceRequest.channelGroups.join(','); - } - - // Update request `state` (if required). - if (Object.keys(aggregatedState).length) request.queryParameters!['state'] = JSON.stringify(aggregatedState); - - // Update `auth` key (if required). - if (request.queryParameters && request.queryParameters.auth) { - const authKey = authKeyForAggregatedClientsRequest(clients); - if (authKey) request.queryParameters.auth = authKey; - } - } else { - serviceRequests[serviceRequestId] = { - requestId: serviceRequestId, - timetoken: (request.queryParameters!.tt as string) ?? '0', - channelGroups: subscription.channelGroups, - channels: subscription.channels, - }; - } - - if (serviceRequests[serviceRequestId]) { - if ( - request.queryParameters && - request.queryParameters.tt !== undefined && - request.queryParameters.tr !== undefined - ) { - serviceRequests[serviceRequestId].region = request.queryParameters.tr as string; - } - if ( - !serviceRequests[serviceRequestId].timetokenOverride || - (serviceRequests[serviceRequestId].timetokenOverride !== '0' && - previousSubscribeTimetoken && - previousSubscribeTimetoken !== '0') - ) { - serviceRequests[serviceRequestId].timetokenOverride = previousSubscribeTimetoken; - serviceRequests[serviceRequestId].regionOverride = previousSubscribeRegion; - } - } - - subscription.serviceRequestId = serviceRequestId; - request.identifier = serviceRequestId; - - const clientIds = clients - .reduce((identifiers: string[], { clientIdentifier }) => { - identifiers.push(clientIdentifier); - return identifiers; - }, []) - .join(', '); - - if (clientIds.length > 0) { - for (const _client of clients) - consoleDir(serviceRequests[serviceRequestId], `Started aggregated request for clients: ${clientIds}`, _client); - } - - return request; -}; - -/** - * Construct transport request from send heartbeat request event. - * - * Update transport request to aggregate channels and groups if possible. - * - * @param event - Client's send heartbeat event request. - * @param [actualRequest] - Whether handling actual request from the core-part of the client and not backup heartbeat in - * the `SharedWorker`. - * @param [outOfOrder] - Whether handling request which is sent on irregular basis (setting update). - * - * @returns Final transport request or identifier from active request which will provide response to required - * channels and groups. - */ -const heartbeatTransportRequestFromEvent = ( - event: SendRequestEvent, - actualRequest: boolean, - outOfOrder: boolean, -): TransportRequest | undefined => { - const client = pubNubClients[event.clientIdentifier]; - const clients = clientsForSendHeartbeatRequestEvent(event); - const request = { ...event.request }; - - if (!client || !client.heartbeat) return undefined; - - const hbRequestsBySubscriptionKey = (serviceHeartbeatRequests[client.subscriptionKey] ??= {}); - const heartbeatRequestKey = `${client.userId}_${clientAggregateAuthKey(client) ?? ''}`; - const channelGroupsForAnnouncement: string[] = [...client.heartbeat.channelGroups]; - const channelsForAnnouncement: string[] = [...client.heartbeat.channels]; - let aggregatedState: Record; - let failedPreviousRequest = false; - let aggregated: boolean; - - if (!hbRequestsBySubscriptionKey[heartbeatRequestKey]) { - hbRequestsBySubscriptionKey[heartbeatRequestKey] = { - createdByActualRequest: actualRequest, - channels: channelsForAnnouncement, - channelGroups: channelGroupsForAnnouncement, - clientIdentifier: client.clientIdentifier, - timestamp: Date.now(), - }; - aggregatedState = client.heartbeat.presenceState ?? {}; - aggregated = false; - } else { - const { createdByActualRequest, channels, channelGroups, response } = - hbRequestsBySubscriptionKey[heartbeatRequestKey]; - - // Allow out-of-order call from the client for heartbeat initiated by the `SharedWorker`. - if (!createdByActualRequest && actualRequest) { - hbRequestsBySubscriptionKey[heartbeatRequestKey].createdByActualRequest = true; - hbRequestsBySubscriptionKey[heartbeatRequestKey].timestamp = Date.now(); - outOfOrder = true; - } - - aggregatedState = client.heartbeat.presenceState ?? {}; - aggregated = - includesStrings(channels, channelsForAnnouncement) && - includesStrings(channelGroups, channelGroupsForAnnouncement); - if (response) failedPreviousRequest = response[0].status >= 400; - } - - // Find minimum heartbeat interval which maybe required to use. - let minimumHeartbeatInterval = client.heartbeatInterval!; - for (const client of clients) { - if (client.heartbeatInterval) - minimumHeartbeatInterval = Math.min(minimumHeartbeatInterval, client.heartbeatInterval); - } - - // Check whether multiple instance aggregate heartbeat and there is previous sender known. - // `clientIdentifier` maybe empty in case if client which triggered heartbeats before has been invalidated and new - // should handle heartbeat unconditionally. - if (aggregated && hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier) { - const expectedTimestamp = - hbRequestsBySubscriptionKey[heartbeatRequestKey].timestamp + minimumHeartbeatInterval * 1000; - const currentTimestamp = Date.now(); - - // Request should be sent if a previous attempt failed. - if (!outOfOrder && !failedPreviousRequest && currentTimestamp < expectedTimestamp) { - // Check whether it is too soon to send request or not. - const leeway = minimumHeartbeatInterval * 0.05 * 1000; - - // Leeway can't be applied if actual interval between heartbeat requests is smaller - // than 3 seconds which derived from the server's threshold. - if (minimumHeartbeatInterval - leeway <= 3 || expectedTimestamp - currentTimestamp > leeway) { - startHeartbeatTimer(client, true); - return undefined; - } - } - } - - delete hbRequestsBySubscriptionKey[heartbeatRequestKey]!.response; - hbRequestsBySubscriptionKey[heartbeatRequestKey]!.clientIdentifier = client.clientIdentifier; - - // Aggregate channels for similar clients which is pending for heartbeat. - for (const _client of clients) { - const { heartbeat } = _client; - if (heartbeat === undefined || _client.clientIdentifier === event.clientIdentifier) continue; - - // Append presence state from the client (will override previously set value if already set). - if (heartbeat.presenceState) aggregatedState = { ...aggregatedState, ...heartbeat.presenceState }; - - channelGroupsForAnnouncement.push( - ...heartbeat.channelGroups.filter((channel) => !channelGroupsForAnnouncement.includes(channel)), - ); - channelsForAnnouncement.push(...heartbeat.channels.filter((channel) => !channelsForAnnouncement.includes(channel))); - } - - hbRequestsBySubscriptionKey[heartbeatRequestKey].channels = channelsForAnnouncement; - hbRequestsBySubscriptionKey[heartbeatRequestKey].channelGroups = channelGroupsForAnnouncement; - if (!outOfOrder) hbRequestsBySubscriptionKey[heartbeatRequestKey].timestamp = Date.now(); - - // Remove presence state for objects which is not part of heartbeat. - for (const objectName in Object.keys(aggregatedState)) { - if (!channelsForAnnouncement.includes(objectName) && !channelGroupsForAnnouncement.includes(objectName)) - delete aggregatedState[objectName]; - } - // No need to try send request with empty list of channels and groups. - if (channelsForAnnouncement.length === 0 && channelGroupsForAnnouncement.length === 0) return undefined; - - // Update request channels list (if required). - if (channelsForAnnouncement.length || channelGroupsForAnnouncement.length) { - const pathComponents = request.path.split('/'); - pathComponents[6] = channelsForAnnouncement.length ? channelsForAnnouncement.join(',') : ','; - request.path = pathComponents.join('/'); - } - - // Update request channel groups list (if required). - if (channelGroupsForAnnouncement.length) - request.queryParameters!['channel-group'] = channelGroupsForAnnouncement.join(','); - - // Update request `state` (if required). - if (Object.keys(aggregatedState).length) request.queryParameters!['state'] = JSON.stringify(aggregatedState); - else delete request.queryParameters!['state']; - - // Update `auth` key (if required). - if (clients.length > 1 && request.queryParameters && request.queryParameters.auth) { - const aggregatedAuthKey = authKeyForAggregatedClientsRequest(clients); - if (aggregatedAuthKey) request.queryParameters.auth = aggregatedAuthKey; - } - - return request; -}; - -/** - * Construct transport request from send leave request event. - * - * Filter out channels and groups, which is still in use by other PubNub client instances from leave request. - * - * @param event - Client's sending leave event request. - * @param [invalidatedClient] - Invalidated PubNub client state. - * - * @returns Final transport request or `undefined` in case if there are no channels and groups for which request can be - * done. - */ -const leaveTransportRequestFromEvent = ( - event: SendRequestEvent, - invalidatedClient?: PubNubClientState, -): TransportRequest | undefined => { - const client = invalidatedClient ?? pubNubClients[event.clientIdentifier]; - const clients = clientsForSendLeaveRequestEvent(event, invalidatedClient); - let channelGroups = channelGroupsFromRequest(event.request); - let channels = channelsFromRequest(event.request); - const request = { ...event.request }; - - // Remove channels / groups from active client's subscription. - if (client && client.subscription) { - const { subscription } = client; - if (channels.length) { - subscription.channels = subscription.channels.filter((channel) => !channels.includes(channel)); - - // Modify cached request path. - const pathComponents = subscription.path.split('/'); - - if (pathComponents[4] !== ',') { - const pathChannels = pathComponents[4].split(',').filter((channel) => !channels.includes(channel)); - pathComponents[4] = pathChannels.length ? pathChannels.join(',') : ','; - subscription.path = pathComponents.join('/'); - } - } - if (channelGroups.length) { - subscription.channelGroups = subscription.channelGroups.filter((group) => !channelGroups.includes(group)); - - // Modify cached request path. - if (subscription.channelGroupQuery.length > 0) { - const queryChannelGroups = subscription.channelGroupQuery - .split(',') - .filter((group) => !channelGroups.includes(group)); - - subscription.channelGroupQuery = queryChannelGroups.length ? queryChannelGroups.join(',') : ''; - } - } - } - - // Remove channels / groups from client's presence heartbeat state. - if (client && client.heartbeat) { - const { heartbeat } = client; - if (channels.length) heartbeat.channels = heartbeat.channels.filter((channel) => !channels.includes(channel)); - if (channelGroups.length) - heartbeat.channelGroups = heartbeat.channelGroups.filter((channel) => !channelGroups.includes(channel)); - } - - // Filter out channels and groups which is still in use by the other PubNub client instances. - for (const client of clients) { - const subscription = client.subscription; - if (subscription === undefined) continue; - if (client.clientIdentifier === event.clientIdentifier) continue; - if (channels.length) - channels = channels.filter((channel) => !channel.endsWith('-pnpres') && !subscription.channels.includes(channel)); - if (channelGroups.length) - channelGroups = channelGroups.filter( - (group) => !group.endsWith('-pnpres') && !subscription.channelGroups.includes(group), - ); - } - - // Clean up from presence channels and groups - const channelsAndGroupsCount = channels.length + channelGroups.length; - if (channels.length) channels = channels.filter((channel) => !channel.endsWith('-pnpres')); - if (channelGroups.length) channelGroups = channelGroups.filter((group) => !group.endsWith('-pnpres')); - - if (channels.length === 0 && channelGroups.length === 0) { - if (client && client.workerLogVerbosity) { - const clientIds = clients - .reduce((identifiers: string[], { clientIdentifier }) => { - identifiers.push(clientIdentifier); - return identifiers; - }, []) - .join(', '); - - if (channelsAndGroupsCount > 0) { - consoleLog( - `Leaving only presence channels which doesn't require presence leave. Ignoring leave request.`, - client, - ); - } else { - consoleLog( - `Specified channels and groups still in use by other clients: ${clientIds}. Ignoring leave request.`, - client, - ); - } - } - - return undefined; - } - - // Update aggregated heartbeat state object. - if (client && serviceHeartbeatRequests[client.subscriptionKey] && (channels.length || channelGroups.length)) { - const hbRequestsBySubscriptionKey = serviceHeartbeatRequests[client.subscriptionKey]!; - const heartbeatRequestKey = `${client.userId}_${clientAggregateAuthKey(client) ?? ''}`; - - if (hbRequestsBySubscriptionKey[heartbeatRequestKey]) { - let { channels: hbChannels, channelGroups: hbChannelGroups } = hbRequestsBySubscriptionKey[heartbeatRequestKey]; - - if (channelGroups.length) hbChannelGroups = hbChannelGroups.filter((group) => !channels.includes(group)); - if (channels.length) hbChannels = hbChannels.filter((channel) => !channels.includes(channel)); - - hbRequestsBySubscriptionKey[heartbeatRequestKey].channelGroups = hbChannelGroups; - hbRequestsBySubscriptionKey[heartbeatRequestKey].channels = hbChannels; - } - } - - // Update request channels list (if required). - if (channels.length) { - const pathComponents = request.path.split('/'); - pathComponents[6] = channels.join(','); - request.path = pathComponents.join('/'); - } - - // Update request channel groups list (if required). - if (channelGroups.length) request.queryParameters!['channel-group'] = channelGroups.join(','); - - // Update `auth` key (if required). - if (clients.length > 1 && request.queryParameters && request.queryParameters.auth) { - const aggregatedAuthKey = authKeyForAggregatedClientsRequest(clients); - if (aggregatedAuthKey) request.queryParameters.auth = aggregatedAuthKey; - } - - return request; -}; - -/** - * Send event to the specific PubNub client. - * - * @param client - State for the client which should receive {@link event}. - * @param event - Subscription worker event object. - */ -const publishClientEvent = (client: PubNubClientState, event: SubscriptionWorkerEvent) => { - const receiver = (sharedWorkerClients[client.subscriptionKey] ?? {})[client.clientIdentifier]; - if (!receiver) return false; - - try { - receiver.postMessage(event); - return true; - } catch (error) { - if (client.workerLogVerbosity) console.error(`[SharedWorker] Unable send message using message port: ${error}`); - } - - return false; -}; - -/** - * Send request processing result event. - * - * @param clients - List of PubNub clients which should be notified about request result. - * @param fetchRequest - Actual request which has been used with `fetch` API. - * @param response - PubNub service response. - * @param request - Processed request information. - * @param [result] - Explicit request processing result which should be notified. - */ -const notifyRequestProcessingResult = ( - clients: PubNubClientState[], - fetchRequest: Request, - response: [Response, ArrayBuffer] | null, - request: TransportRequest, - result?: RequestSendingResult, -) => { - if (clients.length === 0) return; - if (!result && !response) return; - - const workerLogVerbosity = clients.some((client) => client && client.workerLogVerbosity); - const clientIds = sharedWorkerClients[clients[0].subscriptionKey] ?? {}; - const isSubscribeRequest = request.path.startsWith('/v2/subscribe'); - - if (!result && response) { - result = - response[0].status >= 400 - ? // Treat 4xx and 5xx status codes as errors. - requestProcessingError(undefined, response) - : requestProcessingSuccess(response); - } - - const headers: Record = {}; - let body: ArrayBuffer | undefined; - let status = 200; - - // Compose request response object. - if (response) { - body = response[1].byteLength > 0 ? response[1] : undefined; - const { headers: requestHeaders } = response[0]; - status = response[0].status; - - // Copy Headers object content into plain Record. - requestHeaders.forEach((value, key) => (headers[key] = value.toLowerCase())); - } - const transportResponse: TransportResponse = { status, url: fetchRequest.url, headers, body }; - - // Notify about subscribe and leave requests completion. - if (workerLogVerbosity && request && !request.path.endsWith('/heartbeat')) { - const notifiedClientIds = clients - .reduce((identifiers: string[], { clientIdentifier }) => { - identifiers.push(clientIdentifier); - return identifiers; - }, []) - .join(', '); - const endpoint = isSubscribeRequest ? 'subscribe' : 'leave'; - - const message = `Notify clients about ${endpoint} request completion: ${notifiedClientIds}`; - for (const client of clients) consoleLog(message, client); - } - - for (const client of clients) { - if (isSubscribeRequest && !client.subscription) { - // Notifying about client with inactive subscription. - if (workerLogVerbosity) { - const message = `${client.clientIdentifier} doesn't have active subscription. Don't notify about completion.`; - for (const nClient of clients) consoleLog(message, nClient); - } - - continue; - } - - const serviceWorkerClientId = clientIds[client.clientIdentifier]; - const { request: clientRequest } = client.subscription ?? {}; - let decidedRequest = clientRequest ?? request; - if (!isSubscribeRequest) decidedRequest = request; - - if (serviceWorkerClientId && decidedRequest) { - const payload = { - ...result!, - clientIdentifier: client.clientIdentifier, - identifier: decidedRequest.identifier, - url: `${decidedRequest.origin}${decidedRequest.path}`, - }; - - if (result!.type === 'request-process-success' && client.workerLogVerbosity) - consoleLog({ messageType: 'network-response', message: transportResponse as unknown as Payload }, client); - else if (result!.type === 'request-process-error' && client.workerLogVerbosity) { - const canceled = result!.error ? result!.error!.type === 'TIMEOUT' || result!.error!.type === 'ABORTED' : false; - let details = result!.error ? result!.error!.message : 'Unknown'; - if (payload.response) { - const contentType = payload.response.headers['content-type']; - - if ( - payload.response.body && - contentType && - (contentType.indexOf('javascript') !== -1 || contentType.indexOf('json') !== -1) - ) { - try { - const serviceResponse = JSON.parse(new TextDecoder().decode(payload.response.body)); - if ('message' in serviceResponse) details = serviceResponse.message; - else if ('error' in serviceResponse) { - if (typeof serviceResponse.error === 'string') details = serviceResponse.error; - else if (typeof serviceResponse.error === 'object' && 'message' in serviceResponse.error) - details = serviceResponse.error.message; - } - } catch (_) {} - } - - if (details === 'Unknown') { - if (payload.response.status >= 500) details = 'Internal Server Error'; - else if (payload.response.status == 400) details = 'Bad request'; - else if (payload.response.status == 403) details = 'Access denied'; - else details = `${payload.response.status}`; - } - } - - consoleLog( - { - messageType: 'network-request', - message: request as unknown as Payload, - details, - canceled, - failed: !canceled, - }, - client, - ); - } - - publishClientEvent(client, payload); - } else if (!serviceWorkerClientId && workerLogVerbosity) { - // Notifying about client without Shared Worker's communication channel. - const message = `${ - client.clientIdentifier - } doesn't have Shared Worker's communication channel. Don't notify about completion.`; - for (const nClient of clients) { - if (nClient.clientIdentifier !== client.clientIdentifier) consoleLog(message, nClient); - } - } - } -}; - -/** - * Create processing success event from service response. - * - * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each - * specific PubNub client state. - * - * @param res - Service response for used REST API endpoint along with response body. - * - * @returns Request processing success event object. - */ -const requestProcessingSuccess = (res: [Response, ArrayBuffer]): RequestSendingSuccess => { - const [response, body] = res; - const responseBody = body.byteLength > 0 ? body : undefined; - const contentLength = parseInt(response.headers.get('Content-Length') ?? '0', 10); - const contentType = response.headers.get('Content-Type')!; - const headers: Record = {}; - - // Copy Headers object content into plain Record. - response.headers.forEach((value, key) => (headers[key] = value.toLowerCase())); - - return { - type: 'request-process-success', - clientIdentifier: '', - identifier: '', - url: '', - response: { - contentLength, - contentType, - headers, - status: response.status, - body: responseBody, - }, - }; -}; - -/** - * Create processing error event from service response. - * - * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each - * specific PubNub client state. - * - * @param [error] - Client-side request processing error (for example network issues). - * @param [res] - Service error response (for example permissions error or malformed - * payload) along with service body. - * - * @returns Request processing error event object. - */ -const requestProcessingError = (error?: unknown, res?: [Response, ArrayBuffer]): RequestSendingError => { - // Use service response as error information source. - if (res) { - return { - ...requestProcessingSuccess(res), - type: 'request-process-error', - }; - } - - let type: NonNullable['type'] = 'NETWORK_ISSUE'; - let message = 'Unknown error'; - let name = 'Error'; - - if (error && error instanceof Error) { - message = error.message; - name = error.name; - } - - const errorMessage = message.toLowerCase(); - if (errorMessage.includes('timeout')) type = 'TIMEOUT'; - else if (name === 'AbortError' || errorMessage.includes('aborted') || errorMessage.includes('cancel')) { - message = 'Request aborted'; - type = 'ABORTED'; - } - - return { - type: 'request-process-error', - clientIdentifier: '', - identifier: '', - url: '', - error: { name, type, message }, - }; -}; -// endregion - -// -------------------------------------------------------- -// ----------------------- Helpers ------------------------ -// -------------------------------------------------------- -// region Helpers - -/** - * Register client if it didn't use Service Worker before. - * - * The registration process updates the Service Worker state with information about channels and groups in which - * particular PubNub clients are interested, and uses this information when another subscribe request is made to build - * shared requests. - * - * @param event - Base information about PubNub client instance and Service Worker {@link Client}. - */ -const registerClientIfRequired = (event: RegisterEvent) => { - const { clientIdentifier } = event; - - if (pubNubClients[clientIdentifier]) return; - - const client = (pubNubClients[clientIdentifier] = { - clientIdentifier, - subscriptionKey: event.subscriptionKey, - userId: event.userId, - heartbeatInterval: event.heartbeatInterval, - newlyRegistered: true, - offlineClientsCheckInterval: event.workerOfflineClientsCheckInterval, - unsubscribeOfflineClients: event.workerUnsubscribeOfflineClients, - workerLogVerbosity: event.workerLogVerbosity, - }); - - // Map registered PubNub client to its subscription key. - const clientsBySubscriptionKey = (pubNubClientsBySubscriptionKey[event.subscriptionKey] ??= []); - if (clientsBySubscriptionKey.every((entry) => entry.clientIdentifier !== clientIdentifier)) - clientsBySubscriptionKey.push(client); - - // Binding PubNub client to the MessagePort (receiver). - (sharedWorkerClients[event.subscriptionKey] ??= {})[clientIdentifier] = event.port; - - const message = - `Registered PubNub client with '${clientIdentifier}' identifier. ` + - `'${clientsBySubscriptionKey.length}' clients currently active.`; - for (const _client of clientsBySubscriptionKey) consoleLog(message, _client); - - if ( - !pingTimeouts[event.subscriptionKey] && - (pubNubClientsBySubscriptionKey[event.subscriptionKey] ?? []).length > 0 - ) { - const { subscriptionKey } = event; - const interval = event.workerOfflineClientsCheckInterval!; - for (const _client of clientsBySubscriptionKey) - consoleLog(`Setup PubNub client ping event ${interval} seconds`, _client); - - pingTimeouts[subscriptionKey] = setTimeout( - () => pingClients(subscriptionKey), - interval * 500 - 1, - ) as unknown as number; - } -}; - -/** - * Update configuration of previously registered PubNub client. - * - * @param event - Object with up-to-date client settings, which should be reflected in SharedWorker's state for the - * registered client. - */ -const updateClientInformation = (event: UpdateEvent) => { - const { clientIdentifier, userId, heartbeatInterval, accessToken: authKey, preProcessedToken: token } = event; - const client = pubNubClients[clientIdentifier]; - - // This should never happen. - if (!client) return; - - consoleDir({ userId, heartbeatInterval, authKey, token } as Payload, `Update client configuration:`, client); - - // Check whether identity changed as part of configuration update or not. - if (userId !== client.userId || (authKey && authKey !== (client.authKey ?? ''))) { - const _heartbeatRequests = serviceHeartbeatRequests[client.subscriptionKey] ?? {}; - const heartbeatRequestKey = `${userId}_${clientAggregateAuthKey(client) ?? ''}`; - // Clean up previous heartbeat aggregation data. - if (_heartbeatRequests[heartbeatRequestKey] !== undefined) delete _heartbeatRequests[heartbeatRequestKey]; - } - - const intervalChanged = client.heartbeatInterval !== heartbeatInterval; - - // Updating client configuration. - client.userId = userId; - client.heartbeatInterval = heartbeatInterval; - if (authKey) client.authKey = authKey; - if (token) client.accessToken = token; - - if (intervalChanged) startHeartbeatTimer(client, true); - updateCachedRequestAuthKeys(client); - - // Make immediate heartbeat call (if possible). - if (!client.heartbeat || !client.heartbeat.heartbeatEvent) return; - handleHeartbeatRequestEvent(client.heartbeat.heartbeatEvent, false, true); -}; - -/** - * Unregister client if it uses Service Worker before. - * - * During registration removal client information will be removed from the Shared Worker and - * long-poll request will be cancelled if possible. - * - * @param event - Base information about PubNub client instance and Service Worker {@link Client}. - */ -const unRegisterClient = (event: UnRegisterEvent) => { - invalidateClient(event.subscriptionKey, event.clientIdentifier); -}; - -/** - * Update information about previously registered client. - * - * Use information from request to populate list of channels and other useful information. - * - * @param event - Send request. - * @returns `true` if channels / groups list has been changed. May return `undefined` because `client` is missing. - */ -const updateClientSubscribeStateIfRequired = (event: SendRequestEvent): boolean | undefined => { - const query = event.request.queryParameters!; - const { clientIdentifier } = event; - const client = pubNubClients[clientIdentifier]; - let changed = false; - - // This should never happen. - if (!client) return; - - const channelGroupQuery = (query!['channel-group'] ?? '') as string; - const state = (query.state ?? '') as string; - - let subscription = client.subscription; - if (!subscription) { - changed = true; - subscription = { - refreshTimestamp: 0, - path: '', - channelGroupQuery: '', - channels: [], - channelGroups: [], - previousTimetoken: '0', - timetoken: '0', - objectsWithState: [], - }; - - if (state.length > 0) { - const parsedState = JSON.parse(state) as Record; - const userState = ((presenceState[client.subscriptionKey] ??= {})[client.userId] ??= {}); - - Object.entries(parsedState).forEach(([objectName, value]) => (userState[objectName] = value)); - subscription.objectsWithState = Object.keys(parsedState); - } - - client.subscription = subscription; - } else { - if (state.length > 0) { - const parsedState = JSON.parse(state) as Record; - const userState = ((presenceState[client.subscriptionKey] ??= {})[client.userId] ??= {}); - Object.entries(parsedState).forEach(([objectName, value]) => (userState[objectName] = value)); - - // Clean up state for objects where presence state has been reset. - for (const objectName of subscription.objectsWithState) - if (!parsedState[objectName]) delete userState[objectName]; - - subscription.objectsWithState = Object.keys(parsedState); - } - // Handle potential presence state reset. - else if (subscription.objectsWithState.length) { - const userState = ((presenceState[client.subscriptionKey] ??= {})[client.userId] ??= {}); - - for (const objectName of subscription.objectsWithState) delete userState[objectName]; - subscription.objectsWithState = []; - } - } - - if (subscription.path !== event.request.path) { - subscription.path = event.request.path; - const _channelsFromRequest = channelsFromRequest(event.request); - if (!changed) changed = !includesStrings(subscription.channels, _channelsFromRequest); - subscription.channels = _channelsFromRequest; - } - - if (subscription.channelGroupQuery !== channelGroupQuery) { - subscription.channelGroupQuery = channelGroupQuery; - const _channelGroupsFromRequest = channelGroupsFromRequest(event.request); - if (!changed) changed = !includesStrings(subscription.channelGroups, _channelGroupsFromRequest); - subscription.channelGroups = _channelGroupsFromRequest; - } - - let { authKey } = client; - const { userId } = client; - subscription.refreshTimestamp = Date.now(); - subscription.request = event.request; - subscription.filterExpression = (query['filter-expr'] ?? '') as string; - subscription.timetoken = (query.tt ?? '0') as string; - if (query.tr !== undefined) subscription.region = query.tr as string; - client.authKey = (query.auth ?? '') as string; - client.origin = event.request.origin; - client.userId = query.uuid as string; - client.pnsdk = query.pnsdk as string; - client.accessToken = event.preProcessedToken; - - if (client.newlyRegistered && !authKey && client.authKey) authKey = client.authKey; - client.newlyRegistered = false; - - return changed; -}; - -/** - * Update presence heartbeat information for previously registered client. - * - * Use information from request to populate list of channels / groups and presence state information. - * - * @param event - Send heartbeat request event. - */ -const updateClientHeartbeatState = (event: SendRequestEvent) => { - const { clientIdentifier } = event; - const client = pubNubClients[clientIdentifier]; - const { request } = event; - const query = request.queryParameters ?? {}; - - // This should never happen. - if (!client) return; - - const _clientHeartbeat = (client.heartbeat ??= { - channels: [], - channelGroups: [], - }); - _clientHeartbeat.heartbeatEvent = { ...event }; - - // Update presence heartbeat information about client. - _clientHeartbeat.channelGroups = channelGroupsFromRequest(request).filter((group) => !group.endsWith('-pnpres')); - _clientHeartbeat.channels = channelsFromRequest(request).filter((channel) => !channel.endsWith('-pnpres')); - - const state = (query.state ?? '') as string; - if (state.length > 0) { - const userPresenceState = JSON.parse(state) as Record; - for (const objectName of Object.keys(userPresenceState)) - if (!_clientHeartbeat.channels.includes(objectName) && !_clientHeartbeat.channelGroups.includes(objectName)) - delete userPresenceState[objectName]; - _clientHeartbeat.presenceState = userPresenceState; - } - - client.accessToken = event.preProcessedToken; -}; - -/** - * Handle PubNub client response on PING request. - * - * @param event - Information about client which responded on PING request. - */ -const handleClientPong = (event: PongEvent) => { - const client = pubNubClients[event.clientIdentifier]; - - if (!client) return; - - client.lastPongEvent = new Date().getTime() / 1000; -}; - -/** - * Clean up resources used by registered PubNub client instance. - * - * @param subscriptionKey - Subscription key which has been used by the - * invalidated instance. - * @param clientId - Unique PubNub client identifier. - */ -const invalidateClient = (subscriptionKey: string, clientId: string) => { - const invalidatedClient = pubNubClients[clientId]; - delete pubNubClients[clientId]; - let clients = pubNubClientsBySubscriptionKey[subscriptionKey]; - let serviceRequestId: string | undefined; - - // Unsubscribe invalidated PubNub client. - if (invalidatedClient) { - // Cancel long-poll request if possible. - if (invalidatedClient.subscription) { - serviceRequestId = invalidatedClient.subscription.serviceRequestId; - delete invalidatedClient.subscription.serviceRequestId; - if (serviceRequestId) cancelRequest(serviceRequestId); - } - - // Make sure to stop heartbeat timer. - stopHeartbeatTimer(invalidatedClient); - - if (serviceHeartbeatRequests[subscriptionKey]) { - const hbRequestsBySubscriptionKey = (serviceHeartbeatRequests[subscriptionKey] ??= {}); - const heartbeatRequestKey = `${invalidatedClient.userId}_${clientAggregateAuthKey(invalidatedClient) ?? ''}`; - - if ( - hbRequestsBySubscriptionKey[heartbeatRequestKey] && - hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier === invalidatedClient.clientIdentifier - ) - delete hbRequestsBySubscriptionKey[heartbeatRequestKey]!.clientIdentifier; - } - - // Leave subscribed channels / groups properly. - if (invalidatedClient.unsubscribeOfflineClients) unsubscribeClient(invalidatedClient, serviceRequestId); - } - - if (clients) { - // Clean up linkage between client and subscription key. - clients = clients.filter((client) => client.clientIdentifier !== clientId); - if (clients.length > 0) pubNubClientsBySubscriptionKey[subscriptionKey] = clients; - else { - delete pubNubClientsBySubscriptionKey[subscriptionKey]; - delete serviceHeartbeatRequests[subscriptionKey]; - } - - // Clean up presence state information if not in use anymore. - if (clients.length === 0) delete presenceState[subscriptionKey]; - - // Clean up service workers client linkage to PubNub clients. - if (clients.length > 0) { - const workerClients = sharedWorkerClients[subscriptionKey]; - if (workerClients) { - delete workerClients[clientId]; - - if (Object.keys(workerClients).length === 0) delete sharedWorkerClients[subscriptionKey]; - } - } else delete sharedWorkerClients[subscriptionKey]; - } - - const message = `Invalidate '${clientId}' client. '${ - (pubNubClientsBySubscriptionKey[subscriptionKey] ?? []).length - }' clients currently active.`; - if (!clients) consoleLog(message); - else for (const _client of clients) consoleLog(message, _client); -}; - -/** - * Unsubscribe offline / invalidated PubNub client. - * - * @param client - Invalidated PubNub client state object. - * @param [invalidatedClientServiceRequestId] - Identifier of the service request ID for which the invalidated - * client waited for a subscribe response. - */ -const unsubscribeClient = (client: PubNubClientState, invalidatedClientServiceRequestId?: string) => { - if (!client.subscription) return; - - const { channels, channelGroups } = client.subscription; - const encodedChannelGroups = (channelGroups ?? []) - .filter((name) => !name.endsWith('-pnpres')) - .map((name) => encodeString(name)) - .sort(); - const encodedChannels = (channels ?? []) - .filter((name) => !name.endsWith('-pnpres')) - .map((name) => encodeString(name)) - .sort(); - - if (encodedChannels.length === 0 && encodedChannelGroups.length === 0) return; - - const channelGroupsString: string | undefined = - encodedChannelGroups.length > 0 ? encodedChannelGroups.join(',') : undefined; - const channelsString = encodedChannels.length === 0 ? ',' : encodedChannels.join(','); - const query: Query = { - instanceid: client.clientIdentifier, - uuid: client.userId, - requestid: uuidGenerator.createUUID(), - ...(client.authKey ? { auth: client.authKey } : {}), - ...(channelGroupsString ? { 'channel-group': channelGroupsString } : {}), - }; - - const request: SendRequestEvent = { - type: 'send-request', - clientIdentifier: client.clientIdentifier, - subscriptionKey: client.subscriptionKey, - request: { - origin: client.origin, - path: `/v2/presence/sub-key/${client.subscriptionKey}/channel/${channelsString}/leave`, - queryParameters: query, - method: TransportMethod.GET, - headers: {}, - timeout: 10, - cancellable: false, - compressible: false, - identifier: query.requestid as string, - }, - }; - - handleSendLeaveRequestEvent(request, client, invalidatedClientServiceRequestId); -}; - -/** - * Start presence heartbeat timer for periodic `heartbeat` API calls. - * - * @param client - Client state with information for heartbeat. - * @param [adjust] - Whether timer fire timer should be re-adjusted or not. - */ -const startHeartbeatTimer = (client: PubNubClientState, adjust: boolean = false) => { - const { heartbeat, heartbeatInterval } = client; - - // Check whether there is a need to run "backup" heartbeat timer or not. - const shouldStart = - heartbeatInterval && - heartbeatInterval > 0 && - heartbeat !== undefined && - heartbeat.heartbeatEvent && - (heartbeat.channels.length > 0 || heartbeat.channelGroups.length > 0); - if (!shouldStart) { - stopHeartbeatTimer(client); - return; - } - - // Check whether there is active timer which should be re-adjusted or not. - if (adjust && !heartbeat.loop) return; - - let targetInterval = heartbeatInterval; - if (adjust && heartbeat.loop) { - const activeTime = (Date.now() - heartbeat.loop.startTimestamp) / 1000; - if (activeTime < targetInterval) targetInterval -= activeTime; - if (targetInterval === heartbeat.loop.heartbeatInterval) targetInterval += 0.05; - } - - stopHeartbeatTimer(client); - if (targetInterval <= 0) return; - - heartbeat.loop = { - timer: setTimeout(() => { - stopHeartbeatTimer(client); - if (!client.heartbeat || !client.heartbeat.heartbeatEvent) return; - - // Generate new request ID - const { request } = client.heartbeat.heartbeatEvent; - request.identifier = uuidGenerator.createUUID(); - request.queryParameters!.requestid = request.identifier; - - handleHeartbeatRequestEvent(client.heartbeat.heartbeatEvent, false); - }, targetInterval * 1000), - heartbeatInterval, - startTimestamp: Date.now(), - }; -}; - -/** - * Stop presence heartbeat timer before it will fire. - * - * @param client - Client state for which presence heartbeat timer should be stopped. - */ -const stopHeartbeatTimer = (client: PubNubClientState) => { - const { heartbeat } = client; - if (heartbeat === undefined || !heartbeat.loop) return; - - clearTimeout(heartbeat.loop.timer); - delete heartbeat.loop; -}; - -/** - * Refresh authentication key stored in cached `subscribe` and `heartbeat` requests. - * - * @param client - Client state for which cached requests should be updated. - */ -const updateCachedRequestAuthKeys = (client: PubNubClientState) => { - const { subscription, heartbeat } = client; - - // Update `auth` query for cached subscribe request (if required). - if (subscription && subscription.request && subscription.request.queryParameters) { - const query = subscription.request.queryParameters; - if (client.authKey && client.authKey.length > 0) query.auth = client.authKey; - else if (query.auth) delete query.auth; - } - - // Update `auth` query for cached heartbeat request (if required). - if (heartbeat?.heartbeatEvent && heartbeat.heartbeatEvent.request) { - if (client.accessToken) heartbeat.heartbeatEvent.preProcessedToken = client.accessToken; - - const hbRequestsBySubscriptionKey = (serviceHeartbeatRequests[client.subscriptionKey] ??= {}); - const heartbeatRequestKey = `${client.userId}_${clientAggregateAuthKey(client) ?? ''}`; - if (hbRequestsBySubscriptionKey[heartbeatRequestKey] && hbRequestsBySubscriptionKey[heartbeatRequestKey].response) - delete hbRequestsBySubscriptionKey[heartbeatRequestKey].response; - - // Generate new request ID - heartbeat.heartbeatEvent.request.identifier = uuidGenerator.createUUID(); - - const query = heartbeat.heartbeatEvent.request.queryParameters!; - query.requestid = heartbeat.heartbeatEvent.request.identifier; - if (client.authKey && client.authKey.length > 0) query.auth = client.authKey; - else if (query.auth) delete query.auth; - } -}; - -/** - * Validate received event payload. - */ -const validateEventPayload = (event: MessageEvent): boolean => { - const { clientIdentifier, subscriptionKey } = event.data as ClientEvent; - if (!clientIdentifier || typeof clientIdentifier !== 'string') return false; - - return !(!subscriptionKey || typeof subscriptionKey !== 'string'); -}; - -/** - * Search for active subscription for one of the passed {@link sharedWorkerClients}. - * - * @param activeClients - List of suitable registered PubNub clients. - * @param event - Send Subscriber Request event data. - * - * @returns Unique identifier of the active request which will receive real-time updates for channels and groups - * requested in received subscription request or `undefined` if none of active (or not scheduled) request can be used. - */ -const activeSubscriptionForEvent = ( - activeClients: PubNubClientState[], - event: SendRequestEvent, -): string | undefined => { - const query = event.request.queryParameters!; - const channelGroupQuery = (query['channel-group'] ?? '') as string; - const requestPath = event.request.path; - let channelGroups: string[] | undefined; - let channels: string[] | undefined; - - for (const client of activeClients) { - const { subscription } = client; - // Skip PubNub clients which doesn't await for subscription response. - if (!subscription || !subscription.serviceRequestId) continue; - const sourceClient = pubNubClients[event.clientIdentifier]; - const requestId = subscription.serviceRequestId; - - if (subscription.path === requestPath && subscription.channelGroupQuery === channelGroupQuery) { - consoleLog( - `Found identical request started by '${client.clientIdentifier}' client. -Waiting for existing '${requestId}' request completion.`, - sourceClient, - ); - - return subscription.serviceRequestId; - } else { - const scheduledRequest = serviceRequests[subscription.serviceRequestId]; - if (!channelGroups) channelGroups = channelGroupsFromRequest(event.request); - if (!channels) channels = channelsFromRequest(event.request); - - // Checking whether all required channels and groups are handled already by active request or not. - if (channels.length && !includesStrings(scheduledRequest.channels, channels)) continue; - if (channelGroups.length && !includesStrings(scheduledRequest.channelGroups, channelGroups)) continue; - - consoleDir( - scheduledRequest, - `'${event.request.identifier}' request channels and groups are subset of ongoing '${requestId}' request -which has started by '${client.clientIdentifier}' client. Waiting for existing '${requestId}' request completion.`, - sourceClient, - ); - - return subscription.serviceRequestId; - } - } - - return undefined; -}; - -/** - * Check whether there are any clients which can be used for subscribe request aggregation or not. - * - * @param client - PubNub client state which will be checked. - * @param event - Send subscribe request event information. - * - * @returns `true` in case there is more than 1 client which has same parameters for subscribe request to aggregate. - */ -const hasClientsForSendAggregatedSubscribeRequestEvent = (client: PubNubClientState, event: SendRequestEvent) => { - return clientsForSendSubscribeRequestEvent((client.subscription ?? {}).timetoken ?? '0', event).length > 1; -}; - -/** - * Find PubNub client states with configuration compatible with the one in request. - * - * Method allow to find information about all PubNub client instances which use same: - * - subscription key - * - `userId` - * - `auth` key - * - `filter expression` - * - `timetoken` (compare should be done against previous timetoken of the client which requested new subscribe). - * - * @param timetoken - Previous timetoken used by the PubNub client which requested to send new subscription request - * (it will be the same as 'current' timetoken of the other PubNub clients). - * @param event - Send subscribe request event information. - * - * @returns List of PubNub client states which works from other pages for the same user. - */ -const clientsForSendSubscribeRequestEvent = (timetoken: string, event: SendRequestEvent) => { - const reqClient = pubNubClients[event.clientIdentifier]; - if (!reqClient) return []; - - const query = event.request.queryParameters!; - const authKey = clientAggregateAuthKey(reqClient); - const filterExpression = (query['filter-expr'] ?? '') as string; - const userId = query.uuid! as string; - - return (pubNubClientsBySubscriptionKey[event.subscriptionKey] ?? []).filter( - (client) => - client.userId === userId && - clientAggregateAuthKey(client) === authKey && - client.subscription && - // Only clients with active subscription can be used. - (client.subscription.channels.length !== 0 || client.subscription.channelGroups.length !== 0) && - client.subscription.filterExpression === filterExpression && - (timetoken === '0' || client.subscription.timetoken === '0' || client.subscription.timetoken === timetoken), - ); -}; - -/** - * Find PubNub client state with configuration compatible with toe one in request. - * - * Method allow to find information about all PubNub client instances which use same: - * - subscription key - * - `userId` - * - `auth` key - * - * @param event - Send heartbeat request event information. - * - * @returns List of PubNub client states which works from other pages for the same user. - */ -const clientsForSendHeartbeatRequestEvent = (event: SendRequestEvent) => { - return clientsForSendLeaveRequestEvent(event); -}; - -/** - * Find PubNub client states with configuration compatible with the one in request. - * - * Method allow to find information about all PubNub client instances which use same: - * - subscription key - * - `userId` - * - `auth` key - * - * @param event - Send leave request event information. - * @param [invalidatedClient] - Invalidated PubNub client state. - * - * @returns List of PubNub client states which works from other pages for the same user. - */ -const clientsForSendLeaveRequestEvent = (event: SendRequestEvent, invalidatedClient?: PubNubClientState) => { - const reqClient = invalidatedClient ?? pubNubClients[event.clientIdentifier]; - if (!reqClient) return []; - - const query = event.request.queryParameters!; - const authKey = clientAggregateAuthKey(reqClient); - const userId = query.uuid! as string; - - return (pubNubClientsBySubscriptionKey[event.subscriptionKey] ?? []).filter( - (client) => client.userId === userId && clientAggregateAuthKey(client) === authKey, - ); -}; - -/** - * Extract list of channels from request URI path. - * - * @param request - Transport request which should provide `path` for parsing. - * - * @returns List of channel names (not percent-decoded) for which `subscribe` or `leave` has been called. - */ -const channelsFromRequest = (request: TransportRequest): string[] => { - const channels = request.path.split('/')[request.path.startsWith('/v2/subscribe/') ? 4 : 6]; - return channels === ',' ? [] : channels.split(',').filter((name) => name.length > 0); -}; - -/** - * Extract list of channel groups from request query. - * - * @param request - Transport request which should provide `query` for parsing. - * - * @returns List of channel group names (not percent-decoded) for which `subscribe` or `leave` has been called. - */ -const channelGroupsFromRequest = (request: TransportRequest): string[] => { - const group = (request.queryParameters!['channel-group'] ?? '') as string; - return group.length === 0 ? [] : group.split(',').filter((name) => name.length > 0); -}; - -/** - * Check whether {@link main} array contains all entries from {@link sub} array. - * - * @param main - Main array with which `intersection` with {@link sub} should be checked. - * @param sub - Sub-array whose values should be checked in {@link main}. - * - * @returns `true` if all entries from {@link sub} is present in {@link main}. - */ -const includesStrings = (main: string[], sub: string[]) => { - const set = new Set(main); - return sub.every(set.has, set); -}; - -/** - * Send PubNub client PING request to identify disconnected instances. - * - * @param subscriptionKey - Subscribe key for which offline PubNub client should be checked. - */ -const pingClients = (subscriptionKey: string) => { - const payload: SharedWorkerPing = { type: 'shared-worker-ping' }; - - const _pubNubClients = Object.values(pubNubClients).filter( - (client) => client && client.subscriptionKey === subscriptionKey, - ); - - _pubNubClients.forEach((client) => { - let clientInvalidated = false; - - if (client && client.lastPingRequest) { - const interval = client.offlineClientsCheckInterval!; - - // Check whether client never respond or last response was too long time ago. - if (!client.lastPongEvent || Math.abs(client.lastPongEvent - client.lastPingRequest) > interval * 0.5) { - clientInvalidated = true; - - for (const _client of _pubNubClients) - consoleLog(`'${client.clientIdentifier}' client is inactive. Invalidating...`, _client); - invalidateClient(client.subscriptionKey, client.clientIdentifier); - } - } - - if (client && !clientInvalidated) { - client.lastPingRequest = new Date().getTime() / 1000; - publishClientEvent(client, payload); - } - }); - - // Restart ping timer if there is still active PubNub clients for subscription key. - if (_pubNubClients && _pubNubClients.length > 0 && _pubNubClients[0]) { - const interval = _pubNubClients[0].offlineClientsCheckInterval!; - pingTimeouts[subscriptionKey] = setTimeout( - () => pingClients(subscriptionKey), - interval * 500 - 1, - ) as unknown as number; - } -}; - -/** - * Retrieve auth key which is suitable for common clients request aggregation. - * - * @param client - Client for which auth key for aggregation should be retrieved. - * - * @returns Client aggregation auth key. - */ -const clientAggregateAuthKey = (client: PubNubClientState): string | undefined => { - return client.accessToken ? (client.accessToken.token ?? client.authKey) : client.authKey; -}; - -/** - * Pick auth key for clients with latest expiration date. - * - * @param clients - List of clients for which latest auth key should be retrieved. - * - * @returns Access token which can be used to confirm `userId` permissions for aggregated request. - */ -const authKeyForAggregatedClientsRequest = (clients: PubNubClientState[]) => { - const latestClient = clients - .filter((client) => !!client.accessToken) - .sort((a, b) => a.accessToken!.expiration - b.accessToken!.expiration) - .pop(); - - return latestClient ? latestClient.authKey : undefined; -}; - -/** - * Compose clients' aggregation key. - * - * Aggregation key includes key parameters which differentiate clients between each other. - * - * @param client - Client for which identifier should be composed. - * - * @returns Aggregation timeout identifier string. - */ -const aggregateTimerId = (client: PubNubClientState) => { - const authKey = clientAggregateAuthKey(client); - let id = `${client.userId}-${client.subscriptionKey}${authKey ? `-${authKey}` : ''}`; - if (client.subscription && client.subscription.filterExpression) id += `-${client.subscription.filterExpression}`; - return id; -}; - -/** - * Print message on the worker's clients console. - * - * @param message - Message which should be printed. - * @param [client] - Target client to which log message should be sent. - */ -const consoleLog = (message: Payload, client?: PubNubClientState): void => { - const clients = (client ? [client] : Object.values(pubNubClients)).filter( - (client) => client && client.workerLogVerbosity, - ); - const payload: SharedWorkerConsoleLog = { - type: 'shared-worker-console-log', - message, - }; - - clients.forEach((client) => { - if (client) publishClientEvent(client, payload); - }); -}; - -/** - * Print message on the worker's clients console. - * - * @param data - Data which should be printed into the console. - * @param [message] - Message which should be printed before {@link data}. - * @param [client] - Target client to which log message should be sent. - */ -const consoleDir = (data: Payload, message?: string, client?: PubNubClientState): void => { - const clients = (client ? [client] : Object.values(pubNubClients)).filter( - (client) => client && client.workerLogVerbosity, - ); - const payload: SharedWorkerConsoleDir = { - type: 'shared-worker-console-dir', - message, - data, - }; - - clients.forEach((client) => { - if (client) publishClientEvent(client, payload); - }); -}; - -/** - * Stringify request query key / value pairs. - * - * @param query - Request query object. - * - * @returns Stringified query object. - */ -const queryStringFromObject = (query: Query) => { - return Object.keys(query) - .map((key) => { - const queryValue = query[key]; - if (!Array.isArray(queryValue)) return `${key}=${encodeString(queryValue)}`; - - return queryValue.map((value) => `${key}=${encodeString(value)}`).join('&'); - }) - .join('&'); -}; - -/** - * Percent-encode input string. - * - * **Note:** Encode content in accordance of the `PubNub` service requirements. - * - * @param input - Source string or number for encoding. - * - * @returns Percent-encoded string. - */ -const encodeString = (input: string | number) => { - return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); -}; -// endregion // endregion diff --git a/src/web/components/configuration.ts b/src/web/components/configuration.ts index b19f3590f..036002ac7 100644 --- a/src/web/components/configuration.ts +++ b/src/web/components/configuration.ts @@ -4,6 +4,7 @@ import { setDefaults as setBaseDefaults, } from '../../core/interfaces/configuration'; import { ICryptoModule } from '../../core/interfaces/crypto-module'; +import { LogLevel } from '../../core/interfaces/logger'; // -------------------------------------------------------- // ----------------------- Defaults ----------------------- @@ -92,9 +93,16 @@ export type PubNubConfiguration = UserConfiguration & { * Whether verbose logging should be enabled for `Subscription` worker should print debug messages or not. * * @default `false` + * + * @deprecated Use {@link PubNubConfiguration.subscriptionWorkerLogLevel|subscriptionWorkerLogLevel} instead. */ subscriptionWorkerLogVerbosity?: boolean; + /** + * Minimum messages log level which should be passed to the `Subscription` worker logger. + */ + subscriptionWorkerLogLevel?: LogLevel; + /** * API which should be used to make network requests. * diff --git a/src/web/index.ts b/src/web/index.ts index d4e72df86..d56494fef 100644 --- a/src/web/index.ts +++ b/src/web/index.ts @@ -19,6 +19,7 @@ import { PubNubMiddleware } from '../transport/middleware'; import { WebTransport } from '../transport/web-transport'; import { decode } from '../core/components/base64_codec'; import { Transport } from '../core/interfaces/transport'; +import { LogLevel } from '../core/interfaces/logger'; import Crypto from '../core/components/cryptography'; import WebCryptography from '../crypto/modules/web'; import { PubNubCore } from '../core/pubnub-common'; @@ -71,6 +72,19 @@ export default class PubNub extends PubNubCore { + transport.disconnect(); + disconnect(); + }; + } } if (configuration.listenToBrowserNetworkEvents ?? true) { diff --git a/test/integration/shared-worker/shared-worker.test.ts b/test/integration/shared-worker/shared-worker.test.ts index 96f108c95..fe4eb6ff9 100644 --- a/test/integration/shared-worker/shared-worker.test.ts +++ b/test/integration/shared-worker/shared-worker.test.ts @@ -1314,11 +1314,11 @@ describe('PubNub Shared Worker Integration Tests', () => { const channel1 = testChannels[0]; const channel2 = testChannels[1]; - let firstRequestIntercepted = false; - let secondRequestIntercepted = false; let errorOccurred = false; let testCompleted = false; - let firstSubscriptionEstablished = false; + let tokenUpdated = false; + let seenFirstForChannel1 = false; + let seenSecondForChannel2 = false; // Set initial token pubnubWithWorker.setToken(initialToken); @@ -1332,7 +1332,19 @@ describe('PubNub Shared Worker Integration Tests', () => { const underlyingTransport = transport.configuration.transport; const originalMakeSendable = underlyingTransport.makeSendable.bind(underlyingTransport); - let interceptedRequests: any[] = []; + const interceptedRequests: any[] = []; + + const pathContainsChannel = (path: string, channel: string) => { + const match = path.match(/\/v2\/subscribe\/[^/]+\/([^/]+)/) || path.match(/\/subscribe\/[^/]+\/([^/]+)/); + if (!match) return false; + try { + const segment = decodeURIComponent(match[1]); + const channels = segment.split(','); + return channels.includes(channel); + } catch { + return false; + } + }; // Override makeSendable to capture requests after middleware processing underlyingTransport.makeSendable = function (req: any) { @@ -1347,43 +1359,41 @@ describe('PubNub Shared Worker Integration Tests', () => { interceptedRequests.push(interceptedRequest); - // Handle first request (should have initial token) - if (!firstRequestIntercepted) { - firstRequestIntercepted = true; + const pathHasChannel1 = pathContainsChannel(interceptedRequest.path, channel1); + const pathHasChannel2 = pathContainsChannel(interceptedRequest.path, channel2); + + // First subscribe: path includes channel1 (but not channel2 yet); must carry initial token + if (!seenFirstForChannel1 && pathHasChannel1 && !pathHasChannel2) { + seenFirstForChannel1 = true; try { expect(interceptedRequest.queryParameters).to.exist; expect(interceptedRequest.queryParameters.auth).to.equal(initialToken); - // Update token and subscribe to second channel after a short delay - setTimeout(() => { - if (!testCompleted && !errorOccurred) { - pubnubWithWorker.setToken(updatedToken); + if (!testCompleted && !errorOccurred) { + // Update token first, then subscribe to channel2 so that the next request uses updated token + pubnubWithWorker.setToken(updatedToken); + tokenUpdated = true; - // Verify token was updated - const newToken = pubnubWithWorker.getToken(); - expect(newToken).to.equal(updatedToken); + const newToken = pubnubWithWorker.getToken(); + expect(newToken).to.equal(updatedToken); - // Subscribe to second channel - const subscription2 = pubnubWithWorker.channel(channel2).subscription(); - subscription2.subscribe(); - } - }, 500); + const subscription2 = pubnubWithWorker.channel(channel2).subscription(); + subscription2.subscribe(); + } } catch (error) { if (!testCompleted) { errorOccurred = true; testCompleted = true; - - // Restore original transport underlyingTransport.makeSendable = originalMakeSendable; done(error); return; } } } - // Handle second request (should have updated token) - else if (!secondRequestIntercepted && firstRequestIntercepted) { - secondRequestIntercepted = true; + // Second subscribe we care about: first request that includes channel2 after token update + else if (!seenSecondForChannel2 && tokenUpdated && pathHasChannel2) { + seenSecondForChannel2 = true; try { expect(interceptedRequest.queryParameters).to.exist; @@ -1391,8 +1401,6 @@ describe('PubNub Shared Worker Integration Tests', () => { if (!testCompleted) { testCompleted = true; - - // Restore original transport underlyingTransport.makeSendable = originalMakeSendable; done(); return; @@ -1401,8 +1409,6 @@ describe('PubNub Shared Worker Integration Tests', () => { if (!testCompleted) { errorOccurred = true; testCompleted = true; - - // Restore original transport underlyingTransport.makeSendable = originalMakeSendable; done(error); return; @@ -1415,72 +1421,30 @@ describe('PubNub Shared Worker Integration Tests', () => { return originalMakeSendable(req); }; - // Set up listener to handle subscription status - pubnubWithWorker.addListener({ - status: (statusEvent) => { - if (errorOccurred || testCompleted) return; - - if (statusEvent.category === PubNub.CATEGORIES.PNConnectedCategory) { - if (!firstSubscriptionEstablished) { - firstSubscriptionEstablished = true; - } - } else if (statusEvent.category === PubNub.CATEGORIES.PNNetworkIssuesCategory) { - if (!testCompleted) { - errorOccurred = true; - testCompleted = true; - - // Restore original transport - underlyingTransport.makeSendable = originalMakeSendable; - done(new Error(`Subscription failed with network issues: ${statusEvent.error || 'Unknown error'}`)); - } - } else if (statusEvent.error) { - if (!testCompleted) { - errorOccurred = true; - testCompleted = true; - - // Restore original transport - underlyingTransport.makeSendable = originalMakeSendable; - done(new Error(`Subscription failed: ${statusEvent.error}`)); - } - } - }, - }); - - // Add a timeout fallback + // Add a timeout fallback that validates we saw both phases setTimeout(() => { if (!testCompleted && !errorOccurred) { testCompleted = true; - - // Restore original transport underlyingTransport.makeSendable = originalMakeSendable; - // Check if we got both requests with correct tokens - if (interceptedRequests.length >= 2) { - try { - const firstReq = interceptedRequests[0]; - const secondReq = interceptedRequests[interceptedRequests.length - 1]; - - expect(firstReq.queryParameters.auth).to.equal(initialToken); - expect(secondReq.queryParameters.auth).to.equal(updatedToken); + try { + const first = interceptedRequests.find((r) => + (r.path.includes('/v2/subscribe/') || r.path.includes('/subscribe')) && + pathContainsChannel(r.path, channel1) && + !pathContainsChannel(r.path, channel2), + ); + const second = interceptedRequests.find((r) => + (r.path.includes('/v2/subscribe/') || r.path.includes('/subscribe')) && + pathContainsChannel(r.path, channel2), + ); - done(); - } catch (error) { - done(error); - } - } else if (interceptedRequests.length === 1) { - // Only got first request, check if it has correct token - try { - expect(interceptedRequests[0].queryParameters.auth).to.equal(initialToken); - done( - new Error( - 'Only received first subscription request, second request with updated token was not intercepted', - ), - ); - } catch (error) { - done(error); - } - } else { - done(new Error('No subscription requests were intercepted - shared worker may not be working')); + expect(first, 'no initial subscribe with channel1 captured').to.exist; + expect(first.queryParameters.auth).to.equal(initialToken); + expect(second, 'no subsequent subscribe with channel2 captured').to.exist; + expect(second.queryParameters.auth).to.equal(updatedToken); + done(); + } catch (error) { + done(error); } } }, 12000);