Skip to content
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
15 changes: 12 additions & 3 deletions .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
---
changelog:
- date: 2025-09-30
version: v10.1.0
changes:
- type: feature
text: "Reintroduced legacy encryption and decryption functions for the React Native target to ensure backward compatibility."
- type: bug
text: "Resolves issue where presence heartbeat channels/groups sets were out of sync."
- type: improvement
text: "Temporarily remove the `offset` parameter until implementation synchronization across SDKs is completed."
- date: 2025-09-18
version: v10.0.0
changes:
Expand Down Expand Up @@ -1340,7 +1349,7 @@ supported-platforms:
- 'Ubuntu 14.04 and up'
- 'Windows 7 and up'
version: 'Pubnub Javascript for Node'
version: '10.0.0'
version: '10.1.0'
sdks:
- full-name: PubNub Javascript SDK
short-name: Javascript
Expand All @@ -1356,7 +1365,7 @@ sdks:
- distribution-type: source
distribution-repository: GitHub release
package-name: pubnub.js
location: https://github.com/pubnub/javascript/archive/refs/tags/v10.0.0.zip
location: https://github.com/pubnub/javascript/archive/refs/tags/v10.1.0.zip
requires:
- name: 'agentkeepalive'
min-version: '3.5.2'
Expand Down Expand Up @@ -2027,7 +2036,7 @@ sdks:
- distribution-type: library
distribution-repository: GitHub release
package-name: pubnub.js
location: https://github.com/pubnub/javascript/releases/download/v10.0.0/pubnub.10.0.0.js
location: https://github.com/pubnub/javascript/releases/download/v10.1.0/pubnub.10.1.0.js
requires:
- name: 'agentkeepalive'
min-version: '3.5.2'
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## v10.1.0
September 30 2025

#### Added
- 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).

#### Fixed
- Resolves issue where presence heartbeat channels/groups sets were out of sync.

#### Modified
- Temporarily remove the `offset` parameter until implementation synchronization across SDKs is completed.

## v10.0.0
September 18 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.10.0.0.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.10.0.0.min.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.10.1.0.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.10.1.0.min.js

2. Configure your keys:

Expand Down
41 changes: 25 additions & 16 deletions dist/web/pubnub.js
Original file line number Diff line number Diff line change
Expand Up @@ -5436,7 +5436,7 @@
return base.PubNubFile;
},
get version() {
return '10.0.0';
return '10.1.0';
},
getVersion() {
return this.version;
Expand Down Expand Up @@ -8923,13 +8923,26 @@
this.engine.transition(joined(this.channels.slice(0), this.groups.slice(0)));
}
leave({ channels, groups }) {
// Update internal channel tracking to prevent stale heartbeat requests
if (channels)
this.channels = this.channels.filter((channel) => !channels.includes(channel));
if (groups)
this.groups = this.groups.filter((group) => !groups.includes(group));
if (this.dependencies.presenceState) {
channels === null || channels === void 0 ? void 0 : channels.forEach((c) => delete this.dependencies.presenceState[c]);
groups === null || groups === void 0 ? void 0 : groups.forEach((g) => delete this.dependencies.presenceState[g]);
}
this.engine.transition(left(channels !== null && channels !== void 0 ? channels : [], groups !== null && groups !== void 0 ? groups : []));
}
leaveAll(isOffline = false) {
// Clear presence state for all current channels and groups
if (this.dependencies.presenceState) {
this.channels.forEach((c) => delete this.dependencies.presenceState[c]);
this.groups.forEach((g) => delete this.dependencies.presenceState[g]);
}
// Reset internal channel and group tracking
this.channels = [];
this.groups = [];
this.engine.transition(leftAll(isOffline));
}
reconnect() {
Expand Down Expand Up @@ -11432,19 +11445,18 @@
*/
class HereNowRequest extends AbstractRequest {
constructor(parameters) {
var _a, _b, _c, _d;
var _e, _f, _g, _h;
var _a, _b, _c;
var _d, _e, _f;
super();
this.parameters = parameters;
// Apply defaults.
(_a = (_e = this.parameters).queryParameters) !== null && _a !== void 0 ? _a : (_e.queryParameters = {});
(_b = (_f = this.parameters).includeUUIDs) !== null && _b !== void 0 ? _b : (_f.includeUUIDs = INCLUDE_UUID$1);
(_c = (_g = this.parameters).includeState) !== null && _c !== void 0 ? _c : (_g.includeState = INCLUDE_STATE);
(_a = (_d = this.parameters).queryParameters) !== null && _a !== void 0 ? _a : (_d.queryParameters = {});
(_b = (_e = this.parameters).includeUUIDs) !== null && _b !== void 0 ? _b : (_e.includeUUIDs = INCLUDE_UUID$1);
(_c = (_f = this.parameters).includeState) !== null && _c !== void 0 ? _c : (_f.includeState = INCLUDE_STATE);
if (this.parameters.limit)
this.parameters.limit = Math.min(this.parameters.limit, MAXIMUM_COUNT);
else
this.parameters.limit = MAXIMUM_COUNT;
(_d = (_h = this.parameters).offset) !== null && _d !== void 0 ? _d : (_h.offset = 0);
}
operation() {
const { channels = [], channelGroups = [] } = this.parameters;
Expand All @@ -11465,6 +11477,8 @@
const totalOccupancy = 'occupancy' in serviceResponse ? serviceResponse.occupancy : serviceResponse.payload.total_occupancy;
const channelsPresence = {};
let channels = {};
const limit = this.parameters.limit;
let occupancyMatchLimit = false;
// Remap single channel presence to multiple channels presence response.
if ('occupancy' in serviceResponse) {
const channel = this.parameters.channels[0];
Expand All @@ -11485,11 +11499,12 @@
name: channel,
occupancy: channelEntry.occupancy,
};
if (!occupancyMatchLimit && channelEntry.occupancy === limit)
occupancyMatchLimit = true;
});
return {
totalChannels,
totalOccupancy,
next: 0,
channels: channelsPresence,
};
});
Expand All @@ -11502,8 +11517,8 @@
return path;
}
get queryParameters() {
const { channelGroups, includeUUIDs, includeState, limit, offset, queryParameters } = this.parameters;
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);
const { channelGroups, includeUUIDs, includeState, limit, queryParameters } = this.parameters;
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);
}
}

