Skip to content

Commit 2c39f2b

Browse files
mohitpubnubMohit TejaniMohit Tejaniparfeon
authored
fix: presence event engine internal state sync (#479)
fix: presence event engine internal state sync when unsubscribed feat(react-native) restore legacy crypto module support for React Native target refactor(presence): temporarily remove `offset` --------- Co-authored-by: Mohit Tejani <mohit.tejani@Mohits-MBP.lan> Co-authored-by: Mohit Tejani <mohit.tejani@Mohits-MacBook-Pro.local> Co-authored-by: Serhii Mamontov <sergey@pubnub.com>
1 parent 9c66040 commit 2c39f2b

File tree

26 files changed

+1209
-233
lines changed

26 files changed

+1209
-233
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-09-30
4+
version: v10.1.0
5+
changes:
6+
- type: feature
7+
text: "Reintroduced legacy encryption and decryption functions for the React Native target to ensure backward compatibility."
8+
- type: bug
9+
text: "Resolves issue where presence heartbeat channels/groups sets were out of sync."
10+
- type: improvement
11+
text: "Temporarily remove the `offset` parameter until implementation synchronization across SDKs is completed."
312
- date: 2025-09-18
413
version: v10.0.0
514
changes:
@@ -1340,7 +1349,7 @@ supported-platforms:
13401349
- 'Ubuntu 14.04 and up'
13411350
- 'Windows 7 and up'
13421351
version: 'Pubnub Javascript for Node'
1343-
version: '10.0.0'
1352+
version: '10.1.0'
13441353
sdks:
13451354
- full-name: PubNub Javascript SDK
13461355
short-name: Javascript
@@ -1356,7 +1365,7 @@ sdks:
13561365
- distribution-type: source
13571366
distribution-repository: GitHub release
13581367
package-name: pubnub.js
1359-
location: https://github.com/pubnub/javascript/archive/refs/tags/v10.0.0.zip
1368+
location: https://github.com/pubnub/javascript/archive/refs/tags/v10.1.0.zip
13601369
requires:
13611370
- name: 'agentkeepalive'
13621371
min-version: '3.5.2'
@@ -2027,7 +2036,7 @@ sdks:
20272036
- distribution-type: library
20282037
distribution-repository: GitHub release
20292038
package-name: pubnub.js
2030-
location: https://github.com/pubnub/javascript/releases/download/v10.0.0/pubnub.10.0.0.js
2039+
location: https://github.com/pubnub/javascript/releases/download/v10.1.0/pubnub.10.1.0.js
20312040
requires:
20322041
- name: 'agentkeepalive'
20332042
min-version: '3.5.2'

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## v10.1.0
2+
September 30 2025
3+
4+
#### Added
5+
- Reintroduced legacy encryption and decryption functions for the React Native target to ensure backward compatibility. This change merges PR #476. Fixed the following issues reported by [@nholik](https://github.com/nholik): [#474](https://github.com/pubnub/javascript/issues/474).
6+
7+
#### Fixed
8+
- Resolves issue where presence heartbeat channels/groups sets were out of sync.
9+
10+
#### Modified
11+
- Temporarily remove the `offset` parameter until implementation synchronization across SDKs is completed.
12+
113
## v10.0.0
214
September 18 2025
315

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.10.0.0.js
31-
* https://cdn.pubnub.com/sdk/javascript/pubnub.10.0.0.min.js
30+
* https://cdn.pubnub.com/sdk/javascript/pubnub.10.1.0.js
31+
* https://cdn.pubnub.com/sdk/javascript/pubnub.10.1.0.min.js
3232
3333
2. Configure your keys:
3434

dist/web/pubnub.js

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5436,7 +5436,7 @@
54365436
return base.PubNubFile;
54375437
},
54385438
get version() {
5439-
return '10.0.0';
5439+
return '10.1.0';
54405440
},
54415441
getVersion() {
54425442
return this.version;
@@ -8923,13 +8923,26 @@
89238923
this.engine.transition(joined(this.channels.slice(0), this.groups.slice(0)));
89248924
}
89258925
leave({ channels, groups }) {
8926+
// Update internal channel tracking to prevent stale heartbeat requests
8927+
if (channels)
8928+
this.channels = this.channels.filter((channel) => !channels.includes(channel));
8929+
if (groups)
8930+
this.groups = this.groups.filter((group) => !groups.includes(group));
89268931
if (this.dependencies.presenceState) {
89278932
channels === null || channels === void 0 ? void 0 : channels.forEach((c) => delete this.dependencies.presenceState[c]);
89288933
groups === null || groups === void 0 ? void 0 : groups.forEach((g) => delete this.dependencies.presenceState[g]);
89298934
}
89308935
this.engine.transition(left(channels !== null && channels !== void 0 ? channels : [], groups !== null && groups !== void 0 ? groups : []));
89318936
}
89328937
leaveAll(isOffline = false) {
8938+
// Clear presence state for all current channels and groups
8939+
if (this.dependencies.presenceState) {
8940+
this.channels.forEach((c) => delete this.dependencies.presenceState[c]);
8941+
this.groups.forEach((g) => delete this.dependencies.presenceState[g]);
8942+
}
8943+
// Reset internal channel and group tracking
8944+
this.channels = [];
8945+
this.groups = [];
89338946
this.engine.transition(leftAll(isOffline));
89348947
}
89358948
reconnect() {
@@ -11432,19 +11445,18 @@
1143211445
*/
1143311446
class HereNowRequest extends AbstractRequest {
1143411447
constructor(parameters) {
11435-
var _a, _b, _c, _d;
11436-
var _e, _f, _g, _h;
11448+
var _a, _b, _c;
11449+
var _d, _e, _f;
1143711450
super();
1143811451
this.parameters = parameters;
1143911452
// Apply defaults.
11440-
(_a = (_e = this.parameters).queryParameters) !== null && _a !== void 0 ? _a : (_e.queryParameters = {});
11441-
(_b = (_f = this.parameters).includeUUIDs) !== null && _b !== void 0 ? _b : (_f.includeUUIDs = INCLUDE_UUID$1);
11442-
(_c = (_g = this.parameters).includeState) !== null && _c !== void 0 ? _c : (_g.includeState = INCLUDE_STATE);
11453+
(_a = (_d = this.parameters).queryParameters) !== null && _a !== void 0 ? _a : (_d.queryParameters = {});
11454+
(_b = (_e = this.parameters).includeUUIDs) !== null && _b !== void 0 ? _b : (_e.includeUUIDs = INCLUDE_UUID$1);
11455+
(_c = (_f = this.parameters).includeState) !== null && _c !== void 0 ? _c : (_f.includeState = INCLUDE_STATE);
1144311456
if (this.parameters.limit)
1144411457
this.parameters.limit = Math.min(this.parameters.limit, MAXIMUM_COUNT);
1144511458
else
1144611459
this.parameters.limit = MAXIMUM_COUNT;
11447-
(_d = (_h = this.parameters).offset) !== null && _d !== void 0 ? _d : (_h.offset = 0);
1144811460
}
1144911461
operation() {
1145011462
const { channels = [], channelGroups = [] } = this.parameters;
@@ -11465,6 +11477,8 @@
1146511477
const totalOccupancy = 'occupancy' in serviceResponse ? serviceResponse.occupancy : serviceResponse.payload.total_occupancy;
1146611478
const channelsPresence = {};
1146711479
let channels = {};
11480+
const limit = this.parameters.limit;
11481+
let occupancyMatchLimit = false;
1146811482
// Remap single channel presence to multiple channels presence response.
1146911483
if ('occupancy' in serviceResponse) {
1147011484
const channel = this.parameters.channels[0];
@@ -11485,11 +11499,12 @@
1148511499
name: channel,
1148611500
occupancy: channelEntry.occupancy,
1148711501
};
11502+
if (!occupancyMatchLimit && channelEntry.occupancy === limit)
11503+
occupancyMatchLimit = true;
1148811504
});
1148911505
return {
1149011506
totalChannels,
1149111507
totalOccupancy,
11492-
next: 0,
1149311508
channels: channelsPresence,
1149411509
};
1149511510
});
@@ -11502,8 +11517,8 @@
1150211517
return path;
1150311518
}
1150411519
get queryParameters() {
11505-
const { channelGroups, includeUUIDs, includeState, limit, offset, queryParameters } = this.parameters;
11506-
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (this.operation() === RequestOperation$1.PNHereNowOperation ? { limit } : {})), (this.operation() === RequestOperation$1.PNHereNowOperation && offset > 0 ? { offset } : {})), (!includeUUIDs ? { disable_uuids: '1' } : {})), ((includeState !== null && includeState !== void 0 ? includeState : false) ? { state: '1' } : {})), (channelGroups && channelGroups.length > 0 ? { 'channel-group': channelGroups.join(',') } : {})), queryParameters);
11520+
const { channelGroups, includeUUIDs, includeState, limit, queryParameters } = this.parameters;
11521+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (this.operation() === RequestOperation$1.PNHereNowOperation ? { limit } : {})), (!includeUUIDs ? { disable_uuids: '1' } : {})), ((includeState !== null && includeState !== void 0 ? includeState : false) ? { state: '1' } : {})), (channelGroups && channelGroups.length > 0 ? { 'channel-group': channelGroups.join(',') } : {})), queryParameters);
1150711522
}
1150811523
}
1150911524

