Skip to content

Fix issue which unsubscribed additional channels #459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
---
changelog:
- date: 2025-06-18
version: v9.6.1
changes:
- type: bug
text: "Fix issue that has been caused by the race of conditions on tab close and led to `presence leave` for channels that were still in use."
- type: improvement
text: "Make leeway depending from the minimal heartbeat interval (5% from it) to filter out too rapid heartbeat calls."
- date: 2025-06-04
version: v9.6.0
changes:
Expand Down Expand Up @@ -1249,7 +1256,7 @@ supported-platforms:
- 'Ubuntu 14.04 and up'
- 'Windows 7 and up'
version: 'Pubnub Javascript for Node'
version: '9.6.0'
version: '9.6.1'
sdks:
- full-name: PubNub Javascript SDK
short-name: Javascript
Expand All @@ -1265,7 +1272,7 @@ sdks:
- distribution-type: source
distribution-repository: GitHub release
package-name: pubnub.js
location: https://github.com/pubnub/javascript/archive/refs/tags/v9.6.0.zip
location: https://github.com/pubnub/javascript/archive/refs/tags/v9.6.1.zip
requires:
- name: 'agentkeepalive'
min-version: '3.5.2'
Expand Down Expand Up @@ -1936,7 +1943,7 @@ sdks:
- distribution-type: library
distribution-repository: GitHub release
package-name: pubnub.js
location: https://github.com/pubnub/javascript/releases/download/v9.6.0/pubnub.9.6.0.js
location: https://github.com/pubnub/javascript/releases/download/v9.6.1/pubnub.9.6.1.js
requires:
- name: 'agentkeepalive'
min-version: '3.5.2'
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## v9.6.1
June 18 2025

#### Fixed
- Fix issue that has been caused by the race of conditions on tab close and led to `presence leave` for channels that were still in use.

#### Modified
- Make leeway depending from the minimal heartbeat interval (5% from it) to filter out too rapid heartbeat calls.

## v9.6.0
June 04 2025

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.6.0.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.6.0.min.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.6.1.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.6.1.min.js

2. Configure your keys:

Expand Down
25 changes: 15 additions & 10 deletions dist/web/pubnub.js
Original file line number Diff line number Diff line change
Expand Up @@ -4957,7 +4957,7 @@
return base.PubNubFile;
},
get version() {
return '9.6.0';
return '9.6.1';
},
getVersion() {
return this.version;
Expand Down Expand Up @@ -7182,10 +7182,12 @@
region: this.region ? this.region : undefined,
};
this.configuration.logger().debug(this.constructor.name, () => {
const hashedEvents = messages.map((event) => ({
type: event.type,
data: Object.assign(Object.assign({}, event.data), { pn_mfp: messageFingerprint(event.data) }),
}));
const hashedEvents = messages.map((event) => {
const pn_mfp = event.type === PubNubEventType.Message || event.type === PubNubEventType.Signal
? messageFingerprint(event.data.message)
: undefined;
return pn_mfp ? { type: event.type, data: Object.assign(Object.assign({}, event.data), { pn_mfp }) } : event;
});
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
});
messages.forEach((message) => {
Expand Down Expand Up @@ -11976,6 +11978,7 @@
if (!this.state.isSubscribed)
return;
if (this.parentSetsCount > 0) {
// Creating from whole payload (not only for published messages).
const fingerprint = messageFingerprint(event.data);
if (this.handledUpdates.includes(fingerprint)) {
this.state.client.logger.trace(this.constructor.name, `Message (${fingerprint}) already handled. Ignoring.`);
Expand Down Expand Up @@ -15013,10 +15016,12 @@
emitMessages: (cursor, events) => {
try {
this.logger.debug('EventEngine', () => {
const hashedEvents = events.map((event) => ({
type: event.type,
data: Object.assign(Object.assign({}, event.data), { pn_mfp: messageFingerprint(event.data) }),
}));
const hashedEvents = events.map((event) => {
const pn_mfp = event.type === PubNubEventType.Message || event.type === PubNubEventType.Signal
? messageFingerprint(event.data.message)
: undefined;
return pn_mfp ? { type: event.type, data: Object.assign(Object.assign({}, event.data), { pn_mfp }) } : event;
});
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
});
events.forEach((event) => this.emitEvent(cursor, event));
Expand Down Expand Up @@ -17765,7 +17770,7 @@
let transport = new WebTransport(clientConfiguration.logger(), platformConfiguration.transport);
{
if (configurationCopy.subscriptionWorkerUrl) {
// Inject subscription worker into transport provider stack.
// Inject subscription worker into the transport provider stack.
const middleware = new SubscriptionWorkerMiddleware({
clientIdentifier: clientConfiguration._instanceId,
subscriptionKey: clientConfiguration.subscribeKey,
Expand Down
2 changes: 1 addition & 1 deletion dist/web/pubnub.min.js

Large diffs are not rendered by default.

56 changes: 33 additions & 23 deletions dist/web/pubnub.worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,18 +435,20 @@
* Handle client request to leave request.
*
* @param data - Leave event details.
* @param [client] - Specific client to handle leave request.
* @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, client) => {
const handleSendLeaveRequestEvent = (data, invalidatedClient, invalidatedClientServiceRequestId) => {
var _a, _b;
var _c;
client = client !== null && client !== void 0 ? client : pubNubClients[data.clientIdentifier];
const request = leaveTransportRequestFromEvent(data);
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 = subscription === null || subscription === void 0 ? void 0 : subscription.serviceRequestId;
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 = '';
Expand Down Expand Up @@ -571,8 +573,6 @@
const sendRequest = (request, getClients, success, failure, responsePreProcess) => {
(() => __awaiter(void 0, void 0, void 0, function* () {
var _a;
// Request progress support.
new Date().getTime();
Promise.race([
fetch(requestFromTransportRequest(request), {
signal: (_a = abortControllers.get(request.identifier)) === null || _a === void 0 ? void 0 : _a.signal,
Expand All @@ -583,7 +583,6 @@
.then((response) => response.arrayBuffer().then((buffer) => [response, buffer]))
.then((response) => (responsePreProcess ? responsePreProcess(response) : response))
.then((response) => {
response[1].byteLength > 0 ? response[1] : undefined;
const clients = getClients();
if (clients.length === 0)
return;
Expand Down Expand Up @@ -872,9 +871,10 @@
if (aggregated && hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier) {
const expectedTimestamp = hbRequestsBySubscriptionKey[heartbeatRequestKey].timestamp + minimumHeartbeatInterval * 1000;
const currentTimestamp = Date.now();
// Check whether it is too soon to send request or not (5 is leeway which let send request a bit earlier).
// Request should be sent if previous attempt failed.
if (!failedPreviousRequest && currentTimestamp < expectedTimestamp && expectedTimestamp - currentTimestamp > 5000)
// Check whether it is too soon to send request or not.
// Request should be sent if a previous attempt failed.
const leeway = minimumHeartbeatInterval * 0.05 * 1000;
if (!failedPreviousRequest && currentTimestamp < expectedTimestamp && expectedTimestamp - currentTimestamp > leeway)
return undefined;
}
delete hbRequestsBySubscriptionKey[heartbeatRequestKey].response;
Expand Down Expand Up @@ -928,15 +928,16 @@
*
* Filter out channels and groups, which is still in use by other PubNub client instances from leave request.
*
* @param event - Client's send leave event 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 is no channels and groups for which request can be
* @returns Final transport request or `undefined` in case if there are no channels and groups for which request can be
* done.
*/
const leaveTransportRequestFromEvent = (event) => {
const leaveTransportRequestFromEvent = (event, invalidatedClient) => {
var _a;
const client = pubNubClients[event.clientIdentifier];
const clients = clientsForSendLeaveRequestEvent(event);
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);
Expand Down Expand Up @@ -1385,11 +1386,12 @@
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) {
const { serviceRequestId } = invalidatedClient.subscription;
serviceRequestId = invalidatedClient.subscription.serviceRequestId;
delete invalidatedClient.subscription.serviceRequestId;
if (serviceRequestId)
cancelRequest(serviceRequestId);
Expand All @@ -1403,7 +1405,7 @@
}
// Leave subscribed channels / groups properly.
if (invalidatedClient.unsubscribeOfflineClients)
unsubscribeClient(invalidatedClient);
unsubscribeClient(invalidatedClient, serviceRequestId);
}
if (clients) {
// Clean up linkage between client and subscription key.
Expand Down Expand Up @@ -1436,10 +1438,17 @@
for (const _client of clients)
consoleLog(message, _client);
};
const unsubscribeClient = (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, serviceRequestId } = client.subscription;
const { channels, channelGroups } = client.subscription;
const encodedChannelGroups = (channelGroups !== null && channelGroups !== void 0 ? channelGroups : [])
.filter((name) => !name.endsWith('-pnpres'))
.map((name) => encodeString(name))
Expand Down Expand Up @@ -1469,7 +1478,7 @@
identifier: query.requestid,
},
};
handleSendLeaveRequestEvent(request, client);
handleSendLeaveRequestEvent(request, client, invalidatedClientServiceRequestId);
};
/**
* Validate received event payload.
Expand Down Expand Up @@ -1583,12 +1592,13 @@ which has started by '${client.clientIdentifier}' client. Waiting for existing '
* - `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) => {
const clientsForSendLeaveRequestEvent = (event, invalidatedClient) => {
var _a;
const reqClient = pubNubClients[event.clientIdentifier];
const reqClient = invalidatedClient !== null && invalidatedClient !== void 0 ? invalidatedClient : pubNubClients[event.clientIdentifier];
if (!reqClient)
return [];
const query = event.request.queryParameters;
Expand Down
2 changes: 1 addition & 1 deletion dist/web/pubnub.worker.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/core/components/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ const makeConfiguration = (base, setupCryptoModule) => {
return base.PubNubFile;
},
get version() {
return '9.6.0';
return '9.6.1';
},
getVersion() {
return this.version;
Expand Down
11 changes: 7 additions & 4 deletions lib/core/components/subscription-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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 reconnection_manager_1 = require("./reconnection_manager");
const categories_1 = __importDefault(require("../constants/categories"));
const deduping_manager_1 = require("./deduping_manager");
Expand Down Expand Up @@ -320,10 +321,12 @@ class SubscriptionManager {
region: this.region ? this.region : undefined,
};
this.configuration.logger().debug(this.constructor.name, () => {
const hashedEvents = messages.map((event) => ({
type: event.type,
data: Object.assign(Object.assign({}, event.data), { pn_mfp: (0, utils_1.messageFingerprint)(event.data) }),
}));
const hashedEvents = messages.map((event) => {
const pn_mfp = event.type === subscribe_1.PubNubEventType.Message || event.type === subscribe_1.PubNubEventType.Signal
? (0, utils_1.messageFingerprint)(event.data.message)
: undefined;
return pn_mfp ? { type: event.type, data: Object.assign(Object.assign({}, event.data), { pn_mfp }) } : event;
});
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
});
messages.forEach((message) => {
Expand Down
10 changes: 6 additions & 4 deletions lib/core/pubnub-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,12 @@ class PubNubCore {
emitMessages: (cursor, events) => {
try {
this.logger.debug('EventEngine', () => {
const hashedEvents = events.map((event) => ({
type: event.type,
data: Object.assign(Object.assign({}, event.data), { pn_mfp: (0, utils_1.messageFingerprint)(event.data) }),
}));
const hashedEvents = events.map((event) => {
const pn_mfp = event.type === subscribe_1.PubNubEventType.Message || event.type === subscribe_1.PubNubEventType.Signal
? (0, utils_1.messageFingerprint)(event.data.message)
: undefined;
return pn_mfp ? { type: event.type, data: Object.assign(Object.assign({}, event.data), { pn_mfp }) } : event;
});
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
});
events.forEach((event) => this.emitEvent(cursor, event));
Expand Down
1 change: 1 addition & 0 deletions lib/entities/subscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class Subscription extends subscription_base_1.SubscriptionBase {
if (!this.state.isSubscribed)
return;
if (this.parentSetsCount > 0) {
// Creating from whole payload (not only for published messages).
const fingerprint = (0, utils_1.messageFingerprint)(event.data);
if (this.handledUpdates.includes(fingerprint)) {
this.state.client.logger.trace(this.constructor.name, `Message (${fingerprint}) already handled. Ignoring.`);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pubnub",
"version": "9.6.0",
"version": "9.6.1",
"author": "PubNub <[email protected]>",
"description": "Publish & Subscribe Real-time Messaging with PubNub",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export const makeConfiguration = (
return base.PubNubFile;
},
get version(): string {
return '9.6.0';
return '9.6.1';
},
getVersion(): string {
return this.version;
Expand Down
13 changes: 8 additions & 5 deletions src/core/components/subscription-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { messageFingerprint, referenceSubscribeTimetoken, subscriptionTimetokenFromReference } from '../utils';
import { SubscribeRequestParameters as SubscribeRequestParameters } from '../endpoints/subscribe';
import { PubNubEventType, SubscribeRequestParameters as SubscribeRequestParameters } from '../endpoints/subscribe';
import { Payload, ResultCallback, Status, StatusCallback, StatusEvent } from '../types/api';
import { PrivateClientConfiguration } from '../interfaces/configuration';
import { HeartbeatRequest } from '../endpoints/presence/heartbeat';
Expand Down Expand Up @@ -501,10 +501,13 @@ export class SubscriptionManager {
};

this.configuration.logger().debug(this.constructor.name, () => {
const hashedEvents = messages.map((event) => ({
type: event.type,
data: { ...event.data, pn_mfp: messageFingerprint(event.data) },
}));
const hashedEvents = messages.map((event) => {
const pn_mfp =
event.type === PubNubEventType.Message || event.type === PubNubEventType.Signal
? messageFingerprint(event.data.message)
: undefined;
return pn_mfp ? { type: event.type, data: { ...event.data, pn_mfp } } : event;
});
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
});

Expand Down
Loading
Loading