Expand Down Expand Up @@ -16778,16 +16793,10 @@
};
if (callback)
return this.sendRequest(request, (status, response) => {
var _a;
if (response && response.totalOccupancy === parameters.limit)
response.next = ((_a = parameters.offset) !== null && _a !== void 0 ? _a : 0) + 1;
logResponse(response);
callback(status, response);
});
return this.sendRequest(request).then((response) => {
var _a;
if (response && response.totalOccupancy === parameters.limit)
response.next = ((_a = parameters.offset) !== null && _a !== void 0 ? _a : 0) + 1;
logResponse(response);
return response;
});
Expand Down
2 changes: 1 addition & 1 deletion dist/web/pubnub.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 @@ -168,7 +168,7 @@ const makeConfiguration = (base, setupCryptoModule) => {
return base.PubNubFile;
},
get version() {
return '10.0.0';
return '10.1.0';
},
getVersion() {
return this.version;
Expand Down
20 changes: 11 additions & 9 deletions lib/core/endpoints/presence/here_now.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,18 @@ const MAXIMUM_COUNT = 1000;
*/
class HereNowRequest extends request_1.AbstractRequest {
constructor(parameters) {
var _a, _b, _c, _d;
var _e, _f, _g, _h;
var _a, _b, _c;
var _d, _e, _f;
super();
this.parameters = parameters;
// Apply defaults.
(_a = (_e = this.parameters).queryParameters) !== null && _a !== void 0 ? _a : (_e.queryParameters = {});
(_b = (_f = this.parameters).includeUUIDs) !== null && _b !== void 0 ? _b : (_f.includeUUIDs = INCLUDE_UUID);
(_c = (_g = this.parameters).includeState) !== null && _c !== void 0 ? _c : (_g.includeState = INCLUDE_STATE);
(_a = (_d = this.parameters).queryParameters) !== null && _a !== void 0 ? _a : (_d.queryParameters = {});
(_b = (_e = this.parameters).includeUUIDs) !== null && _b !== void 0 ? _b : (_e.includeUUIDs = INCLUDE_UUID);
(_c = (_f = this.parameters).includeState) !== null && _c !== void 0 ? _c : (_f.includeState = INCLUDE_STATE);
if (this.parameters.limit)
this.parameters.limit = Math.min(this.parameters.limit, MAXIMUM_COUNT);
else
this.parameters.limit = MAXIMUM_COUNT;
(_d = (_h = this.parameters).offset) !== null && _d !== void 0 ? _d : (_h.offset = 0);
}
operation() {
const { channels = [], channelGroups = [] } = this.parameters;
Expand All @@ -78,6 +77,8 @@ class HereNowRequest extends request_1.AbstractRequest {
const totalOccupancy = 'occupancy' in serviceResponse ? serviceResponse.occupancy : serviceResponse.payload.total_occupancy;
const channelsPresence = {};
let channels = {};
const limit = this.parameters.limit;
let occupancyMatchLimit = false;
// Remap single channel presence to multiple channels presence response.
if ('occupancy' in serviceResponse) {
const channel = this.parameters.channels[0];
Expand All @@ -98,11 +99,12 @@ class HereNowRequest extends request_1.AbstractRequest {
name: channel,
occupancy: channelEntry.occupancy,
};
if (!occupancyMatchLimit && channelEntry.occupancy === limit)
occupancyMatchLimit = true;
});
return {
totalChannels,
totalOccupancy,
next: 0,
channels: channelsPresence,
};
});
Expand All @@ -115,8 +117,8 @@ class HereNowRequest extends request_1.AbstractRequest {
return path;
}
get queryParameters() {
const { channelGroups, includeUUIDs, includeState, limit, offset, queryParameters } = this.parameters;
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);
const { channelGroups, includeUUIDs, includeState, limit, queryParameters } = this.parameters;
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);
}
}
exports.HereNowRequest = HereNowRequest;
6 changes: 0 additions & 6 deletions lib/core/pubnub-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -1701,16 +1701,10 @@ class PubNubCore {
};
if (callback)
return this.sendRequest(request, (status, response) => {
var _a;
if (response && response.totalOccupancy === parameters.limit)
response.next = ((_a = parameters.offset) !== null && _a !== void 0 ? _a : 0) + 1;
logResponse(response);
callback(status, response);
});
return this.sendRequest(request).then((response) => {
var _a;
if (response && response.totalOccupancy === parameters.limit)
response.next = ((_a = parameters.offset) !== null && _a !== void 0 ? _a : 0) + 1;
logResponse(response);
return response;
});
Expand Down
116 changes: 116 additions & 0 deletions lib/crypto/modules/LegacyCryptoModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"use strict";
/**
* ICryptoModule adapter that delegates to the legacy Crypto implementation.
*
* This adapter bridges React Native's cipherKey configuration to the modern
* ICryptoModule interface, ensuring backward compatibility with v10 apps
* while supporting the new crypto module architecture.
*
* @internal This is an internal adapter and should not be used directly.
*/
var __awaiter = (this && this.__awaiter) || function (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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const buffer_1 = require("buffer");
class LegacyCryptoModule {
/**
* @param legacy - Configured legacy crypto instance
* @throws {Error} When legacy crypto instance is not provided
*/
constructor(legacy) {
this.legacy = legacy;
if (!legacy) {
throw new Error('Legacy crypto instance is required');
}
}
/**
* Set the logger manager for the legacy crypto instance.
*
* @param logger - The logger manager instance to use for logging
*/
set logger(logger) {
this.legacy.logger = logger;
}
// --------------------------------------------------------
// --------------------- Encryption -----------------------
// --------------------------------------------------------
/**
* Encrypt data using the legacy cryptography implementation.
*
* @param data - The data to encrypt (string or ArrayBuffer)
* @returns The encrypted data as a string
* @throws {Error} When data is null/undefined or encryption fails
*/
encrypt(data) {
if (data === null || data === undefined) {
throw new Error('Encryption data cannot be null or undefined');
}
try {
const plaintext = typeof data === 'string' ? data : buffer_1.Buffer.from(new Uint8Array(data)).toString('utf8');
const encrypted = this.legacy.encrypt(plaintext);
if (typeof encrypted !== 'string') {
throw new Error('Legacy encryption failed: expected string result');
}
return encrypted;
}
catch (error) {
throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
encryptFile(_file, _File) {
return __awaiter(this, void 0, void 0, function* () {
// Not used on RN when cipherKey is set: file endpoints take the cipherKey + cryptography path.
return undefined;
});
}
// --------------------------------------------------------
// --------------------- Decryption -----------------------
// --------------------------------------------------------
/**
* Decrypt data using the legacy cryptography implementation.
*
* @param data - The encrypted data to decrypt (string or ArrayBuffer)
* @returns The decrypted payload, or null if decryption fails
* @throws {Error} When data is null/undefined/empty or decryption fails
*/
decrypt(data) {
if (data === null || data === undefined) {
throw new Error('Decryption data cannot be null or undefined');
}
try {
let ciphertextB64;
if (typeof data === 'string') {
if (data.trim() === '') {
throw new Error('Decryption data cannot be empty string');
}
ciphertextB64 = data;
}
else {
if (data.byteLength === 0) {
throw new Error('Decryption data cannot be empty ArrayBuffer');
}
ciphertextB64 = buffer_1.Buffer.from(new Uint8Array(data)).toString('base64');
}
const decrypted = this.legacy.decrypt(ciphertextB64);
// The legacy decrypt method returns Payload | null, so no unsafe casting needed
return decrypted;
}
catch (error) {
throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
decryptFile(_file, _File) {
return __awaiter(this, void 0, void 0, function* () {
// Not used on RN when cipherKey is set: file endpoints take the cipherKey + cryptography path.
return undefined;
});
}
}
exports.default = LegacyCryptoModule;
Loading
Loading