@@ -16778,16 +16793,10 @@
1677816793
};
1677916794
if (callback)
1678016795
return this.sendRequest(request, (status, response) => {
16781-
var _a;
16782-
if (response && response.totalOccupancy === parameters.limit)
16783-
response.next = ((_a = parameters.offset) !== null && _a !== void 0 ? _a : 0) + 1;
1678416796
logResponse(response);
1678516797
callback(status, response);
1678616798
});
1678716799
return this.sendRequest(request).then((response) => {
16788-
var _a;
16789-
if (response && response.totalOccupancy === parameters.limit)
16790-
response.next = ((_a = parameters.offset) !== null && _a !== void 0 ? _a : 0) + 1;
1679116800
logResponse(response);
1679216801
return response;
1679316802
});

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.

lib/core/components/configuration.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ const makeConfiguration = (base, setupCryptoModule) => {
168168
return base.PubNubFile;
169169
},
170170
get version() {
171-
return '10.0.0';
171+
return '10.1.0';
172172
},
173173
getVersion() {
174174
return this.version;

lib/core/endpoints/presence/here_now.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,18 @@ const MAXIMUM_COUNT = 1000;
4545
*/
4646
class HereNowRequest extends request_1.AbstractRequest {
4747
constructor(parameters) {
48-
var _a, _b, _c, _d;
49-
var _e, _f, _g, _h;
48+
var _a, _b, _c;
49+
var _d, _e, _f;
5050
super();
5151
this.parameters = parameters;
5252
// Apply defaults.
53-
(_a = (_e = this.parameters).queryParameters) !== null && _a !== void 0 ? _a : (_e.queryParameters = {});
54-
(_b = (_f = this.parameters).includeUUIDs) !== null && _b !== void 0 ? _b : (_f.includeUUIDs = INCLUDE_UUID);
55-
(_c = (_g = this.parameters).includeState) !== null && _c !== void 0 ? _c : (_g.includeState = INCLUDE_STATE);
53+
(_a = (_d = this.parameters).queryParameters) !== null && _a !== void 0 ? _a : (_d.queryParameters = {});
54+
(_b = (_e = this.parameters).includeUUIDs) !== null && _b !== void 0 ? _b : (_e.includeUUIDs = INCLUDE_UUID);
55+
(_c = (_f = this.parameters).includeState) !== null && _c !== void 0 ? _c : (_f.includeState = INCLUDE_STATE);
5656
if (this.parameters.limit)
5757
this.parameters.limit = Math.min(this.parameters.limit, MAXIMUM_COUNT);
5858
else
5959
this.parameters.limit = MAXIMUM_COUNT;
60-
(_d = (_h = this.parameters).offset) !== null && _d !== void 0 ? _d : (_h.offset = 0);
6160
}
6261
operation() {
6362
const { channels = [], channelGroups = [] } = this.parameters;
@@ -78,6 +77,8 @@ class HereNowRequest extends request_1.AbstractRequest {
7877
const totalOccupancy = 'occupancy' in serviceResponse ? serviceResponse.occupancy : serviceResponse.payload.total_occupancy;
7978
const channelsPresence = {};
8079
let channels = {};
80+
const limit = this.parameters.limit;
81+
let occupancyMatchLimit = false;
8182
// Remap single channel presence to multiple channels presence response.
8283
if ('occupancy' in serviceResponse) {
8384
const channel = this.parameters.channels[0];
@@ -98,11 +99,12 @@ class HereNowRequest extends request_1.AbstractRequest {
9899
name: channel,
99100
occupancy: channelEntry.occupancy,
100101
};
102+
if (!occupancyMatchLimit && channelEntry.occupancy === limit)
103+
occupancyMatchLimit = true;
101104
});
102105
return {
103106
totalChannels,
104107
totalOccupancy,
105-
next: 0,
106108
channels: channelsPresence,
107109
};
108110
});
@@ -115,8 +117,8 @@ class HereNowRequest extends request_1.AbstractRequest {
115117
return path;
116118
}
117119
get queryParameters() {
118-
const { channelGroups, includeUUIDs, includeState, limit, offset, queryParameters } = this.parameters;
119-
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (this.operation() === operations_1.default.PNHereNowOperation ? { limit } : {})), (this.operation() === operations_1.default.PNHereNowOperation && offset > 0 ? { offset } : {})), (!includeUUIDs ? { disable_uuids: '1' } : {})), ((includeState !== null && includeState !== void 0 ? includeState : false) ? { state: '1' } : {})), (channelGroups && channelGroups.length > 0 ? { 'channel-group': channelGroups.join(',') } : {})), queryParameters);
120+
const { channelGroups, includeUUIDs, includeState, limit, queryParameters } = this.parameters;
121+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (this.operation() === operations_1.default.PNHereNowOperation ? { limit } : {})), (!includeUUIDs ? { disable_uuids: '1' } : {})), ((includeState !== null && includeState !== void 0 ? includeState : false) ? { state: '1' } : {})), (channelGroups && channelGroups.length > 0 ? { 'channel-group': channelGroups.join(',') } : {})), queryParameters);
120122
}
121123
}
122124
exports.HereNowRequest = HereNowRequest;

