Skip to content

Commit 61a55e7

Browse files
authored
Subscription set will subscribe added entities and SharedWorker will make proper subscribe throttling (#443)
feat(subscription-set): subscription set will subscribe added entities `SubscriptionSet` will subscribe / unsubscribe added / removed `Subscription` or `SubscriptionSet` objects if the set itself already subscribed. fix(shared-worker): fix subscribe throttle Fix issue because of which throttle didn't consider difference in client settings (throttled only by user ID and subscribe key, which is not enough). fix(smart-heartbeat): fix smart heartbeat usage With the fix, smart heartbeat as feature has been added to the SDK, and it is disabled by default. refactor(shared-worker): use more complex aggregation timer key
1 parent 0cbe409 commit 61a55e7

21 files changed

+375
-50
lines changed

.pubnub.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
---
22
changelog:
3+
- date: 2025-03-10
4+
version: v9.0.0
5+
changes:
6+
- type: feature
7+
text: "BREAKING CHANGES: `SubscriptionSet` will subscribe / unsubscribe added / removed `Subscription` or `SubscriptionSet` objects if the set itself already subscribed."
8+
- type: bug
9+
text: "Fix issue because of which throttle didn't consider difference in client settings (throttled only by user ID and subscribe key, which is not enough)."
10+
- type: bug
11+
text: "With the fix, smart heartbeat as feature has been added to the SDK, and it is disabled by default."
312
- date: 2025-03-06
413
version: v8.10.0
514
changes:
@@ -1160,7 +1169,7 @@ supported-platforms:
11601169
- 'Ubuntu 14.04 and up'
11611170
- 'Windows 7 and up'
11621171
version: 'Pubnub Javascript for Node'
1163-
version: '8.10.0'
1172+
version: '9.0.0'
11641173
sdks:
11651174
- full-name: PubNub Javascript SDK
11661175
short-name: Javascript
@@ -1176,7 +1185,7 @@ sdks:
11761185
- distribution-type: source
11771186
distribution-repository: GitHub release
11781187
package-name: pubnub.js
1179-
location: https://github.com/pubnub/javascript/archive/refs/tags/v8.10.0.zip
1188+
location: https://github.com/pubnub/javascript/archive/refs/tags/v9.0.0.zip
11801189
requires:
11811190
- name: 'agentkeepalive'
11821191
min-version: '3.5.2'
@@ -1847,7 +1856,7 @@ sdks:
18471856
- distribution-type: library
18481857
distribution-repository: GitHub release
18491858
package-name: pubnub.js
1850-
location: https://github.com/pubnub/javascript/releases/download/v8.10.0/pubnub.8.10.0.js
1859+
location: https://github.com/pubnub/javascript/releases/download/v9.0.0/pubnub.9.0.0.js
18511860
requires:
18521861
- name: 'agentkeepalive'
18531862
min-version: '3.5.2'

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## v9.0.0
2+
March 10 2025
3+
4+
#### Added
5+
- BREAKING CHANGES: `SubscriptionSet` will subscribe / unsubscribe added / removed `Subscription` or `SubscriptionSet` objects if the set itself already subscribed.
6+
7+
#### Fixed
8+
- Fix issue because of which throttle didn't consider difference in client settings (throttled only by user ID and subscribe key, which is not enough).
9+
- With the fix, smart heartbeat as feature has been added to the SDK, and it is disabled by default.
10+
111
## v8.10.0
212
March 06 2025
313

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# PubNub JavaScript SDK (V4)
22

3-
[![Build Status](https://travis-ci.com/pubnub/javascript.svg?branch=master)](https://travis-ci.com/pubnub/javascript)
43
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2859917905c549b8bfa27630ff276fce)](https://www.codacy.com/app/PubNub/javascript?utm_source=github.com&utm_medium=referral&utm_content=pubnub/javascript&utm_campaign=Badge_Grade)
54
[![npm](https://img.shields.io/npm/v/pubnub.svg)]()
65
[![Bower](https://img.shields.io/bower/v/pubnub.svg)]()
@@ -28,8 +27,8 @@ Watch [Getting Started with PubNub JS SDK](https://app.dashcam.io/replay/64ee0d2
2827
npm install pubnub
2928
```
3029
* or download one of our builds from our CDN:
31-
* https://cdn.pubnub.com/sdk/javascript/pubnub.8.10.0.js
32-
* https://cdn.pubnub.com/sdk/javascript/pubnub.8.10.0.min.js
30+
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.0.0.js
31+
* https://cdn.pubnub.com/sdk/javascript/pubnub.9.0.0.min.js
3332
3433
2. Configure your keys:
3534

dist/web/pubnub.js

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3438,7 +3438,7 @@
34383438
/**
34393439
* Whether heartbeat should be postponed on successful subscribe response or not.
34403440
*/
3441-
const USE_SMART_HEARTBEAT = true;
3441+
const USE_SMART_HEARTBEAT = false;
34423442
/**
34433443
* Whether PubNub client should try to utilize existing TCP connection for new requests or not.
34443444
*/
@@ -3818,7 +3818,7 @@
38183818
return base.PubNubFile;
38193819
},
38203820
get version() {
3821-
return '8.10.0';
3821+
return '9.0.0';
38223822
},
38233823
getVersion() {
38243824
return this.version;
@@ -4944,8 +4944,9 @@
49444944
* unsubscribe or not.
49454945
*/
49464946
reconnect(forUnsubscribe = false) {
4947-
this.startSubscribeLoop();
4948-
if (!forUnsubscribe && this.configuration.useSmartHeartbeat)
4947+
this.startSubscribeLoop(forUnsubscribe);
4948+
// Starting heartbeat loop for provided channels and groups.
4949+
if (!forUnsubscribe && !this.configuration.useSmartHeartbeat)
49494950
this.startHeartbeatTimer();
49504951
}
49514952
/**
@@ -5053,7 +5054,15 @@
50535054
channelGroups: this.subscribedChannelGroups,
50545055
}, isOffline);
50555056
}
5056-
startSubscribeLoop() {
5057+
/**
5058+
* Start next subscription loop.
5059+
*
5060+
* @param restartOnUnsubscribe - Whether restarting subscription loop as part of channels list change on
5061+
* unsubscribe or not.
5062+
*
5063+
* @internal
5064+
*/
5065+
startSubscribeLoop(restartOnUnsubscribe = false) {
50575066
this.stopSubscribeLoop();
50585067
const channelGroups = [...Object.keys(this.channelGroups)];
50595068
const channels = [...Object.keys(this.channels)];
@@ -5073,6 +5082,8 @@
50735082
}, (status, result) => {
50745083
this.processSubscribeResponse(status, result);
50755084
});
5085+
if (!restartOnUnsubscribe && this.configuration.useSmartHeartbeat)
5086+
this.startHeartbeatTimer();
50765087
}
50775088
stopSubscribeLoop() {
50785089
if (this._subscribeAbort) {
@@ -5224,7 +5235,9 @@
52245235
const heartbeatInterval = this.configuration.getHeartbeatInterval();
52255236
if (!heartbeatInterval || heartbeatInterval === 0)
52265237
return;
5227-
this.sendHeartbeat();
5238+
// Sending immediate heartbeat only if not working as smart heartbeat.
5239+
if (!this.configuration.useSmartHeartbeat)
5240+
this.sendHeartbeat();
52285241
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), heartbeatInterval * 1000);
52295242
}
52305243
/**
@@ -10084,13 +10097,17 @@
1008410097
subscribe(subscribeParameters) {
1008510098
const timetoken = subscribeParameters === null || subscribeParameters === void 0 ? void 0 : subscribeParameters.timetoken;
1008610099
this.pubnub.registerSubscribeCapable(this);
10100+
this.subscribedAutomatically = false;
10101+
this.subscribed = true;
1008710102
this.pubnub.subscribe(Object.assign({ channels: this.channelNames, channelGroups: this.groupNames }, (timetoken !== null && timetoken !== '' && { timetoken: timetoken })));
1008810103
}
1008910104
/**
1009010105
* Stop real-time events processing.
1009110106
*/
1009210107
unsubscribe() {
1009310108
this.pubnub.unregisterSubscribeCapable(this);
10109+
this.subscribedAutomatically = false;
10110+
this.subscribed = false;
1009410111
const { channels, channelGroups } = this.pubnub.getSubscribeCapableEntities();
1009510112
// Identify channels and groups from which PubNub client can safely unsubscribe.
1009610113
const filteredChannelGroups = this.groupNames.filter((cg) => !channelGroups.includes(cg));
@@ -10251,6 +10268,19 @@
1025110268
* @internal
1025210269
*/
1025310270
this.subscriptionList = [];
10271+
/**
10272+
* Whether subscribed ({@link SubscribeCapable#subscribe}) automatically during subscription
10273+
* object / sets manipulation or not.
10274+
*
10275+
* @internal
10276+
*/
10277+
this.subscribedAutomatically = false;
10278+
/**
10279+
* Whether subscribable object subscribed ({@link SubscribeCapable#subscribe}) or not.
10280+
*
10281+
* @internal
10282+
*/
10283+
this.subscribed = false;
1025410284
this.options = subscriptionOptions;
1025510285
this.eventEmitter = eventEmitter;
1025610286
this.pubnub = pubnub;
@@ -10280,6 +10310,13 @@
1028010310
this.channelNames = [...this.channelNames, ...subscription.channels];
1028110311
this.groupNames = [...this.groupNames, ...subscription.channelGroups];
1028210312
this.eventEmitter.addListener(this.listener, subscription.channels, subscription.channelGroups);
10313+
// Subscribe subscription object if subscription set already subscribed.
10314+
// @ts-expect-error: Required access of protected field.
10315+
if (this.subscribed && !subscription.subscribed) {
10316+
subscription.subscribe();
10317+
// @ts-expect-error: Required modification of protected field.
10318+
subscription.subscribedAutomatically = true; // should be placed after .subscribe() call.
10319+
}
1028310320
}
1028410321
/**
1028510322
* Remove entity's subscription object from the set.
@@ -10296,6 +10333,9 @@
1029610333
this.groupNames = this.groupNames.filter((cg) => !groupsToRemove.includes(cg));
1029710334
this.subscriptionList = this.subscriptionList.filter((s) => s !== subscription);
1029810335
this.eventEmitter.removeListener(this.listener, channelsToRemove, groupsToRemove);
10336+
// @ts-expect-error: Required access of protected field.
10337+
if (subscription.subscribedAutomatically)
10338+
subscription.unsubscribe();
1029910339
}
1030010340
/**
1030110341
* Merge with other subscription set object.
@@ -10310,6 +10350,11 @@
1031010350
this.channelNames = [...this.channelNames, ...subscriptionSet.channels];
1031110351
this.groupNames = [...this.groupNames, ...subscriptionSet.channelGroups];
1031210352
this.eventEmitter.addListener(this.listener, subscriptionSet.channels, subscriptionSet.channelGroups);
10353+
// Subscribe subscription object if subscription set already subscribed.
10354+
if (this.subscribed && !subscriptionSet.subscribed) {
10355+
subscriptionSet.subscribe();
10356+
subscriptionSet.subscribedAutomatically = true; // should be placed after .subscribe() call.
10357+
}
1031310358
}
1031410359
/**
1031510360
* Subtract other subscription set object.
@@ -10326,6 +10371,8 @@
1032610371
this.groupNames = this.groupNames.filter((cg) => !groupsToRemove.includes(cg));
1032710372
this.subscriptionList = this.subscriptionList.filter((s) => !subscriptionSet.subscriptions.includes(s));
1032810373
this.eventEmitter.removeListener(this.listener, channelsToRemove, groupsToRemove);
10374+
if (subscriptionSet.subscribedAutomatically)
10375+
subscriptionSet.unsubscribe();
1032910376
}
1033010377
/**
1033110378
* Get list of entities' subscription objects registered in subscription set.
@@ -10381,6 +10428,19 @@
1038110428
* @internal
1038210429
*/
1038310430
this.groupNames = [];
10431+
/**
10432+
* Whether subscribed ({@link SubscribeCapable#subscribe}) automatically during subscription
10433+
* object / sets manipulation or not.
10434+
*
10435+
* @internal
10436+
*/
10437+
this.subscribedAutomatically = false;
10438+
/**
10439+
* Whether subscribable object subscribed ({@link SubscribeCapable#subscribe}) or not.
10440+
*
10441+
* @internal
10442+
*/
10443+
this.subscribed = false;
1038410444
this.channelNames = channels;
1038510445
this.groupNames = channelGroups;
1038610446
this.options = subscriptionOptions;
@@ -10396,13 +10456,25 @@
1039610456
* @return Subscription set which contains both receiver and other entities' subscription objects.
1039710457
*/
1039810458
addSubscription(subscription) {
10399-
return new SubscriptionSet({
10459+
const subscriptionSet = new SubscriptionSet({
1040010460
channels: [...this.channelNames, ...subscription.channels],
1040110461
channelGroups: [...this.groupNames, ...subscription.channelGroups],
1040210462
subscriptionOptions: Object.assign(Object.assign({}, this.options), subscription === null || subscription === void 0 ? void 0 : subscription.options),
1040310463
eventEmitter: this.eventEmitter,
1040410464
pubnub: this.pubnub,
1040510465
});
10466+
// Subscribe whole subscription set if it has been created with receiving subscription object
10467+
// which is already subscribed.
10468+
if (this.subscribed) {
10469+
if (!subscription.subscribed) {
10470+
subscription.subscribe();
10471+
subscription.subscribedAutomatically = true; // should be placed after .subscribe() call.
10472+
}
10473+
this.pubnub.registerSubscribeCapable(subscriptionSet);
10474+
// @ts-expect-error: Required modification of protected field.
10475+
subscriptionSet.subscribed = true;
10476+
}
10477+
return subscriptionSet;
1040610478
}
1040710479
}
1040810480

dist/web/pubnub.min.js

Lines changed: 2 additions & 2 deletions
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: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,15 +239,21 @@
239239
updateClientSubscribeStateIfRequired(data);
240240
const client = pubNubClients[data.clientIdentifier];
241241
if (client) {
242-
const timerIdentifier = `${client.userId}-${client.subscriptionKey}`;
243-
// Check whether we need to start new aggregation timer or not.
244-
if (!aggregationTimers.has(timerIdentifier)) {
245-
const aggregationTimer = setTimeout(() => {
246-
handleSendSubscribeRequestEvent(data);
247-
aggregationTimers.delete(timerIdentifier);
248-
}, subscribeAggregationTimeout);
249-
aggregationTimers.set(timerIdentifier, aggregationTimer);
242+
// Check whether there are more clients which may schedule next subscription loop and they need to be
243+
// aggregated or not.
244+
if (hasClientsForSendAggregatedSubscribeRequestEvent(client, data)) {
245+
const timerIdentifier = aggregateTimerId(client);
246+
// Check whether we need to start new aggregation timer or not.
247+
if (!aggregationTimers.has(timerIdentifier)) {
248+
const aggregationTimer = setTimeout(() => {
249+
handleSendSubscribeRequestEvent(data);
250+
aggregationTimers.delete(timerIdentifier);
251+
}, subscribeAggregationTimeout);
252+
aggregationTimers.set(timerIdentifier, aggregationTimer);
253+
}
250254
}
255+
else
256+
handleSendSubscribeRequestEvent(data);
251257
}
252258
}
253259
else if (data.request.path.endsWith('/heartbeat')) {
@@ -1477,6 +1483,18 @@ which has started by '${client.clientIdentifier}' client. Waiting for existing '
14771483
}
14781484
return undefined;
14791485
};
1486+
/**
1487+
* Check whether there are any clients which can be used for subscribe request aggregation or not.
1488+
*
1489+
* @param client - PubNub client state which will be checked.
1490+
* @param event - Send subscribe request event information.
1491+
*
1492+
* @returns `true` in case there is more than 1 client which has same parameters for subscribe request to aggregate.
1493+
*/
1494+
const hasClientsForSendAggregatedSubscribeRequestEvent = (client, event) => {
1495+
var _a, _b;
1496+
return clientsForSendSubscribeRequestEvent((_b = ((_a = client.subscription) !== null && _a !== void 0 ? _a : {}).timetoken) !== null && _b !== void 0 ? _b : '0', event).length > 1;
1497+
};
14801498
/**
14811499
* Find PubNub client states with configuration compatible with the one in request.
14821500
*
@@ -1607,6 +1625,21 @@ which has started by '${client.clientIdentifier}' client. Waiting for existing '
16071625
pingTimeouts[subscriptionKey] = setTimeout(() => pingClients(subscriptionKey), interval * 500 - 1);
16081626
}
16091627
};
1628+
/**
1629+
* Compose clients' aggregation key.
1630+
*
1631+
* Aggregation key includes key parameters which differentiate clients between each other.
1632+
*
1633+
* @param client - Client for which identifier should be composed.
1634+
*
1635+
* @returns Aggregation timeout identifier string.
1636+
*/
1637+
const aggregateTimerId = (client) => {
1638+
let id = `${client.userId}-${client.subscriptionKey}${client.authKey ? `-${client.authKey}` : ''}`;
1639+
if (client.subscription && client.subscription.filterExpression)
1640+
id += `-${client.subscription.filterExpression}`;
1641+
return id;
1642+
};
16101643
/**
16111644
* Print message on the worker's clients console.
16121645
*

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
@@ -120,7 +120,7 @@ const makeConfiguration = (base, setupCryptoModule) => {
120120
return base.PubNubFile;
121121
},
122122
get version() {
123-
return '8.10.0';
123+
return '9.0.0';
124124
},
125125
getVersion() {
126126
return this.version;

lib/core/components/subscription-manager.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ class SubscriptionManager {
8181
* unsubscribe or not.
8282
*/
8383
reconnect(forUnsubscribe = false) {
84-
this.startSubscribeLoop();
85-
if (!forUnsubscribe && this.configuration.useSmartHeartbeat)
84+
this.startSubscribeLoop(forUnsubscribe);
85+
// Starting heartbeat loop for provided channels and groups.
86+
if (!forUnsubscribe && !this.configuration.useSmartHeartbeat)
8687
this.startHeartbeatTimer();
8788
}
8889
/**
@@ -190,7 +191,15 @@ class SubscriptionManager {
190191
channelGroups: this.subscribedChannelGroups,
191192
}, isOffline);
192193
}
193-
startSubscribeLoop() {
194+
/**
195+
* Start next subscription loop.
196+
*
197+
* @param restartOnUnsubscribe - Whether restarting subscription loop as part of channels list change on
198+
* unsubscribe or not.
199+
*
200+
* @internal
201+
*/
202+
startSubscribeLoop(restartOnUnsubscribe = false) {
194203
this.stopSubscribeLoop();
195204
const channelGroups = [...Object.keys(this.channelGroups)];
196205
const channels = [...Object.keys(this.channels)];
@@ -210,6 +219,8 @@ class SubscriptionManager {
210219
}, (status, result) => {
211220
this.processSubscribeResponse(status, result);
212221
});
222+
if (!restartOnUnsubscribe && this.configuration.useSmartHeartbeat)
223+
this.startHeartbeatTimer();
213224
}
214225
stopSubscribeLoop() {
215226
if (this._subscribeAbort) {
@@ -361,7 +372,9 @@ class SubscriptionManager {
361372
const heartbeatInterval = this.configuration.getHeartbeatInterval();
362373
if (!heartbeatInterval || heartbeatInterval === 0)
363374
return;
364-
this.sendHeartbeat();
375+
// Sending immediate heartbeat only if not working as smart heartbeat.
376+
if (!this.configuration.useSmartHeartbeat)
377+
this.sendHeartbeat();
365378
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), heartbeatInterval * 1000);
366379
}
367380
/**

0 commit comments

Comments
 (0)