Skip to content

Commit 913ebaf

Browse files
authored
Fix issue which unsubscribed additional channels (#459)
fix(shared-worker): fix issue which unsubscribed additional channels 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. refactor(shared-worker): change leeway for rapid heartbeat filter out Make leeway depending from the minimal heartbeat interval (5% from it) to filter out too rapid heartbeat calls.
1 parent 1a4ce6b commit 913ebaf

File tree

18 files changed

+151
-87
lines changed

18 files changed

+151
-87
lines changed

.pubnub.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
---
22
changelog:
3+
- date: 2025-06-18
4+
version: v9.6.1
5+
changes:
6+
- type: bug
7+
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."
8+
- type: improvement
9+
text: "Make leeway depending from the minimal heartbeat interval (5% from it) to filter out too rapid heartbeat calls."
310
- date: 2025-06-04
411
version: v9.6.0
512
changes:
@@ -1249,7 +1256,7 @@ supported-platforms:
12491256
- 'Ubuntu 14.04 and up'
12501257
- 'Windows 7 and up'
12511258
version: 'Pubnub Javascript for Node'
1252-
version: '9.6.0'
1259+
version: '9.6.1'
12531260
sdks:
12541261
- full-name: PubNub Javascript SDK
12551262
short-name: Javascript
@@ -1265,7 +1272,7 @@ sdks:
12651272
- distribution-type: source
12661273
distribution-repository: GitHub release
12671274
package-name: pubnub.js
1268-
location: https://github.com/pubnub/javascript/archive/refs/tags/v9.6.0.zip
1275+
location: https://github.com/pubnub/javascript/archive/refs/tags/v9.6.1.zip
12691276
requires:
12701277
- name: 'agentkeepalive'
12711278
min-version: '3.5.2'
@@ -1936,7 +1943,7 @@ sdks:
19361943
- distribution-type: library
19371944
distribution-repository: GitHub release
19381945
package-name: pubnub.js
1939-
location: https://github.com/pubnub/javascript/releases/download/v9.6.0/pubnub.9.6.0.js
1946+
location: https://github.com/pubnub/javascript/releases/download/v9.6.1/pubnub.9.6.1.js
19401947
requires:
19411948
- name: 'agentkeepalive'
19421949
min-version: '3.5.2'

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## v9.6.1
2+
June 18 2025
3+
4+
#### Fixed
5+
- 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.
6+
7+
#### Modified
8+
- Make leeway depending from the minimal heartbeat interval (5% from it) to filter out too rapid heartbeat calls.
9+
110
## v9.6.0
211
June 04 2025
312

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ Watch [Getting Started with PubNub JS SDK](https://app.dashcam.io/replay/64ee0d2
2727
npm install pubnub
2828
```
2929
* or download one of our builds from our CDN:
30-
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.6.0.js
31-
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.6.0.min.js
30+
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.6.1.js
31+
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.6.1.min.js
3232
3333
2. Configure your keys:
3434

dist/web/pubnub.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4957,7 +4957,7 @@
49574957
return base.PubNubFile;
49584958
},
49594959
get version() {
4960-
return '9.6.0';
4960+
return '9.6.1';
49614961
},
49624962
getVersion() {
49634963
return this.version;
@@ -7182,10 +7182,12 @@
71827182
region: this.region ? this.region : undefined,
71837183
};
71847184
this.configuration.logger().debug(this.constructor.name, () => {
7185-
const hashedEvents = messages.map((event) => ({
7186-
type: event.type,
7187-
data: Object.assign(Object.assign({}, event.data), { pn_mfp: messageFingerprint(event.data) }),
7188-
}));
7185+
const hashedEvents = messages.map((event) => {
7186+
const pn_mfp = event.type === PubNubEventType.Message || event.type === PubNubEventType.Signal
7187+
? messageFingerprint(event.data.message)
7188+
: undefined;
7189+
return pn_mfp ? { type: event.type, data: Object.assign(Object.assign({}, event.data), { pn_mfp }) } : event;
7190+
});
71897191
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
71907192
});
71917193
messages.forEach((message) => {
@@ -11976,6 +11978,7 @@
1197611978
if (!this.state.isSubscribed)
1197711979
return;
1197811980
if (this.parentSetsCount > 0) {
11981+
// Creating from whole payload (not only for published messages).
1197911982
const fingerprint = messageFingerprint(event.data);
1198011983
if (this.handledUpdates.includes(fingerprint)) {
1198111984
this.state.client.logger.trace(this.constructor.name, `Message (${fingerprint}) already handled. Ignoring.`);
@@ -15013,10 +15016,12 @@
1501315016
emitMessages: (cursor, events) => {
1501415017
try {
1501515018
this.logger.debug('EventEngine', () => {
15016-
const hashedEvents = events.map((event) => ({
15017-
type: event.type,
15018-
data: Object.assign(Object.assign({}, event.data), { pn_mfp: messageFingerprint(event.data) }),
15019-
}));
15019+
const hashedEvents = events.map((event) => {
15020+
const pn_mfp = event.type === PubNubEventType.Message || event.type === PubNubEventType.Signal
15021+
? messageFingerprint(event.data.message)
15022+
: undefined;
15023+
return pn_mfp ? { type: event.type, data: Object.assign(Object.assign({}, event.data), { pn_mfp }) } : event;
15024+
});
1502015025
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
1502115026
});
1502215027
events.forEach((event) => this.emitEvent(cursor, event));
@@ -17765,7 +17770,7 @@
1776517770
let transport = new WebTransport(clientConfiguration.logger(), platformConfiguration.transport);
1776617771
{
1776717772
if (configurationCopy.subscriptionWorkerUrl) {
17768-
// Inject subscription worker into transport provider stack.
17773+
// Inject subscription worker into the transport provider stack.
1776917774
const middleware = new SubscriptionWorkerMiddleware({
1777017775
clientIdentifier: clientConfiguration._instanceId,
1777117776
subscriptionKey: clientConfiguration.subscribeKey,

dist/web/pubnub.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/web/pubnub.worker.js

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -435,18 +435,20 @@
435435
* Handle client request to leave request.
436436
*
437437
* @param data - Leave event details.
438-
* @param [client] - Specific client to handle leave request.
438+
* @param [invalidatedClient] - Specific client to handle leave request.
439+
* @param [invalidatedClientServiceRequestId] - Identifier of the service request ID for which the invalidated
440+
* client waited for a subscribe response.
439441
*/
440-
const handleSendLeaveRequestEvent = (data, client) => {
442+
const handleSendLeaveRequestEvent = (data, invalidatedClient, invalidatedClientServiceRequestId) => {
441443
var _a, _b;
442444
var _c;
443-
client = client !== null && client !== void 0 ? client : pubNubClients[data.clientIdentifier];
444-
const request = leaveTransportRequestFromEvent(data);
445+
const client = invalidatedClient !== null && invalidatedClient !== void 0 ? invalidatedClient : pubNubClients[data.clientIdentifier];
446+
const request = leaveTransportRequestFromEvent(data, invalidatedClient);
445447
if (!client)
446448
return;
447449
// Clean up client subscription information if there is no more channels / groups to use.
448450
const { subscription, heartbeat } = client;
449-
const serviceRequestId = subscription === null || subscription === void 0 ? void 0 : subscription.serviceRequestId;
451+
const serviceRequestId = invalidatedClientServiceRequestId !== null && invalidatedClientServiceRequestId !== void 0 ? invalidatedClientServiceRequestId : subscription === null || subscription === void 0 ? void 0 : subscription.serviceRequestId;
450452
if (subscription && subscription.channels.length === 0 && subscription.channelGroups.length === 0) {
451453
subscription.channelGroupQuery = '';
452454
subscription.path = '';
@@ -571,8 +573,6 @@
571573
const sendRequest = (request, getClients, success, failure, responsePreProcess) => {
572574
(() => __awaiter(void 0, void 0, void 0, function* () {
573575
var _a;
574-
// Request progress support.
575-
new Date().getTime();
576576
Promise.race([
577577
fetch(requestFromTransportRequest(request), {
578578
signal: (_a = abortControllers.get(request.identifier)) === null || _a === void 0 ? void 0 : _a.signal,
@@ -583,7 +583,6 @@
583583
.then((response) => response.arrayBuffer().then((buffer) => [response, buffer]))
584584
.then((response) => (responsePreProcess ? responsePreProcess(response) : response))
585585
.then((response) => {
586-
response[1].byteLength > 0 ? response[1] : undefined;
587586
const clients = getClients();
588587
if (clients.length === 0)
589588
return;
@@ -872,9 +871,10 @@
872871
if (aggregated && hbRequestsBySubscriptionKey[heartbeatRequestKey].clientIdentifier) {
873872
const expectedTimestamp = hbRequestsBySubscriptionKey[heartbeatRequestKey].timestamp + minimumHeartbeatInterval * 1000;
874873
const currentTimestamp = Date.now();
875-
// Check whether it is too soon to send request or not (5 is leeway which let send request a bit earlier).
876-
// Request should be sent if previous attempt failed.
877-
if (!failedPreviousRequest && currentTimestamp < expectedTimestamp && expectedTimestamp - currentTimestamp > 5000)
874+
// Check whether it is too soon to send request or not.
875+
// Request should be sent if a previous attempt failed.
876+
const leeway = minimumHeartbeatInterval * 0.05 * 1000;
877+
if (!failedPreviousRequest && currentTimestamp < expectedTimestamp && expectedTimestamp - currentTimestamp > leeway)
878878
return undefined;
879879
}
880880
delete hbRequestsBySubscriptionKey[heartbeatRequestKey].response;
@@ -928,15 +928,16 @@
928928
*
929929
* Filter out channels and groups, which is still in use by other PubNub client instances from leave request.
930930
*
931-
* @param event - Client's send leave event request.
931+
* @param event - Client's sending leave event request.
932+
* @param [invalidatedClient] - Invalidated PubNub client state.
932933
*
933-
* @returns Final transport request or `undefined` in case if there is no channels and groups for which request can be
934+
* @returns Final transport request or `undefined` in case if there are no channels and groups for which request can be
934935
* done.
935936
*/
936-
const leaveTransportRequestFromEvent = (event) => {
937+
const leaveTransportRequestFromEvent = (event, invalidatedClient) => {
937938
var _a;
938-
const client = pubNubClients[event.clientIdentifier];
939-
const clients = clientsForSendLeaveRequestEvent(event);
939+
const client = invalidatedClient !== null && invalidatedClient !== void 0 ? invalidatedClient : pubNubClients[event.clientIdentifier];
940+
const clients = clientsForSendLeaveRequestEvent(event, invalidatedClient);
940941
let channelGroups = channelGroupsFromRequest(event.request);
941942
let channels = channelsFromRequest(event.request);
942943
const request = Object.assign({}, event.request);
@@ -1385,11 +1386,12 @@
13851386
const invalidatedClient = pubNubClients[clientId];
13861387
delete pubNubClients[clientId];
13871388
let clients = pubNubClientsBySubscriptionKey[subscriptionKey];
1389+
let serviceRequestId;
13881390
// Unsubscribe invalidated PubNub client.
13891391
if (invalidatedClient) {
13901392
// Cancel long-poll request if possible.
13911393
if (invalidatedClient.subscription) {
1392-
const { serviceRequestId } = invalidatedClient.subscription;
1394+
serviceRequestId = invalidatedClient.subscription.serviceRequestId;
13931395
delete invalidatedClient.subscription.serviceRequestId;
13941396
if (serviceRequestId)
13951397
cancelRequest(serviceRequestId);
@@ -1403,7 +1405,7 @@
14031405
}
14041406
// Leave subscribed channels / groups properly.
14051407
if (invalidatedClient.unsubscribeOfflineClients)
1406-
unsubscribeClient(invalidatedClient);
1408+
unsubscribeClient(invalidatedClient, serviceRequestId);
14071409
}
14081410
if (clients) {
14091411
// Clean up linkage between client and subscription key.
@@ -1436,10 +1438,17 @@
14361438
for (const _client of clients)
14371439
consoleLog(message, _client);
14381440
};
1439-
const unsubscribeClient = (client) => {
1441+
/**
1442+
* Unsubscribe offline / invalidated PubNub client.
1443+
*
1444+
* @param client - Invalidated PubNub client state object.
1445+
* @param [invalidatedClientServiceRequestId] - Identifier of the service request ID for which the invalidated
1446+
* client waited for a subscribe response.
1447+
*/
1448+
const unsubscribeClient = (client, invalidatedClientServiceRequestId) => {
14401449
if (!client.subscription)
14411450
return;
1442-
const { channels, channelGroups, serviceRequestId } = client.subscription;
1451+
const { channels, channelGroups } = client.subscription;
14431452
const encodedChannelGroups = (channelGroups !== null && channelGroups !== void 0 ? channelGroups : [])
14441453
.filter((name) => !name.endsWith('-pnpres'))
14451454
.map((name) => encodeString(name))
@@ -1469,7 +1478,7 @@
14691478
identifier: query.requestid,
14701479
},
14711480
};
1472-
handleSendLeaveRequestEvent(request, client);
1481+
handleSendLeaveRequestEvent(request, client, invalidatedClientServiceRequestId);
14731482
};
14741483
/**
14751484
* Validate received event payload.
@@ -1583,12 +1592,13 @@ which has started by '${client.clientIdentifier}' client. Waiting for existing '
15831592
* - `auth` key
15841593
*
15851594
* @param event - Send leave request event information.
1595+
* @param [invalidatedClient] - Invalidated PubNub client state.
15861596
*
15871597
* @returns List of PubNub client states which works from other pages for the same user.
15881598
*/
1589-
const clientsForSendLeaveRequestEvent = (event) => {
1599+
const clientsForSendLeaveRequestEvent = (event, invalidatedClient) => {
15901600
var _a;
1591-
const reqClient = pubNubClients[event.clientIdentifier];
1601+
const reqClient = invalidatedClient !== null && invalidatedClient !== void 0 ? invalidatedClient : pubNubClients[event.clientIdentifier];
15921602
if (!reqClient)
15931603
return [];
15941604
const query = event.request.queryParameters;

dist/web/pubnub.worker.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/core/components/configuration.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ const makeConfiguration = (base, setupCryptoModule) => {
164164
return base.PubNubFile;
165165
},
166166
get version() {
167-
return '9.6.0';
167+
return '9.6.1';
168168
},
169169
getVersion() {
170170
return this.version;

lib/core/components/subscription-manager.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
2121
Object.defineProperty(exports, "__esModule", { value: true });
2222
exports.SubscriptionManager = void 0;
2323
const utils_1 = require("../utils");
24+
const subscribe_1 = require("../endpoints/subscribe");
2425
const reconnection_manager_1 = require("./reconnection_manager");
2526
const categories_1 = __importDefault(require("../constants/categories"));
2627
const deduping_manager_1 = require("./deduping_manager");
@@ -320,10 +321,12 @@ class SubscriptionManager {
320321
region: this.region ? this.region : undefined,
321322
};
322323
this.configuration.logger().debug(this.constructor.name, () => {
323-
const hashedEvents = messages.map((event) => ({
324-
type: event.type,
325-
data: Object.assign(Object.assign({}, event.data), { pn_mfp: (0, utils_1.messageFingerprint)(event.data) }),
326-
}));
324+
const hashedEvents = messages.map((event) => {
325+
const pn_mfp = event.type === subscribe_1.PubNubEventType.Message || event.type === subscribe_1.PubNubEventType.Signal
326+
? (0, utils_1.messageFingerprint)(event.data.message)
327+
: undefined;
328+
return pn_mfp ? { type: event.type, data: Object.assign(Object.assign({}, event.data), { pn_mfp }) } : event;
329+
});
327330
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
328331
});
329332
messages.forEach((message) => {

lib/core/pubnub-common.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,12 @@ class PubNubCore {
308308
emitMessages: (cursor, events) => {
309309
try {
310310
this.logger.debug('EventEngine', () => {
311-
const hashedEvents = events.map((event) => ({
312-
type: event.type,
313-
data: Object.assign(Object.assign({}, event.data), { pn_mfp: (0, utils_1.messageFingerprint)(event.data) }),
314-
}));
311+
const hashedEvents = events.map((event) => {
312+
const pn_mfp = event.type === subscribe_1.PubNubEventType.Message || event.type === subscribe_1.PubNubEventType.Signal
313+
? (0, utils_1.messageFingerprint)(event.data.message)
314+
: undefined;
315+
return pn_mfp ? { type: event.type, data: Object.assign(Object.assign({}, event.data), { pn_mfp }) } : event;
316+
});
315317
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
316318
});
317319
events.forEach((event) => this.emitEvent(cursor, event));

lib/entities/subscription.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class Subscription extends subscription_base_1.SubscriptionBase {
116116
if (!this.state.isSubscribed)
117117
return;
118118
if (this.parentSetsCount > 0) {
119+
// Creating from whole payload (not only for published messages).
119120
const fingerprint = (0, utils_1.messageFingerprint)(event.data);
120121
if (this.handledUpdates.includes(fingerprint)) {
121122
this.state.client.logger.trace(this.constructor.name, `Message (${fingerprint}) already handled. Ignoring.`);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pubnub",
3-
"version": "9.6.0",
3+
"version": "9.6.1",
44
"author": "PubNub <[email protected]>",
55
"description": "Publish & Subscribe Real-time Messaging with PubNub",
66
"scripts": {

src/core/components/configuration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export const makeConfiguration = (
232232
return base.PubNubFile;
233233
},
234234
get version(): string {
235-
return '9.6.0';
235+
return '9.6.1';
236236
},
237237
getVersion(): string {
238238
return this.version;

src/core/components/subscription-manager.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

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

503503
this.configuration.logger().debug(this.constructor.name, () => {
504-
const hashedEvents = messages.map((event) => ({
505-
type: event.type,
506-
data: { ...event.data, pn_mfp: messageFingerprint(event.data) },
507-
}));
504+
const hashedEvents = messages.map((event) => {
505+
const pn_mfp =
506+
event.type === PubNubEventType.Message || event.type === PubNubEventType.Signal
507+
? messageFingerprint(event.data.message)
508+
: undefined;
509+
return pn_mfp ? { type: event.type, data: { ...event.data, pn_mfp } } : event;
510+
});
508511
return { messageType: 'object', message: hashedEvents, details: 'Received events:' };
509512
});
510513

0 commit comments

Comments
 (0)