lib/core/pubnub-common.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,16 +1701,10 @@ class PubNubCore {
17011701
};
17021702
if (callback)
17031703
return this.sendRequest(request, (status, response) => {
1704-
var _a;
1705-
if (response && response.totalOccupancy === parameters.limit)
1706-
response.next = ((_a = parameters.offset) !== null && _a !== void 0 ? _a : 0) + 1;
17071704
logResponse(response);
17081705
callback(status, response);
17091706
});
17101707
return this.sendRequest(request).then((response) => {
1711-
var _a;
1712-
if (response && response.totalOccupancy === parameters.limit)
1713-
response.next = ((_a = parameters.offset) !== null && _a !== void 0 ? _a : 0) + 1;
17141708
logResponse(response);
17151709
return response;
17161710
});
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"use strict";
2+
/**
3+
* ICryptoModule adapter that delegates to the legacy Crypto implementation.
4+
*
5+
* This adapter bridges React Native's cipherKey configuration to the modern
6+
* ICryptoModule interface, ensuring backward compatibility with v10 apps
7+
* while supporting the new crypto module architecture.
8+
*
9+
* @internal This is an internal adapter and should not be used directly.
10+
*/
11+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
13+
return new (P || (P = Promise))(function (resolve, reject) {
14+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
15+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
16+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
17+
step((generator = generator.apply(thisArg, _arguments || [])).next());
18+
});
19+
};
20+
Object.defineProperty(exports, "__esModule", { value: true });
21+
const buffer_1 = require("buffer");
22+
class LegacyCryptoModule {
23+
/**
24+
* @param legacy - Configured legacy crypto instance
25+
* @throws {Error} When legacy crypto instance is not provided
26+
*/
27+
constructor(legacy) {
28+
this.legacy = legacy;
29+
if (!legacy) {
30+
throw new Error('Legacy crypto instance is required');
31+
}
32+
}
33+
/**
34+
* Set the logger manager for the legacy crypto instance.
35+
*
36+
* @param logger - The logger manager instance to use for logging
37+
*/
38+
set logger(logger) {
39+
this.legacy.logger = logger;
40+
}
41+
// --------------------------------------------------------
42+
// --------------------- Encryption -----------------------
43+
// --------------------------------------------------------
44+
/**
45+
* Encrypt data using the legacy cryptography implementation.
46+
*
47+
* @param data - The data to encrypt (string or ArrayBuffer)
48+
* @returns The encrypted data as a string
49+
* @throws {Error} When data is null/undefined or encryption fails
50+
*/
51+
encrypt(data) {
52+
if (data === null || data === undefined) {
53+
throw new Error('Encryption data cannot be null or undefined');
54+
}
55+
try {
56+
const plaintext = typeof data === 'string' ? data : buffer_1.Buffer.from(new Uint8Array(data)).toString('utf8');
57+
const encrypted = this.legacy.encrypt(plaintext);
58+
if (typeof encrypted !== 'string') {
59+
throw new Error('Legacy encryption failed: expected string result');
60+
}
61+
return encrypted;
62+
}
63+
catch (error) {
64+
throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
65+
}
66+
}
67+
encryptFile(_file, _File) {
68+
return __awaiter(this, void 0, void 0, function* () {
69+
// Not used on RN when cipherKey is set: file endpoints take the cipherKey + cryptography path.
70+
return undefined;
71+
});
72+
}
73+
// --------------------------------------------------------
74+
// --------------------- Decryption -----------------------
75+
// --------------------------------------------------------
76+
/**
77+
* Decrypt data using the legacy cryptography implementation.
78+
*
79+
* @param data - The encrypted data to decrypt (string or ArrayBuffer)
80+
* @returns The decrypted payload, or null if decryption fails
81+
* @throws {Error} When data is null/undefined/empty or decryption fails
82+
*/
83+
decrypt(data) {
84+
if (data === null || data === undefined) {
85+
throw new Error('Decryption data cannot be null or undefined');
86+
}
87+
try {
88+
let ciphertextB64;
89+
if (typeof data === 'string') {
90+
if (data.trim() === '') {
91+
throw new Error('Decryption data cannot be empty string');
92+
}
93+
ciphertextB64 = data;
94+
}
95+
else {
96+
if (data.byteLength === 0) {
97+
throw new Error('Decryption data cannot be empty ArrayBuffer');
98+
}
99+
ciphertextB64 = buffer_1.Buffer.from(new Uint8Array(data)).toString('base64');
100+
}
101+
const decrypted = this.legacy.decrypt(ciphertextB64);
102+
// The legacy decrypt method returns Payload | null, so no unsafe casting needed
103+
return decrypted;
104+
}
105+
catch (error) {
106+
throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
107+
}
108+
}
109+
decryptFile(_file, _File) {
110+
return __awaiter(this, void 0, void 0, function* () {
111+
// Not used on RN when cipherKey is set: file endpoints take the cipherKey + cryptography path.
112+
return undefined;
113+
});
114+
}
115+
}
116+
exports.default = LegacyCryptoModule;

0 commit comments

Comments
 (0)