From 66085b92a1f19b21dd9452432aecc4d34dbea67a Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 4 Apr 2023 15:11:46 +0100 Subject: [PATCH 01/46] refactor: fix ClientOptions.agents typing --- src/common/types/ClientOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/types/ClientOptions.ts b/src/common/types/ClientOptions.ts index cb2694c3e5..70a6b1a06b 100644 --- a/src/common/types/ClientOptions.ts +++ b/src/common/types/ClientOptions.ts @@ -9,7 +9,7 @@ export type RestAgentOptions = { export default interface ClientOptions extends API.Types.ClientOptions { restAgentOptions?: RestAgentOptions; pushFullWait?: boolean; - agents?: string[]; + agents?: Record; } export type DeprecatedClientOptions = Modify< From e44103c37238989bb2f0f61d8b3ad54f0747bfa7 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 25 Apr 2023 10:14:59 +0100 Subject: [PATCH 02/46] docs: document required NodeJS build version --- CONTRIBUTING.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fc72ee060..dd6437ab46 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,12 @@ 11. For nontrivial releases: update the ably-js submodule ref in the realtime repo 12. Update the [Ably Changelog](https://changelog.ably.com/) (via [headwayapp](https://headwayapp.co/)) with these changes (again, you can just copy the notes you added to the CHANGELOG) +## Building the library + +To build the library, simply run `npm run build`. Building the library currently requires NodeJS <= v16. + +Since webpack builds are slow, commands are also available to only build the output for specific platforms (eg `npm run build:node`), see [package.json](./package.json) for the full list of available commands + ## Test suite To run the Mocha tests, simply run the following command: From a1f6f64d3b360d79da02c6090a9fdb31584b9829 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 21 Jun 2023 14:19:09 +0530 Subject: [PATCH 03/46] Added separate getRetryTime utils function with proper documentation --- src/common/lib/util/utils.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/common/lib/util/utils.ts b/src/common/lib/util/utils.ts index 6c76ce7f85..5a1063e7f4 100644 --- a/src/common/lib/util/utils.ts +++ b/src/common/lib/util/utils.ts @@ -531,14 +531,28 @@ export function allToUpperCase(arr: Array): Array { }); } -export function getBackoffCoefficient(n: number) { - return Math.min((n + 2) / 3, 2); +export function getBackoffCoefficient(count: number) { + return Math.min((count + 2) / 3, 2); } export function getJitterCoefficient() { return 1 - Math.random() * 0.2; } +/** + * + * @param initialTimeout initial timeout value + * @param retryAttempt integer indicating retryAttempt + * @returns RetryTimeout value for given timeout and retryAttempt. + * If x is the value generated then, + * Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout, + * Lower bound = 0.8 * Upper bound, + * Lower bound < x < Upper bound + */ +export function getRetryTime(initialTimeout: number, retryAttempt: number) { + return initialTimeout * getBackoffCoefficient(retryAttempt) * getJitterCoefficient(); +} + export function getGlobalObject() { if (global) { return global; From 3ec3d161e4198ae6bec020e716e5dc1bd0684656 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 14:41:23 +0000 Subject: [PATCH 04/46] retryDelay calculated using utils method --- src/common/lib/client/realtimechannel.ts | 5 +---- src/common/lib/transport/connectionmanager.ts | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 58a4abf595..4844025a19 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -929,10 +929,7 @@ class RealtimeChannel extends Channel { if (this.retryTimer) return; this.retryCount++; - const retryDelay = - this.realtime.options.timeouts.channelRetryTimeout * - Utils.getJitterCoefficient() * - Utils.getBackoffCoefficient(this.retryCount); + const retryDelay = Utils.getRetryTime(this.realtime.options.timeouts.channelRetryTimeout, this.retryCount); this.retryTimer = setTimeout(() => { /* If connection is not connected, just leave in suspended, a reattach diff --git a/src/common/lib/transport/connectionmanager.ts b/src/common/lib/transport/connectionmanager.ts index 7f12205900..6f8b82f71f 100644 --- a/src/common/lib/transport/connectionmanager.ts +++ b/src/common/lib/transport/connectionmanager.ts @@ -1320,10 +1320,7 @@ class ConnectionManager extends EventEmitter { let retryDelay = newState.retryDelay; if (newState.state === 'disconnected') { this.disconnectedRetryCount++; - retryDelay = - (newState.retryDelay as number) * - Utils.getBackoffCoefficient(this.disconnectedRetryCount) * - Utils.getJitterCoefficient(); + retryDelay = Utils.getRetryTime(newState.retryDelay as number, this.disconnectedRetryCount); } const change = new ConnectionStateChange( From 1034b2d6fcf9feb664e0ce5a50455773618cc7b9 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 16:57:42 +0000 Subject: [PATCH 05/46] Added nvmrc file for selecting right node version by nvm --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..53d0020fde --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v16.20.1 From 597022e0896ca5f1805f7bd8397382e9117cdcc7 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 17:17:20 +0000 Subject: [PATCH 06/46] Updated contributing file for testname --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4faa7a424a..ea41d9021a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,10 @@ Or run just one test file npm run test:node -- --file=test/realtime/auth.test.js +Or run just one test + + npm run test:node -- --file=test/rest/status.test.js --grep=test_name_here + ### Debugging the mocha tests locally with a debugger Run the following command to launch tests with the debugger enabled. The tests will block until you attach a debugger. From 1d9037776b16e177e31114fdc684d636d14226fd Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 17:24:04 +0000 Subject: [PATCH 07/46] Updated contributing doc with VScode single test run --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea41d9021a..e8a1c93a89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,6 +61,10 @@ The included vscode launch config allows you to launch and attach the debugger i file you want to run and start debugging. Note that breakpoint setting for realtime code will be within the browser/static directory, not the raw source files, and breakpoints in files under test should work directly. +VSCode allows you to directly launch `package.json` scripts in debug mode. +Simply click on `Debug` option above `scripts` and select the command to run. +This way, you can add a command to run a single test as a part of script and debug directly from package.json. + ### Debugging the tests in a browser with Mocha test runner Run the following command to start a local Mocha test runner web server From a4f5276c0cae7d72d800abfc5f53044bc72f7e2c Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 17:24:59 +0000 Subject: [PATCH 08/46] Fixed contributing.md doc --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8a1c93a89..58ef2102a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ browser/static directory, not the raw source files, and breakpoints in files und VSCode allows you to directly launch `package.json` scripts in debug mode. Simply click on `Debug` option above `scripts` and select the command to run. -This way, you can add a command to run a single test as a part of script and debug directly from package.json. +This way, you can add a command to run a single test as a part of `scripts` and debug directly from package.json. ### Debugging the tests in a browser with Mocha test runner From e9ddfd37f5c1a5530294f93b61e5c34a320e9da9 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 18:16:06 +0000 Subject: [PATCH 09/46] added test to calculate incremental backoff and jitter --- test/realtime/utils.test.js | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/realtime/utils.test.js diff --git a/test/realtime/utils.test.js b/test/realtime/utils.test.js new file mode 100644 index 0000000000..0ee29b0f9d --- /dev/null +++ b/test/realtime/utils.test.js @@ -0,0 +1,49 @@ +'use strict'; + +define(['shared_helper', 'chai'], function (helper, chai) { + + var utils = helper.Utils; + var expect = chai.expect; + + // RSL8 + describe('incremental backoff and jitter', function () { + this.timeout(30 * 1000); + + it('should calculate retry timeouts using incremental backoff and jitter', function () { + var retryAttempts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + var initialTimeout = 15; + + var retryTimeouts = retryAttempts.map(attempt => utils.getRetryTime(initialTimeout, attempt)); + expect(retryTimeouts.filter(timeout => timeout >= 30).length).to.equal(0); + + function checkIsBetween(value, min, max) { + expect(value).to.be.above(min); + expect(value).to.be.below(max); + }; + + // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout + // Lower bound = 0.8 * Upper bound + checkIsBetween(retryTimeouts[0], 12, 15); + checkIsBetween(retryTimeouts[1], 16, 20); + checkIsBetween(retryTimeouts[2], 20, 25); + + for (var i = 3; i < retryTimeouts.length; i++) + { + checkIsBetween(retryTimeouts[i], 24, 30); + } + + function calculateBounds(retryAttempt, initialTimeout) { + var upperBound = Math.min((retryAttempt + 2) / 3, 2) * initialTimeout; + var lowerBound = 0.8 * upperBound; + return {lowerBound, upperBound}; + } + + for (var i = 0; i < retryTimeouts.length; i++) + { + var {lowerBound, upperBound} = calculateBounds(retryAttempts[i], initialTimeout); + checkIsBetween(retryTimeouts[i], lowerBound, upperBound); + } + + }); + }); +}); From 89d9f38c8478af27887b3a93d56d62c7e9de9719 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 18:18:54 +0000 Subject: [PATCH 10/46] added correct spec id for the test --- test/realtime/utils.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/realtime/utils.test.js b/test/realtime/utils.test.js index 0ee29b0f9d..c1a83d0d28 100644 --- a/test/realtime/utils.test.js +++ b/test/realtime/utils.test.js @@ -5,7 +5,7 @@ define(['shared_helper', 'chai'], function (helper, chai) { var utils = helper.Utils; var expect = chai.expect; - // RSL8 + // RTB1 describe('incremental backoff and jitter', function () { this.timeout(30 * 1000); From d2b4012047d024fca500c810250436f26b860bff Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 18:42:36 +0000 Subject: [PATCH 11/46] updated disconnected_backoff and channel_backoff test --- test/realtime/failure.test.js | 34 +++++++++++++++++++++++++--------- test/realtime/utils.test.js | 1 - 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/test/realtime/failure.test.js b/test/realtime/failure.test.js index 03dd02757e..6094c8b49e 100644 --- a/test/realtime/failure.test.js +++ b/test/realtime/failure.test.js @@ -185,6 +185,24 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async } }); + function checkIfRetryTimeoutsAreCorrect(retryTimeouts, calculationDelay = 0) { + function checkIsBetween(value, min, max) { + expect(value).to.be.above(min); + expect(value).to.be.below(max); + }; + + // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout + // Lower bound = 0.8 * Upper bound + checkIsBetween(retryTimeouts[0], 120, 150 + calculationDelay); + checkIsBetween(retryTimeouts[1], 160, 200 + calculationDelay); + checkIsBetween(retryTimeouts[2], 200, 250 + calculationDelay); + + for (var i = 3; i < retryTimeouts.length; i++) + { + checkIsBetween(retryTimeouts[i], 240, 300 + calculationDelay); + } + } + utils.arrForEach(availableTransports, function (transport) { it('disconnected_backoff_' + transport, function (done) { var disconnectedRetryTimeout = 150; @@ -196,16 +214,17 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }); var retryCount = 0; + var retryTimeouts = []; realtime.connection.on(function (stateChange) { if (stateChange.previous === 'connecting' && stateChange.current === 'disconnected') { if (retryCount > 4) { + checkIfRetryTimeoutsAreCorrect(retryTimeouts); closeAndFinish(done, realtime); return; } try { - expect(stateChange.retryIn).to.be.below(disconnectedRetryTimeout + Math.min(retryCount, 3) * 50); - expect(stateChange.retryIn).to.be.above(0.8 * (disconnectedRetryTimeout + Math.min(retryCount, 3) * 50)); + retryTimeouts.push(stateChange.retryIn); retryCount += 1; } catch (err) { closeAndFinish(done, realtime, err); @@ -379,6 +398,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async originalOnMessage(message); }; + var retryTimeouts = []; + realtime.connection.on('connected', function () { realtime.options.timeouts.realtimeRequestTimeout = 1; channel.attach(function (err) { @@ -387,19 +408,14 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async channel.on(function (stateChange) { if (stateChange.current === 'suspended') { if (retryCount > 4) { + checkIfRetryTimeoutsAreCorrect(retryTimeouts, 10); closeAndFinish(done, realtime); return; } var elapsedSinceSuspneded = performance.now() - lastSuspended; lastSuspended = performance.now(); try { - // JS timers don't work precisely and realtimeRequestTimeout can't be 0 so add 5ms to the max timeout length each retry - expect(elapsedSinceSuspneded).to.be.below( - channelRetryTimeout + Math.min(retryCount, 3) * 50 + 5 * (retryCount + 1) - ); - expect(elapsedSinceSuspneded).to.be.above( - 0.8 * (channelRetryTimeout + Math.min(retryCount, 3) * 50) - ); + retryTimeouts.push(elapsedSinceSuspneded); retryCount += 1; } catch (err) { closeAndFinish(done, realtime, err); diff --git a/test/realtime/utils.test.js b/test/realtime/utils.test.js index c1a83d0d28..9879a6c5bf 100644 --- a/test/realtime/utils.test.js +++ b/test/realtime/utils.test.js @@ -7,7 +7,6 @@ define(['shared_helper', 'chai'], function (helper, chai) { // RTB1 describe('incremental backoff and jitter', function () { - this.timeout(30 * 1000); it('should calculate retry timeouts using incremental backoff and jitter', function () { var retryAttempts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; From 5adf2887816501f1b183fb1cfc5964ba752b7963 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 18:47:21 +0000 Subject: [PATCH 12/46] properly formatted js files --- src/common/lib/util/utils.ts | 8 ++++---- test/realtime/failure.test.js | 5 ++--- test/realtime/utils.test.js | 21 ++++++++------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/common/lib/util/utils.ts b/src/common/lib/util/utils.ts index 5a1063e7f4..24077849fb 100644 --- a/src/common/lib/util/utils.ts +++ b/src/common/lib/util/utils.ts @@ -540,17 +540,17 @@ export function getJitterCoefficient() { } /** - * + * * @param initialTimeout initial timeout value - * @param retryAttempt integer indicating retryAttempt + * @param retryAttempt integer indicating retryAttempt * @returns RetryTimeout value for given timeout and retryAttempt. - * If x is the value generated then, + * If x is the value generated then, * Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout, * Lower bound = 0.8 * Upper bound, * Lower bound < x < Upper bound */ export function getRetryTime(initialTimeout: number, retryAttempt: number) { - return initialTimeout * getBackoffCoefficient(retryAttempt) * getJitterCoefficient(); + return initialTimeout * getBackoffCoefficient(retryAttempt) * getJitterCoefficient(); } export function getGlobalObject() { diff --git a/test/realtime/failure.test.js b/test/realtime/failure.test.js index 6094c8b49e..fb8f88d551 100644 --- a/test/realtime/failure.test.js +++ b/test/realtime/failure.test.js @@ -189,7 +189,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async function checkIsBetween(value, min, max) { expect(value).to.be.above(min); expect(value).to.be.below(max); - }; + } // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout // Lower bound = 0.8 * Upper bound @@ -197,8 +197,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async checkIsBetween(retryTimeouts[1], 160, 200 + calculationDelay); checkIsBetween(retryTimeouts[2], 200, 250 + calculationDelay); - for (var i = 3; i < retryTimeouts.length; i++) - { + for (var i = 3; i < retryTimeouts.length; i++) { checkIsBetween(retryTimeouts[i], 240, 300 + calculationDelay); } } diff --git a/test/realtime/utils.test.js b/test/realtime/utils.test.js index 9879a6c5bf..fa1d855e62 100644 --- a/test/realtime/utils.test.js +++ b/test/realtime/utils.test.js @@ -1,24 +1,22 @@ 'use strict'; define(['shared_helper', 'chai'], function (helper, chai) { - var utils = helper.Utils; var expect = chai.expect; // RTB1 describe('incremental backoff and jitter', function () { - it('should calculate retry timeouts using incremental backoff and jitter', function () { var retryAttempts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; var initialTimeout = 15; - var retryTimeouts = retryAttempts.map(attempt => utils.getRetryTime(initialTimeout, attempt)); - expect(retryTimeouts.filter(timeout => timeout >= 30).length).to.equal(0); + var retryTimeouts = retryAttempts.map((attempt) => utils.getRetryTime(initialTimeout, attempt)); + expect(retryTimeouts.filter((timeout) => timeout >= 30).length).to.equal(0); function checkIsBetween(value, min, max) { expect(value).to.be.above(min); expect(value).to.be.below(max); - }; + } // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout // Lower bound = 0.8 * Upper bound @@ -26,23 +24,20 @@ define(['shared_helper', 'chai'], function (helper, chai) { checkIsBetween(retryTimeouts[1], 16, 20); checkIsBetween(retryTimeouts[2], 20, 25); - for (var i = 3; i < retryTimeouts.length; i++) - { + for (var i = 3; i < retryTimeouts.length; i++) { checkIsBetween(retryTimeouts[i], 24, 30); } function calculateBounds(retryAttempt, initialTimeout) { var upperBound = Math.min((retryAttempt + 2) / 3, 2) * initialTimeout; var lowerBound = 0.8 * upperBound; - return {lowerBound, upperBound}; + return { lowerBound, upperBound }; } - for (var i = 0; i < retryTimeouts.length; i++) - { - var {lowerBound, upperBound} = calculateBounds(retryAttempts[i], initialTimeout); - checkIsBetween(retryTimeouts[i], lowerBound, upperBound); + for (var i = 0; i < retryTimeouts.length; i++) { + var { lowerBound, upperBound } = calculateBounds(retryAttempts[i], initialTimeout); + checkIsBetween(retryTimeouts[i], lowerBound, upperBound); } - }); }); }); From a676c2f7b84c7993358ba3b2dc9f1f5ede33107e Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Sat, 24 Jun 2023 18:51:21 +0000 Subject: [PATCH 13/46] Added section for formatting and linting --- CONTRIBUTING.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58ef2102a9..49fc9fee80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,12 @@ Run the following command to start a local Mocha test runner web server Open your browser to [http://localhost:3000](http://localhost:3000). If you are using a remote browser, refer to https://docs.saucelabs.com/reference/sauce-connect/ for instructions on setting up a local tunnel to your Mocha runner web server. +### Formatting/linting files + +Run the following command to fix linting/formatting issues + + npm run format + ### Testing environment variables for Node.js All tests are run against the sandbox environment by default. However, the following environment variables can be set before running the Karma server to change the environment the tests are run against. From 23f8a73ac38a2537c8185a67f32be69729533096 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 27 Jun 2023 17:01:11 +0100 Subject: [PATCH 14/46] fix(ConnectionManager): pass connectCount through connectImpl --- src/common/lib/transport/connectionmanager.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/lib/transport/connectionmanager.ts b/src/common/lib/transport/connectionmanager.ts index 7f12205900..cfd765b2b3 100644 --- a/src/common/lib/transport/connectionmanager.ts +++ b/src/common/lib/transport/connectionmanager.ts @@ -1529,19 +1529,19 @@ class ConnectionManager extends EventEmitter { } else if (state == this.states.connected.state) { this.upgradeIfNeeded(transportParams); } else if (this.transports.length > 1 && this.getTransportPreference()) { - this.connectPreference(transportParams); + this.connectPreference(transportParams, connectCount); } else { this.connectBase(transportParams, connectCount); } } - connectPreference(transportParams: TransportParams): void { + connectPreference(transportParams: TransportParams, connectCount?: number): void { const preference = this.getTransportPreference(); let preferenceTimeoutExpired = false; if (!Utils.arrIn(this.transports, preference)) { this.unpersistTransportPreference(); - this.connectImpl(transportParams); + this.connectImpl(transportParams, connectCount); } Logger.logAction( @@ -1564,7 +1564,7 @@ class ConnectionManager extends EventEmitter { /* Be quite agressive about clearing the stored preference if ever it doesn't work */ this.unpersistTransportPreference(); } - this.connectImpl(transportParams); + this.connectImpl(transportParams, connectCount); }, this.options.timeouts.preferenceConnectTimeout); /* For connectPreference, just use the main host. If host fallback is needed, do it in connectBase. @@ -1582,7 +1582,7 @@ class ConnectionManager extends EventEmitter { } else if (!transport && !fatal) { /* Preference failed in a transport-specific way. Try more */ this.unpersistTransportPreference(); - this.connectImpl(transportParams); + this.connectImpl(transportParams, connectCount); } /* If suceeded, or failed fatally, nothing to do */ }); From 8a50060e08133b4077d3224821003e71cc1ec921 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 22 Jun 2023 10:50:39 +0100 Subject: [PATCH 15/46] feat: `RealtimeChannel.attach` returns `ChannelStateChange` Makes it so that `RealtimeChannel.attach` exposes the `ChannelStateChange` via whatever async api (invoke callback or fulfill promise). This makes it easy for users to access flags on the `ChannelStateChange` to access information about the attachment. --- ably.d.ts | 52 ++++++++++++-------- src/common/lib/client/realtimechannel.ts | 19 +++++--- test/realtime/channel.test.js | 61 ++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 26 deletions(-) diff --git a/ably.d.ts b/ably.d.ts index bc92e8565e..f404077c14 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -2556,9 +2556,9 @@ declare namespace Types { /** * Attach to this channel ensuring the channel is created in the Ably system and all messages published on the channel are received by any channel listeners registered using {@link RealtimeChannelCallbacks.subscribe | `subscribe()`}. Any resulting channel state change will be emitted to any listeners registered using the {@link EventEmitter.on | `on()`} or {@link EventEmitter.once | `once()`} methods. As a convenience, `attach()` is called implicitly if {@link RealtimeChannelCallbacks.subscribe | `subscribe()`} for the channel is called, or {@link RealtimePresenceCallbacks.enter | `enter()`} or {@link RealtimePresenceCallbacks.subscribe | `subscribe()`} are called on the {@link RealtimePresenceCallbacks} object for this channel. * - * @param callback - A function which will be called upon completion of the operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. + * @param callback - A function which will be called upon completion of the operation. If the operation succeeded and the channel became attached, then the function will be called with a {@link ChannelStateChange} object. If the channel was already attached the function will be called with `null`. If it failed, the function will be called with information about the error. */ - attach(callback?: errorCallback): void; + attach(callback?: StandardCallback): void; /** * Detach from this channel. Any resulting channel state change is emitted to any listeners registered using the {@link EventEmitter.on | `on()`} or {@link EventEmitter.once | `once()`} methods. Once all clients globally have detached from the channel, the channel will be released in the Ably service within two minutes. * @@ -2590,32 +2590,44 @@ declare namespace Types { * * @param event - The event name. * @param listener - An event listener function. - * @param callbackWhenAttached - A function which will be called upon completion of the channel {@link RealtimeChannelCallbacks.attach | `attach()`} operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. + * @param callbackWhenAttached - A function which will be called upon completion of the channel {@link RealtimeChannelCallbacks.attach | `attach()`} operation. If the operation succeeded and the channel became attached, then the function will be called with a {@link ChannelStateChange} object. If the channel was already attached the function will be called with `null`. If it failed, the function will be called with information about the error. */ - subscribe(event: string, listener?: messageCallback, callbackWhenAttached?: errorCallback): void; + subscribe( + event: string, + listener?: messageCallback, + callbackWhenAttached?: StandardCallback + ): void; /** * Registers a listener for messages on this channel for multiple event name values. * * @param events - An array of event names. * @param listener - An event listener function. - * @param callbackWhenAttached - A function which will be called upon completion of the channel {@link RealtimeChannelCallbacks.attach | `attach()`} operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. + * @param callbackWhenAttached - A function which will be called upon completion of the channel {@link RealtimeChannelCallbacks.attach | `attach()`} operation. If the operation succeeded and the channel became attached, then the function will be called with a {@link ChannelStateChange} object. If the channel was already attached the function will be called with `null`. If it failed, the function will be called with information about the error. */ - subscribe(events: Array, listener?: messageCallback, callbackWhenAttached?: errorCallback): void; + subscribe( + events: Array, + listener?: messageCallback, + callbackWhenAttached?: StandardCallback + ): void; /** * Registers a listener for messages on this channel that match the supplied filter. * * @param filter - A {@link MessageFilter}. * @param listener - An event listener function. - * @param callbackWhenAttached - A function which will be called upon completion of the channel {@link RealtimeChannelCallbacks.attach | `attach()`} operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. + * @param callbackWhenAttached - A function which will be called upon completion of the channel {@link RealtimeChannelCallbacks.attach | `attach()`} operation. If the operation succeeded and the channel became attached, then the function will be called with a {@link ChannelStateChange} object. If the channel was already attached the function will be called with `null`. If it failed, the function will be called with information about the error. */ - subscribe(filter: MessageFilter, listener?: messageCallback, callbackWhenAttached?: errorCallback): void; + subscribe( + filter: MessageFilter, + listener?: messageCallback, + callbackWhenAttached?: StandardCallback + ): void; /** * Registers a listener for messages on this channel. The caller supplies a listener function, which is called each time one or more messages arrives on the channel. * * @param listener - An event listener function. - * @param callbackWhenAttached - A function which will be called upon completion of the channel {@link RealtimeChannelCallbacks.attach | `attach()`} operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. + * @param callbackWhenAttached - A function which will be called upon completion of the channel {@link RealtimeChannelCallbacks.attach | `attach()`} operation. If the operation succeeded and the channel became attached, then the function will be called with a {@link ChannelStateChange} object. If the channel was already attached the function will be called with `null`. If it failed, the function will be called with information about the error. */ - subscribe(listener: messageCallback, callbackWhenAttached?: errorCallback): void; + subscribe(listener: messageCallback, callbackWhenAttached?: StandardCallback): void; /** * Publishes a single message to the channel with the given event name and payload. When publish is called with this client library, it won't attempt to implicitly attach to the channel, so long as [transient publishing](https://ably.com/docs/realtime/channels#transient-publish) is available in the library. Otherwise, the client will implicitly attach. * @@ -2666,9 +2678,9 @@ declare namespace Types { /** * Attach to this channel ensuring the channel is created in the Ably system and all messages published on the channel are received by any channel listeners registered using {@link RealtimeChannelPromise.subscribe | `subscribe()`}. Any resulting channel state change will be emitted to any listeners registered using the {@link EventEmitter.on | `on()`} or {@link EventEmitter.once | `once()`} methods. As a convenience, `attach()` is called implicitly if {@link RealtimeChannelPromise.subscribe | `subscribe()`} for the channel is called, or {@link RealtimePresencePromise.enter | `enter()`} or {@link RealtimePresencePromise.subscribe | `subscribe()`} are called on the {@link RealtimePresencePromise} object for this channel. * - * @returns A promise which resolves upon success of the operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon success, if the channel became attached will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be fulfilled with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - attach(): Promise; + attach(): Promise; /** * Detach from this channel. Any resulting channel state change is emitted to any listeners registered using the {@link EventEmitter.on | `on()`} or {@link EventEmitter.once | `once()`} methods. Once all clients globally have detached from the channel, the channel will be released in the Ably service within two minutes. * @@ -2694,32 +2706,32 @@ declare namespace Types { * * @param event - The event name. * @param listener - An event listener function. - * @returns A promise which resolves upon success of the channel {@link RealtimeChannelPromise.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon successful attachment to the channel, will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be resolved with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - subscribe(event: string, listener?: messageCallback): Promise; + subscribe(event: string, listener?: messageCallback): Promise; /** * Registers a listener for messages on this channel for multiple event name values. * * @param events - An array of event names. * @param listener - An event listener function. - * @returns A promise which resolves upon success of the channel {@link RealtimeChannelPromise.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon successful attachment to the channel, will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be resolved with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - subscribe(events: Array, listener?: messageCallback): Promise; + subscribe(events: Array, listener?: messageCallback): Promise; /** * Registers a listener for messages on this channel that match the supplied filter. * * @param filter - A {@link MessageFilter}. * @param listener - An event listener function. - * @returns A promise which resolves upon success of the channel {@link RealtimeChannelPromise.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon successful attachment to the channel, will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be resolved with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - subscribe(filter: MessageFilter, listener?: messageCallback): Promise; + subscribe(filter: MessageFilter, listener?: messageCallback): Promise; /** * Registers a listener for messages on this channel. The caller supplies a listener function, which is called each time one or more messages arrives on the channel. * * @param callback - An event listener function. - * @returns A promise which resolves upon success of the channel {@link RealtimeChannelPromise.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon successful attachment to the channel, will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be resolved with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - subscribe(callback: messageCallback): Promise; + subscribe(callback: messageCallback): Promise; /** * Publishes a single message to the channel with the given event name and payload. When publish is called with this client library, it won't attempt to implicitly attach to the channel, so long as [transient publishing](https://ably.com/docs/realtime/channels#transient-publish) is available in the library. Otherwise, the client will implicitly attach. * diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 58a4abf595..538e49a7bf 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -12,7 +12,7 @@ import ConnectionErrors from '../transport/connectionerrors'; import * as API from '../../../../ably'; import ConnectionManager from '../transport/connectionmanager'; import ConnectionStateChange from './connectionstatechange'; -import { ErrCallback, PaginatedResultCallback } from '../../types/utils'; +import { ErrCallback, PaginatedResultCallback, StandardCallback } from '../../types/utils'; import Realtime from './realtime'; interface RealtimeHistoryParams { @@ -272,7 +272,10 @@ class RealtimeChannel extends Channel { } } - attach(flags?: API.Types.ChannelMode[] | ErrCallback, callback?: ErrCallback): void | Promise { + attach( + flags?: API.Types.ChannelMode[] | ErrCallback, + callback?: StandardCallback + ): void | Promise { let _flags: API.Types.ChannelMode[] | null | undefined; if (typeof flags === 'function') { callback = flags; @@ -296,14 +299,18 @@ class RealtimeChannel extends Channel { * current mode differs from requested mode */ this._requestedFlags = _flags as API.Types.ChannelMode[]; } else if (this.state === 'attached') { - callback(); + callback(null, null); return; } this._attach(false, null, callback); } - _attach(forceReattach: boolean, attachReason: ErrorInfo | null, callback?: ErrCallback): void { + _attach( + forceReattach: boolean, + attachReason: ErrorInfo | null, + callback?: StandardCallback + ): void { if (!callback) { callback = function (err?: ErrorInfo | null) { if (err) { @@ -325,7 +332,7 @@ class RealtimeChannel extends Channel { this.once(function (this: { event: string }, stateChange: ChannelStateChange) { switch (this.event) { case 'attached': - callback?.(); + callback?.(null, stateChange); break; case 'detached': case 'suspended': @@ -422,7 +429,7 @@ class RealtimeChannel extends Channel { this.sendMessage(msg, callback || noop); } - subscribe(...args: unknown[] /* [event], listener, [callback] */): void | Promise { + subscribe(...args: unknown[] /* [event], listener, [callback] */): void | Promise { const [event, listener, callback] = RealtimeChannel.processListenerArgs(args); if (!callback && this.realtime.options.promises) { diff --git a/test/realtime/channel.test.js b/test/realtime/channel.test.js index 13993e6f54..b12519bb2b 100644 --- a/test/realtime/channel.test.js +++ b/test/realtime/channel.test.js @@ -1569,5 +1569,66 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async channel.subscribe(subscriber); }); + + it('attach_returns_state_change', function (done) { + var realtime = helper.AblyRealtime(); + var channelName = 'attach_returns_state_chnage'; + var channel = realtime.channels.get(channelName); + channel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange.current).to.equal('attached'); + expect(stateChange.previous).to.equal('attaching'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + + // for an already-attached channel, null is returned + channel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange).to.equal(null); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + }); + + it('subscribe_returns_state_change', function (done) { + var realtime = helper.AblyRealtime(); + var channelName = 'subscribe_returns_state_chnage'; + var channel = realtime.channels.get(channelName); + channel.subscribe( + function () {}, // message listener + // attach callback + function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange.current).to.equal('attached'); + expect(stateChange.previous).to.equal('attaching'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + } + ); + }); }); }); From b75e0e46be5ed371d9e16a7aff1d3bf90e6058af Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 22 Jun 2023 11:09:38 +0100 Subject: [PATCH 16/46] feat: add `ChannelStateChange.hasBacklog` Exposes the `hasBacklog` flag on `ChannelStateChange`. This may be used in combination with rewind to check whether to expect a backlog of messages upon attachment. --- ably.d.ts | 4 ++ src/common/lib/client/channelstatechange.ts | 14 +++++- src/common/lib/client/realtimechannel.ts | 10 ++-- src/common/lib/client/realtimepresence.ts | 2 +- test/realtime/channel.test.js | 54 +++++++++++++++++++++ 5 files changed, 77 insertions(+), 7 deletions(-) diff --git a/ably.d.ts b/ably.d.ts index f404077c14..331835ac36 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -1149,6 +1149,10 @@ declare namespace Types { * Indicates whether message continuity on this channel is preserved, see [Nonfatal channel errors](https://ably.com/docs/realtime/channels#nonfatal-errors) for more info. */ resumed: boolean; + /** + * Indicates whether the client can expect a backlog of messages from a rewind or resume. + */ + hasBacklog?: boolean; } /** diff --git a/src/common/lib/client/channelstatechange.ts b/src/common/lib/client/channelstatechange.ts index b778210032..09cf0a2c6f 100644 --- a/src/common/lib/client/channelstatechange.ts +++ b/src/common/lib/client/channelstatechange.ts @@ -5,11 +5,21 @@ class ChannelStateChange { current: string; resumed?: boolean; reason?: string | Error | ErrorInfo; + hasBacklog?: boolean; - constructor(previous: string, current: string, resumed?: boolean, reason?: string | Error | ErrorInfo | null) { + constructor( + previous: string, + current: string, + resumed?: boolean, + hasBacklog?: boolean, + reason?: string | Error | ErrorInfo | null + ) { this.previous = previous; this.current = current; - if (current === 'attached') this.resumed = resumed; + if (current === 'attached') { + this.resumed = resumed; + this.hasBacklog = hasBacklog; + } if (reason) this.reason = reason; } } diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 538e49a7bf..9b8829cea5 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -622,12 +622,13 @@ class RealtimeChannel extends Channel { this.modes = (modesFromFlags && Utils.allToLowerCase(modesFromFlags)) || undefined; const resumed = message.hasFlag('RESUMED'); const hasPresence = message.hasFlag('HAS_PRESENCE'); + const hasBacklog = message.hasFlag('HAS_BACKLOG'); if (this.state === 'attached') { if (!resumed) { /* On a loss of continuity, the presence set needs to be re-synced */ this.presence.onAttached(hasPresence); } - const change = new ChannelStateChange(this.state, this.state, resumed, message.error); + const change = new ChannelStateChange(this.state, this.state, resumed, hasBacklog, message.error); this._allChannelChanges.emit('update', change); if (!resumed || this.channelOptions.updateOnAttached) { this.emit('update', change); @@ -636,7 +637,7 @@ class RealtimeChannel extends Channel { /* RTL5i: re-send DETACH and remain in the 'detaching' state */ this.checkPendingState(); } else { - this.notifyState('attached', message.error, resumed, hasPresence); + this.notifyState('attached', message.error, resumed, hasPresence, hasBacklog); } break; } @@ -797,7 +798,8 @@ class RealtimeChannel extends Channel { state: API.Types.ChannelState, reason?: ErrorInfo | null, resumed?: boolean, - hasPresence?: boolean + hasPresence?: boolean, + hasBacklog?: boolean ): void { Logger.logAction( Logger.LOG_MICRO, @@ -823,7 +825,7 @@ class RealtimeChannel extends Channel { if (reason) { this.errorReason = reason; } - const change = new ChannelStateChange(this.state, state, resumed, reason); + const change = new ChannelStateChange(this.state, state, resumed, hasBacklog, reason); const logLevel = state === 'failed' ? Logger.LOG_ERROR : Logger.LOG_MAJOR; Logger.logAction( logLevel, diff --git a/src/common/lib/client/realtimepresence.ts b/src/common/lib/client/realtimepresence.ts index 632bf49182..4cbc4148d5 100644 --- a/src/common/lib/client/realtimepresence.ts +++ b/src/common/lib/client/realtimepresence.ts @@ -477,7 +477,7 @@ class RealtimePresence extends Presence { const msg = 'Presence auto-re-enter failed: ' + err.toString(); const wrappedErr = new ErrorInfo(msg, 91004, 400); Logger.logAction(Logger.LOG_ERROR, 'RealtimePresence._ensureMyMembersPresent()', msg); - const change = new ChannelStateChange(this.channel.state, this.channel.state, true, wrappedErr); + const change = new ChannelStateChange(this.channel.state, this.channel.state, true, false, wrappedErr); this.channel.emit('update', change); } }; diff --git a/test/realtime/channel.test.js b/test/realtime/channel.test.js index b12519bb2b..886548bdf9 100644 --- a/test/realtime/channel.test.js +++ b/test/realtime/channel.test.js @@ -1630,5 +1630,59 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async } ); }); + + it('rewind_has_backlog_0', function (done) { + var realtime = helper.AblyRealtime(); + var channelName = 'rewind_has_backlog_0'; + var channelOpts = { params: { rewind: '1' } }; + var channel = realtime.channels.get(channelName, channelOpts); + + // attach with rewind but no channel history - hasBacklog should be false + channel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(!stateChange.hasBacklog).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + + it('rewind_has_backlog_1', function (done) { + var realtime = helper.AblyRealtime(); + var rest = helper.AblyRest(); + var channelName = 'rewind_has_backlog_1'; + var channelOpts = { params: { rewind: '1' } }; + var rtChannel = realtime.channels.get(channelName, channelOpts); + var restChannel = rest.channels.get(channelName); + + // attach with rewind after publishing - hasBacklog should be true + restChannel.publish('foo', 'bar', function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + rtChannel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange.hasBacklog).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + }); }); }); From f6cf0b9da32b56c5e79ec85fbd298d71ec8f622b Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 29 Jun 2023 15:16:07 +0100 Subject: [PATCH 17/46] docs: update CHANGELOG for 1.2.41 release --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d042f33f3c..22c5365c53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ This contains only the most important and/or user-facing changes; for a full changelog, see the commit history. +## [1.2.41](https://github.com/ably/ably-js/tree/1.2.41) (2023-06-29) + +- add `ChannelStateChange.hasBacklog` and return state change to attach promise/callback [\#1347](https://github.com/ably/ably-js/pull/1347) +- fix a bug where host fallback was initially skipped after falling back to the base transport [\#1357](https://github.com/ably/ably-js/pull/1357) + ## [1.2.40](https://github.com/ably/ably-js/tree/1.2.40) (2023-05-26) This release adds a new experimental `channels.getDerived` method which allows you to create custom realtime data feeds by selectively subscribing to receive only part of the data from the channel. See the [announcement post](https://pages.ably.com/subscription-filters-preview) for more information. From 6d4c700c1e9eaa35409d70b67a6b8b8c5d9a0f15 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 29 Jun 2023 15:16:32 +0100 Subject: [PATCH 18/46] chore: bump version for 1.2.41 release --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9c3a58509..0f2ed984b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ably", - "version": "1.2.40", + "version": "1.2.41", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ably", - "version": "1.2.40", + "version": "1.2.41", "license": "Apache-2.0", "dependencies": { "@ably/msgpack-js": "^0.4.0", diff --git a/package.json b/package.json index 7b89011f90..94c0f714e3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ably", "description": "Realtime client library for Ably, the realtime messaging service", - "version": "1.2.40", + "version": "1.2.41", "license": "Apache-2.0", "bugs": { "url": "https://github.com/ably/ably-js/issues", From 1d2e8da3a4f804745cb859bea310152d52774425 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Thu, 29 Jun 2023 16:25:34 +0000 Subject: [PATCH 19/46] removed nvmrc file since multiple node versions are used for testing --- .nvmrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 53d0020fde..0000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v16.20.1 From c43c6d9f171e1e15a3a96e9fd159bfd8481fd759 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Thu, 29 Jun 2023 16:29:29 +0000 Subject: [PATCH 20/46] updated utils test to not use object destructuring --- test/realtime/utils.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/realtime/utils.test.js b/test/realtime/utils.test.js index fa1d855e62..244e4a3a4a 100644 --- a/test/realtime/utils.test.js +++ b/test/realtime/utils.test.js @@ -31,12 +31,12 @@ define(['shared_helper', 'chai'], function (helper, chai) { function calculateBounds(retryAttempt, initialTimeout) { var upperBound = Math.min((retryAttempt + 2) / 3, 2) * initialTimeout; var lowerBound = 0.8 * upperBound; - return { lowerBound, upperBound }; + return { lower: lowerBound, upper: upperBound }; } for (var i = 0; i < retryTimeouts.length; i++) { - var { lowerBound, upperBound } = calculateBounds(retryAttempts[i], initialTimeout); - checkIsBetween(retryTimeouts[i], lowerBound, upperBound); + var bounds = calculateBounds(retryAttempts[i], initialTimeout); + checkIsBetween(retryTimeouts[i], bounds.lower, bounds.upper); } }); }); From 9f9dfaf5d358694d2b2888ea3b30399c64236182 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Thu, 29 Jun 2023 17:02:36 +0000 Subject: [PATCH 21/46] updated contributing doc --- CONTRIBUTING.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4faa7a424a..854571d32f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,10 @@ Or run just one test file npm run test:node -- --file=test/realtime/auth.test.js +Or run just one test + + npm run test:node -- --file=test/rest/status.test.js --grep=test_name_here + ### Debugging the mocha tests locally with a debugger Run the following command to launch tests with the debugger enabled. The tests will block until you attach a debugger. @@ -65,6 +69,12 @@ Run the following command to start a local Mocha test runner web server Open your browser to [http://localhost:3000](http://localhost:3000). If you are using a remote browser, refer to https://docs.saucelabs.com/reference/sauce-connect/ for instructions on setting up a local tunnel to your Mocha runner web server. +### Formatting/linting files + +Run the following command to fix linting/formatting issues + + npm run format + ### Testing environment variables for Node.js All tests are run against the sandbox environment by default. However, the following environment variables can be set before running the Karma server to change the environment the tests are run against. From bdeece143304096fa39294fc71e5d0c920b430d4 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Thu, 29 Jun 2023 17:09:32 +0000 Subject: [PATCH 22/46] Added script command to skip build while running tests --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 94c0f714e3..3fb7c02aed 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "grunt": "grunt", "test": "grunt test", "test:node": "grunt test:node", + "test:node:skip-build": "grunt mocha", "test:webserver": "grunt test:webserver", "test:playwright": "node test/support/runPlaywrightTests.js", "concat": "grunt concat", From 4d8f86ee8bce931dbd2e0196367df94308adbc79 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Thu, 29 Jun 2023 17:12:06 +0000 Subject: [PATCH 23/46] updated contrbuting doc with newly added test skip build command --- CONTRIBUTING.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 854571d32f..8769a28e2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,11 @@ Or run just one test file Or run just one test - npm run test:node -- --file=test/rest/status.test.js --grep=test_name_here + npm run test:node -- --file=test/rest/status.test.js --grep=test_name_here + +Or run test skipping the build + + npm run test:node:skip-build -- --file=test/rest/status.test.js --grep=test_name_here ### Debugging the mocha tests locally with a debugger @@ -74,7 +78,7 @@ Open your browser to [http://localhost:3000](http://localhost:3000). If you are Run the following command to fix linting/formatting issues npm run format - + ### Testing environment variables for Node.js All tests are run against the sandbox environment by default. However, the following environment variables can be set before running the Karma server to change the environment the tests are run against. From 5b6706755dab1a442b9d383ab272654b6adb11ef Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Thu, 29 Jun 2023 17:14:02 +0000 Subject: [PATCH 24/46] reverted contributing file to original format --- CONTRIBUTING.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49fc9fee80..4faa7a424a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,10 +43,6 @@ Or run just one test file npm run test:node -- --file=test/realtime/auth.test.js -Or run just one test - - npm run test:node -- --file=test/rest/status.test.js --grep=test_name_here - ### Debugging the mocha tests locally with a debugger Run the following command to launch tests with the debugger enabled. The tests will block until you attach a debugger. @@ -61,10 +57,6 @@ The included vscode launch config allows you to launch and attach the debugger i file you want to run and start debugging. Note that breakpoint setting for realtime code will be within the browser/static directory, not the raw source files, and breakpoints in files under test should work directly. -VSCode allows you to directly launch `package.json` scripts in debug mode. -Simply click on `Debug` option above `scripts` and select the command to run. -This way, you can add a command to run a single test as a part of `scripts` and debug directly from package.json. - ### Debugging the tests in a browser with Mocha test runner Run the following command to start a local Mocha test runner web server @@ -73,12 +65,6 @@ Run the following command to start a local Mocha test runner web server Open your browser to [http://localhost:3000](http://localhost:3000). If you are using a remote browser, refer to https://docs.saucelabs.com/reference/sauce-connect/ for instructions on setting up a local tunnel to your Mocha runner web server. -### Formatting/linting files - -Run the following command to fix linting/formatting issues - - npm run format - ### Testing environment variables for Node.js All tests are run against the sandbox environment by default. However, the following environment variables can be set before running the Karma server to change the environment the tests are run against. From fbfa599cd1d1e6046f3d6ab9fe5b8a57bae22b8b Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Fri, 30 Jun 2023 11:08:37 +0100 Subject: [PATCH 25/46] docs: add 'OS Connectivity Events' to capabilities spec --- .ably/capabilities.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ably/capabilities.yaml b/.ably/capabilities.yaml index 739b2fe54a..26b9371774 100644 --- a/.ably/capabilities.yaml +++ b/.ably/capabilities.yaml @@ -46,6 +46,7 @@ compliance: Get Identifier: Incremental Backoff: Lifecycle Control: + OS Connectivity Events: Ping: Recovery: State Events: From af395cdd45c4bfbe2fd621da4d17f3ce58d32e22 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Fri, 30 Jun 2023 14:23:07 +0100 Subject: [PATCH 26/46] docs: fix description of AuthOptions.token This incorrectly claimed that it was possible to pass a `TokenRequest` object, which is not supported. --- ably.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ably.d.ts b/ably.d.ts index 331835ac36..8df7cbb0a0 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -634,7 +634,7 @@ declare namespace Types { queryTime?: boolean; /** - * An authenticated token. This can either be a {@link TokenDetails} object, a {@link TokenRequest} object, or token string (obtained from the `token` property of a {@link TokenDetails} component of an Ably {@link TokenRequest} response, or a JSON Web Token satisfying [the Ably requirements for JWTs](https://ably.com/docs/core-features/authentication#ably-jwt)). This option is mostly useful for testing: since tokens are short-lived, in production you almost always want to use an authentication method that enables the client library to renew the token automatically when the previous one expires, such as `authUrl` or `authCallback`. Read more about [Token authentication](https://ably.com/docs/core-features/authentication#token-authentication). + * An authenticated token. This can either be a {@link TokenDetails} object or token string (obtained from the `token` property of a {@link TokenDetails} component of an Ably {@link TokenRequest} response, or a JSON Web Token satisfying [the Ably requirements for JWTs](https://ably.com/docs/core-features/authentication#ably-jwt)). This option is mostly useful for testing: since tokens are short-lived, in production you almost always want to use an authentication method that enables the client library to renew the token automatically when the previous one expires, such as `authUrl` or `authCallback`. Read more about [Token authentication](https://ably.com/docs/core-features/authentication#token-authentication). */ token?: TokenDetails | string; From 0b75dec29ce633086d2e5cdc9a22e10a97961e14 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Tue, 4 Jul 2023 14:17:19 +0000 Subject: [PATCH 27/46] refactored failure.test.js, updated disconnectedTimeout and channelTimeout tests --- test/realtime/failure.test.js | 41 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/test/realtime/failure.test.js b/test/realtime/failure.test.js index fb8f88d551..ac09c2e043 100644 --- a/test/realtime/failure.test.js +++ b/test/realtime/failure.test.js @@ -185,26 +185,14 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async } }); - function checkIfRetryTimeoutsAreCorrect(retryTimeouts, calculationDelay = 0) { - function checkIsBetween(value, min, max) { - expect(value).to.be.above(min); - expect(value).to.be.below(max); - } - - // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout - // Lower bound = 0.8 * Upper bound - checkIsBetween(retryTimeouts[0], 120, 150 + calculationDelay); - checkIsBetween(retryTimeouts[1], 160, 200 + calculationDelay); - checkIsBetween(retryTimeouts[2], 200, 250 + calculationDelay); - - for (var i = 3; i < retryTimeouts.length; i++) { - checkIsBetween(retryTimeouts[i], 240, 300 + calculationDelay); - } + function checkIsBetween(value, min, max) { + expect(value).to.be.above(min); + expect(value).to.be.below(max); } utils.arrForEach(availableTransports, function (transport) { it('disconnected_backoff_' + transport, function (done) { - var disconnectedRetryTimeout = 150; + var disconnectedRetryTimeout = ; var realtime = helper.AblyRealtime({ disconnectedRetryTimeout: disconnectedRetryTimeout, realtimeHost: 'invalid', @@ -218,7 +206,15 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async realtime.connection.on(function (stateChange) { if (stateChange.previous === 'connecting' && stateChange.current === 'disconnected') { if (retryCount > 4) { - checkIfRetryTimeoutsAreCorrect(retryTimeouts); + // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout + // Lower bound = 0.8 * Upper bound + checkIsBetween(retryTimeouts[0], 120, 150); + checkIsBetween(retryTimeouts[1], 160, 200); + checkIsBetween(retryTimeouts[2], 200, 250); + + for (var i = 3; i < retryTimeouts.length; i++) { + checkIsBetween(retryTimeouts[i], 240, 300); + } closeAndFinish(done, realtime); return; } @@ -407,7 +403,16 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async channel.on(function (stateChange) { if (stateChange.current === 'suspended') { if (retryCount > 4) { - checkIfRetryTimeoutsAreCorrect(retryTimeouts, 10); + // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout + // Lower bound = 0.8 * Upper bound + // Additional 10 is a calculationDelayTimeout + checkIsBetween(retryTimeouts[0], 120, 150 + 10); + checkIsBetween(retryTimeouts[1], 160, 200 + 10); + checkIsBetween(retryTimeouts[2], 200, 250 + 10); + + for (var i = 3; i < retryTimeouts.length; i++) { + checkIsBetween(retryTimeouts[i], 240, 300 + 10); + } closeAndFinish(done, realtime); return; } From 0c49c0b7694fee041da8ec529e96cbf8d2257ea4 Mon Sep 17 00:00:00 2001 From: sachin shinde Date: Tue, 4 Jul 2023 17:12:33 +0000 Subject: [PATCH 28/46] fixed formatting issue for disconnectedRetryTimeout --- test/realtime/failure.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/realtime/failure.test.js b/test/realtime/failure.test.js index ac09c2e043..d3906ca705 100644 --- a/test/realtime/failure.test.js +++ b/test/realtime/failure.test.js @@ -192,7 +192,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async utils.arrForEach(availableTransports, function (transport) { it('disconnected_backoff_' + transport, function (done) { - var disconnectedRetryTimeout = ; + var disconnectedRetryTimeout = 150; var realtime = helper.AblyRealtime({ disconnectedRetryTimeout: disconnectedRetryTimeout, realtimeHost: 'invalid', @@ -406,7 +406,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout // Lower bound = 0.8 * Upper bound // Additional 10 is a calculationDelayTimeout - checkIsBetween(retryTimeouts[0], 120, 150 + 10); + checkIsBetween(retryTimeouts[0], 120, 150 + 10); checkIsBetween(retryTimeouts[1], 160, 200 + 10); checkIsBetween(retryTimeouts[2], 200, 250 + 10); From 5af0acce6955e2b454ddd8547b691d9808140b0d Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 6 Jul 2023 13:17:41 +0100 Subject: [PATCH 29/46] chore: bump features version --- .ably/capabilities.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ably/capabilities.yaml b/.ably/capabilities.yaml index 26b9371774..ce13a41b5d 100644 --- a/.ably/capabilities.yaml +++ b/.ably/capabilities.yaml @@ -1,6 +1,6 @@ %YAML 1.2 --- -common-version: 1.2.0 +common-version: 1.2.1 compliance: Agent Identifier: Agents: From def1d7ecc9ea54feba990a32aca23a2977e433a2 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Mon, 10 Jul 2023 17:30:44 +0100 Subject: [PATCH 30/46] ci: log failed tests at end of playwright suite --- test/support/playwrightSetup.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/support/playwrightSetup.js b/test/support/playwrightSetup.js index 8ab4a8a99b..8ade4fe779 100644 --- a/test/support/playwrightSetup.js +++ b/test/support/playwrightSetup.js @@ -6,6 +6,7 @@ class CustomEventReporter extends Mocha.reporters.HTML { constructor(runner) { super(runner); this.indents = 0; + this.failedTests = []; runner .on(EVENT_SUITE_BEGIN, (suite) => { @@ -22,9 +23,13 @@ class CustomEventReporter extends Mocha.reporters.HTML { }) .on(EVENT_TEST_FAIL, (test, err) => { this.logToNodeConsole(`${failSymbol}: ${test.title} - error: ${err.message}`); + this.failedTests.push(test.title); }) .once(EVENT_RUN_END, () => { this.indents = 0; + if (this.failedTests.length > 0) { + this.logToNodeConsole('\nfailed tests: \n' + this.failedTests.map((x) => ' - ' + x).join('\n') + '\n'); + } runner.stats && window.dispatchEvent( new CustomEvent('testResult', { From 63c532dadea7bc0d3e5098cf8c62893188887a18 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 11 Jul 2023 11:15:20 +0100 Subject: [PATCH 31/46] deps: bump aws-sdk to 2.1413.0 --- package-lock.json | 53 +++++++++++++++++++++++++---------------------- package.json | 2 +- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f2ed984b2..83246cd1e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", "async": "ably-forks/async#requirejs", - "aws-sdk": "^2.1075.0", + "aws-sdk": "^2.1413.0", "chai": "^4.2.0", "copy-webpack-plugin": "^6.4.1", "cors": "~2.7", @@ -1167,9 +1167,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.1286.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1286.0.tgz", - "integrity": "sha512-CvkCD1+NSk2MPOutD2hEPhXDET/79w/gd9a359QWb9Ja0Fd4vVFXPkhlm1DTGzuwqFKGinpCMxDP4md7QPsVvw==", + "version": "2.1414.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1414.0.tgz", + "integrity": "sha512-WhqTWiTZRUxWITvUG5VMPYGdCLNAm4zOTDIiotbErR9x+uDExk2CAGbXE8HH11+tD8PhZVXyukymSiG+7rJMMg==", "dev": true, "dependencies": { "buffer": "4.9.2", @@ -1181,7 +1181,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.4.19" + "xml2js": "0.5.0" }, "engines": { "node": ">= 10.0.0" @@ -7965,7 +7965,7 @@ "node_modules/sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "dev": true }, "node_modules/schema-utils": { @@ -10214,19 +10214,22 @@ } }, "node_modules/xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" } }, "node_modules/xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, "engines": { "node": ">=4.0" @@ -11256,9 +11259,9 @@ "dev": true }, "aws-sdk": { - "version": "2.1286.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1286.0.tgz", - "integrity": "sha512-CvkCD1+NSk2MPOutD2hEPhXDET/79w/gd9a359QWb9Ja0Fd4vVFXPkhlm1DTGzuwqFKGinpCMxDP4md7QPsVvw==", + "version": "2.1414.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1414.0.tgz", + "integrity": "sha512-WhqTWiTZRUxWITvUG5VMPYGdCLNAm4zOTDIiotbErR9x+uDExk2CAGbXE8HH11+tD8PhZVXyukymSiG+7rJMMg==", "dev": true, "requires": { "buffer": "4.9.2", @@ -11270,7 +11273,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.4.19" + "xml2js": "0.5.0" }, "dependencies": { "events": { @@ -16573,7 +16576,7 @@ "sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "dev": true }, "schema-utils": { @@ -18376,19 +18379,19 @@ } }, "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "requires": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" } }, "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true }, "xtend": { diff --git a/package.json b/package.json index 3fb7c02aed..9dc04102ca 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", "async": "ably-forks/async#requirejs", - "aws-sdk": "^2.1075.0", + "aws-sdk": "^2.1413.0", "chai": "^4.2.0", "copy-webpack-plugin": "^6.4.1", "cors": "~2.7", From 19c46e366895462aff078a34f2e4b1c95c20b712 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 11 Jul 2023 11:16:22 +0100 Subject: [PATCH 32/46] deps: bump cors to 2.8.5 --- package-lock.json | 48 ++++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83246cd1e0..bae30b5763 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "aws-sdk": "^2.1413.0", "chai": "^4.2.0", "copy-webpack-plugin": "^6.4.1", - "cors": "~2.7", + "cors": "^2.8.5", "crypto-js": "ably-forks/crypto-js#crypto-lite", "eslint": "^7.13.0", "eslint-plugin-jsdoc": "^40.0.0", @@ -1072,15 +1072,6 @@ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", "dev": true }, - "node_modules/assert/node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/assert/node_modules/util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", @@ -2484,15 +2475,16 @@ "dev": true }, "node_modules/cors": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.7.2.tgz", - "integrity": "sha1-IThd6//yTCI6EGBbgjEUUpmaHLs=", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, "dependencies": { + "object-assign": "^4", "vary": "^1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, "node_modules/create-ecdh": { @@ -6786,6 +6778,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -11187,12 +11188,6 @@ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", "dev": true }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", @@ -12357,11 +12352,12 @@ "dev": true }, "cors": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.7.2.tgz", - "integrity": "sha1-IThd6//yTCI6EGBbgjEUUpmaHLs=", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, "requires": { + "object-assign": "^4", "vary": "^1" } }, @@ -15692,6 +15688,12 @@ } } }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", diff --git a/package.json b/package.json index 9dc04102ca..111c5277a9 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "aws-sdk": "^2.1413.0", "chai": "^4.2.0", "copy-webpack-plugin": "^6.4.1", - "cors": "~2.7", + "cors": "^2.8.5", "crypto-js": "ably-forks/crypto-js#crypto-lite", "eslint": "^7.13.0", "eslint-plugin-jsdoc": "^40.0.0", From 70f9c5be8f82762bdccdbe947b80eada8b3f4a6b Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 11 Jul 2023 11:16:55 +0100 Subject: [PATCH 33/46] deps: bump grunt to 1.6.1 --- package-lock.json | 217 ++++++++++++++++++++++++++++++++++------------ package.json | 2 +- 2 files changed, 161 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index bae30b5763..5827513716 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "express": "^4.17.1", "glob": "~4.4", "google-closure-compiler": "^20180610.0.1", - "grunt": "^1.4.1", + "grunt": "^1.6.1", "grunt-bump": "^0.3.1", "grunt-cli": "~1.2.0", "grunt-closure-tools": "^1.0.0", @@ -2580,9 +2580,9 @@ "dev": true }, "node_modules/dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "dev": true, "engines": { "node": "*" @@ -4442,32 +4442,30 @@ } }, "node_modules/grunt": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz", - "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", + "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", "dev": true, "dependencies": { - "dateformat": "~3.0.3", + "dateformat": "~4.6.2", "eventemitter2": "~0.4.13", "exit": "~0.1.2", - "findup-sync": "~0.3.0", + "findup-sync": "~5.0.0", "glob": "~7.1.6", "grunt-cli": "~1.4.3", "grunt-known-options": "~2.0.0", "grunt-legacy-log": "~3.0.0", "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.4.13", + "iconv-lite": "~0.6.3", "js-yaml": "~3.14.0", "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" + "nopt": "~3.0.6" }, "bin": { "grunt": "bin/grunt" }, "engines": { - "node": ">=8" + "node": ">=16" } }, "node_modules/grunt-bump": { @@ -4773,6 +4771,45 @@ "webpack": "^4.0.0" } }, + "node_modules/grunt/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt/node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/grunt/node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -4834,31 +4871,50 @@ "node": ">=0.10.0" } }, - "node_modules/grunt/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/grunt/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/grunt/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "node_modules/grunt/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/grunt/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" }, - "bin": { - "rimraf": "bin.js" + "engines": { + "node": ">=8.6" + } + }, + "node_modules/grunt/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=8.0" } }, "node_modules/gzip-size": { @@ -7253,9 +7309,9 @@ } }, "node_modules/picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" @@ -12449,9 +12505,9 @@ "dev": true }, "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "dev": true }, "debug": { @@ -13928,28 +13984,56 @@ "dev": true }, "grunt": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz", - "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", + "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", "dev": true, "requires": { - "dateformat": "~3.0.3", + "dateformat": "~4.6.2", "eventemitter2": "~0.4.13", "exit": "~0.1.2", - "findup-sync": "~0.3.0", + "findup-sync": "~5.0.0", "glob": "~7.1.6", "grunt-cli": "~1.4.3", "grunt-known-options": "~2.0.0", "grunt-legacy-log": "~3.0.0", "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.4.13", + "iconv-lite": "~0.6.3", "js-yaml": "~3.14.0", "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" + "nopt": "~3.0.6" }, "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + } + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -13995,19 +14079,38 @@ "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", "dev": true }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "glob": "^7.1.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" } } } @@ -16050,9 +16153,9 @@ } }, "picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pify": { diff --git a/package.json b/package.json index 111c5277a9..d1cd21366b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "express": "^4.17.1", "glob": "~4.4", "google-closure-compiler": "^20180610.0.1", - "grunt": "^1.4.1", + "grunt": "^1.6.1", "grunt-bump": "^0.3.1", "grunt-cli": "~1.2.0", "grunt-closure-tools": "^1.0.0", From 23cbfc1cc30fd25ede990433c18ce4ef7539af9c Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 11 Jul 2023 11:17:24 +0100 Subject: [PATCH 34/46] deps: bump tsconfig-paths-webpack-plugin to 4.0.1 --- package-lock.json | 78 +++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5827513716..bd0f34eaa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "shelljs": "~0.8", "source-map-explorer": "^2.5.2", "ts-loader": "^8.2.0", - "tsconfig-paths-webpack-plugin": "^3.5.2", + "tsconfig-paths-webpack-plugin": "^4.0.1", "tslib": "^2.3.1", "typedoc": "^0.23.8", "typescript": "^4.6.4", @@ -302,12 +302,6 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, "node_modules/@types/keyv": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz", @@ -8885,7 +8879,7 @@ "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "engines": { "node": ">=4" @@ -9295,26 +9289,31 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, "node_modules/tsconfig-paths-webpack-plugin": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz", - "integrity": "sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.1.tgz", + "integrity": "sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw==", "dev": true, "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", - "tsconfig-paths": "^3.9.0" + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" } }, "node_modules/tsconfig-paths-webpack-plugin/node_modules/enhanced-resolve": { @@ -9339,6 +9338,18 @@ "node": ">=6" } }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -10639,12 +10650,6 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, "@types/keyv": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz", @@ -17407,7 +17412,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, "strip-final-newline": { @@ -17718,26 +17723,33 @@ } }, "tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + } } }, "tsconfig-paths-webpack-plugin": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz", - "integrity": "sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.1.tgz", + "integrity": "sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw==", "dev": true, "requires": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", - "tsconfig-paths": "^3.9.0" + "tsconfig-paths": "^4.1.2" }, "dependencies": { "enhanced-resolve": { diff --git a/package.json b/package.json index d1cd21366b..dbc9ad14a7 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "shelljs": "~0.8", "source-map-explorer": "^2.5.2", "ts-loader": "^8.2.0", - "tsconfig-paths-webpack-plugin": "^3.5.2", + "tsconfig-paths-webpack-plugin": "^4.0.1", "tslib": "^2.3.1", "typedoc": "^0.23.8", "typescript": "^4.6.4", From 73d27719487123f495c7e8049d651bbe93dec1fa Mon Sep 17 00:00:00 2001 From: Simon Woolf Date: Tue, 11 Jul 2023 18:43:18 +0100 Subject: [PATCH 35/46] Auth: adhere to RSA4c3 which requires us to remain connected if we do an authorize which fails for some reason other than an explicit 403 being returned. Much discussion on what the correct behaviour should be at https://ably-real-time.slack.com/archives/C8SPU4589/p1686825790902119 --- src/common/lib/client/auth.ts | 9 +++++---- src/common/lib/transport/connectionmanager.ts | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/common/lib/client/auth.ts b/src/common/lib/client/auth.ts index 9be9b824a6..b92a3668d8 100644 --- a/src/common/lib/client/auth.ts +++ b/src/common/lib/client/auth.ts @@ -12,6 +12,7 @@ import Rest from './rest'; import Realtime from './realtime'; import ClientOptions from '../../types/ClientOptions'; import HttpMethods from '../../constants/HttpMethods'; +import HttpStatusCodes from 'common/constants/HttpStatusCodes'; import Platform from '../../platform'; const MAX_TOKEN_LENGTH = Math.pow(2, 17); @@ -294,10 +295,10 @@ class Auth { _authOptions, (err: ErrorInfo, tokenDetails: API.Types.TokenDetails) => { if (err) { - if ((this.client as Realtime).connection) { - /* We interpret RSA4d as including requests made by a client lib to - * authenticate triggered by an explicit authorize() or an AUTH received from - * ably, not just connect-sequence-triggered token fetches */ + if ((this.client as Realtime).connection && err.statusCode === HttpStatusCodes.Forbidden) { + /* Per RSA4d & RSA4d1, if the auth server explicitly repudiates our right to + * stay connecticed by returning a 403, we actively disconnect the connection + * even though we may well still have time left in the old token. */ (this.client as Realtime).connection.connectionManager.actOnErrorFromAuthorize(err); } callback?.(err); diff --git a/src/common/lib/transport/connectionmanager.ts b/src/common/lib/transport/connectionmanager.ts index 016c50d513..d86edf13ff 100644 --- a/src/common/lib/transport/connectionmanager.ts +++ b/src/common/lib/transport/connectionmanager.ts @@ -2081,9 +2081,10 @@ class ConnectionManager extends EventEmitter { } } - /* This method is only used during connection attempts, so implements RSA4c1, - * RSA4c2, and RSA4d. In particular, it is not invoked for - * serverside-triggered reauths or manual reauths, so RSA4c3 does not apply */ + /* This method is only used during connection attempts, so implements RSA4c1, RSA4c2, + * and RSA4d. It is generally not invoked for serverside-triggered reauths or manual + * reauths, so RSA4c3 does not apply, except (per per RSA4d1) in the case that the auth + * server returns 403. */ actOnErrorFromAuthorize(err: ErrorInfo): void { if (err.code === 40171) { /* No way to reauth */ From 9434d4e75f9941f9bc821c2f55764ecd5ae1358d Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 13 Jul 2023 10:20:39 -0300 Subject: [PATCH 36/46] Make Utils#inspectError use toString for Error-like values This used to be the logic but was (presumably unintentionally) swapped in 1430d68. Resolves #1390. --- src/common/lib/util/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/lib/util/utils.ts b/src/common/lib/util/utils.ts index 24077849fb..533daef412 100644 --- a/src/common/lib/util/utils.ts +++ b/src/common/lib/util/utils.ts @@ -413,8 +413,8 @@ export function inspectError(err: unknown): string { (err as ErrorInfo)?.constructor?.name === 'ErrorInfo' || (err as PartialErrorInfo)?.constructor?.name === 'PartialErrorInfo' ) - return Platform.Config.inspect(err); - return (err as Error).toString(); + return (err as Error).toString(); + return Platform.Config.inspect(err); } export function inspectBody(body: unknown): string { From 070f2d673eb23368795b9b3fd03b0e0d4ce85e22 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 17 Jul 2023 17:24:46 -0300 Subject: [PATCH 37/46] =?UTF-8?q?Rename=20BodyHandler=E2=80=99s=20`packed`?= =?UTF-8?q?=20param=20to=20`unpacked`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is how it’s named at all of the call sites of `new PaginatedResource`, and how it’s named inside PaginatedResource up until the point where the callback is invoked on line 154 of this file. --- src/common/lib/client/paginatedresource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/lib/client/paginatedresource.ts b/src/common/lib/client/paginatedresource.ts index 3cadfaba2b..4d16a3b894 100644 --- a/src/common/lib/client/paginatedresource.ts +++ b/src/common/lib/client/paginatedresource.ts @@ -5,7 +5,7 @@ import ErrorInfo, { IPartialErrorInfo } from '../types/errorinfo'; import { PaginatedResultCallback } from '../../types/utils'; import Rest from './rest'; -export type BodyHandler = (body: unknown, headers: Record, packed?: boolean) => any; +export type BodyHandler = (body: unknown, headers: Record, unpacked?: boolean) => any; function getRelParams(linkUrl: string) { const urlMatch = linkUrl.match(/^\.\/(\w+)\?(.*)$/); From 6fae7c2522865ca8dcba008939c272ce61370731 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 17 Jul 2023 17:28:22 -0300 Subject: [PATCH 38/46] =?UTF-8?q?Rename=20RequestCallback=E2=80=99s=20`pac?= =?UTF-8?q?ked`=20param=20to=20`unpacked`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is how it's named at the call sites of rest.http.do, and it’s consistent with how its value is being populated in web/lib/transport/fetchrequest.ts and how its value is being used (i.e. if its false then we need to deserialize). --- src/common/types/http.d.ts | 2 +- src/platform/nodejs/lib/util/http.ts | 2 +- src/platform/web/lib/transport/fetchrequest.ts | 6 +++--- src/platform/web/lib/util/http.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/types/http.d.ts b/src/common/types/http.d.ts index bd3b33fa59..c3f454d475 100644 --- a/src/common/types/http.d.ts +++ b/src/common/types/http.d.ts @@ -8,7 +8,7 @@ export type RequestCallback = ( error?: ErrnoException | IPartialErrorInfo | null, body?: unknown, headers?: IncomingHttpHeaders, - packed?: boolean, + unpacked?: boolean, statusCode?: number ) => void; export type RequestParams = Record | null; diff --git a/src/platform/nodejs/lib/util/http.ts b/src/platform/nodejs/lib/util/http.ts index 0b2c3307d2..a6e757f37e 100644 --- a/src/platform/nodejs/lib/util/http.ts +++ b/src/platform/nodejs/lib/util/http.ts @@ -261,7 +261,7 @@ const Http: typeof IHttp = class { err?: ErrnoException | ErrorInfo | null, responseText?: unknown, headers?: any, - packed?: boolean, + unpacked?: boolean, statusCode?: number ) { if (!err && !connectivityUrlIsDefault) { diff --git a/src/platform/web/lib/transport/fetchrequest.ts b/src/platform/web/lib/transport/fetchrequest.ts index 360b3f5a5b..9af6592628 100644 --- a/src/platform/web/lib/transport/fetchrequest.ts +++ b/src/platform/web/lib/transport/fetchrequest.ts @@ -63,7 +63,7 @@ export default function fetchRequest( prom = res.text(); } prom.then((body) => { - const packed = !!contentType && contentType.indexOf('application/x-msgpack') === -1; + const unpacked = !!contentType && contentType.indexOf('application/x-msgpack') === -1; if (!res.ok) { const err = getAblyError(body, res.headers) || @@ -72,9 +72,9 @@ export default function fetchRequest( null, res.status ); - callback(err, body, res.headers, packed, res.status); + callback(err, body, res.headers, unpacked, res.status); } else { - callback(null, body, res.headers, packed, res.status); + callback(null, body, res.headers, unpacked, res.status); } }); }) diff --git a/src/platform/web/lib/util/http.ts b/src/platform/web/lib/util/http.ts index c0b5942375..2be9519de7 100644 --- a/src/platform/web/lib/util/http.ts +++ b/src/platform/web/lib/util/http.ts @@ -100,7 +100,7 @@ const Http: typeof IHttp = class { err?: ErrorInfo | ErrnoException | null, responseText?: unknown, headers?: any, - packed?: boolean, + unpacked?: boolean, statusCode?: number ) { let result = false; From a1ca52a5fbb68c615ca3da4e403651bbdbbfb387 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 20 Jul 2023 11:01:49 -0300 Subject: [PATCH 39/46] Run channel.status test using both formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We do this elsewhere, noticed we weren’t doing it here, seems like a good idea. --- test/rest/status.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/rest/status.test.js b/test/rest/status.test.js index 3a0a2bbd8a..55573b4594 100644 --- a/test/rest/status.test.js +++ b/test/rest/status.test.js @@ -4,6 +4,7 @@ define(['shared_helper', 'chai'], function (helper, chai) { var rest; var utils = helper.Utils; var expect = chai.expect; + var restTestOnJsonMsgpack = helper.restTestOnJsonMsgpack; // RSL8 describe('rest/status', function () { @@ -20,7 +21,7 @@ define(['shared_helper', 'chai'], function (helper, chai) { }); }); - it('status0', function (done) { + restTestOnJsonMsgpack('status0', function (done, rest) { var channel = rest.channels.get('status0'); channel.status(function (err, channelDetails) { try { From 5309db53435a53b76d266e3777305918709ea817 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 20 Jul 2023 11:03:29 -0300 Subject: [PATCH 40/46] Run channel.status test in browser Missed out in 8e0f0a8. --- test/support/browser_file_list.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/support/browser_file_list.js b/test/support/browser_file_list.js index 26ec6f053e..31369d1f3a 100644 --- a/test/support/browser_file_list.js +++ b/test/support/browser_file_list.js @@ -69,4 +69,5 @@ window.__testFiles__.files = { 'test/browser/connection.test.js': true, 'test/browser/simple.test.js': true, 'test/browser/http.test.js': true, + 'test/rest/status.test.js': true, }; From b82e6716c29d33c89d643f8f50f39744cfff8b9a Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Mon, 24 Jul 2023 16:03:54 +0100 Subject: [PATCH 41/46] docs: update CHANGELOG for 1.2.42 release --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c5365c53..6e6e9efb76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ This contains only the most important and/or user-facing changes; for a full changelog, see the commit history. +## [1.2.42](https://github.com/ably/ably-js/tree/1.2.42) (2023-07-24) + +- Auth: remain connected upon failed authorize unless returning explicit 403 [\#1385](https://github.com/ably/ably-js/pull/1385) +- Make `Utils#inspectError` use `toString` for `Error`-like values [\#1391](https://github.com/ably/ably-js/pull/1391) +- docs: fix description of AuthOptions.token [\#1368](https://github.com/ably/ably-js/pull/1368) + ## [1.2.41](https://github.com/ably/ably-js/tree/1.2.41) (2023-06-29) - add `ChannelStateChange.hasBacklog` and return state change to attach promise/callback [\#1347](https://github.com/ably/ably-js/pull/1347) From 01929274dd91f718ab797dedb61e394f44ac8ba4 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Mon, 24 Jul 2023 16:04:26 +0100 Subject: [PATCH 42/46] chore: bump version for 1.2.42 release --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd0f34eaa1..c571a7576e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ably", - "version": "1.2.41", + "version": "1.2.42", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ably", - "version": "1.2.41", + "version": "1.2.42", "license": "Apache-2.0", "dependencies": { "@ably/msgpack-js": "^0.4.0", diff --git a/package.json b/package.json index dbc9ad14a7..df123478df 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ably", "description": "Realtime client library for Ably, the realtime messaging service", - "version": "1.2.41", + "version": "1.2.42", "license": "Apache-2.0", "bugs": { "url": "https://github.com/ably/ably-js/issues", From 21bd8493a7662b3693902ac4ca31fbdf22153c8f Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 17 Jul 2023 09:21:38 -0300 Subject: [PATCH 43/46] Implement REST batch publish As described by spec at commit 18ef967. Documentation based on sdk-api-reference repo at commit 652ef2f. Part of #989. --- ably.d.ts | 133 ++++++++++++ src/common/lib/client/rest.ts | 70 ++++++ test/rest/batch.test.js | 343 ++++++++++++++++++++++++++++++ test/support/browser_file_list.js | 1 + 4 files changed, 547 insertions(+) create mode 100644 test/rest/batch.test.js diff --git a/ably.d.ts b/ably.d.ts index 8df7cbb0a0..bf94252d61 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -1434,6 +1434,66 @@ declare namespace Types { unit?: StatsIntervalGranularity; } + /** + * Contains information about the results of a batch operation. + */ + interface BatchResult { + /** + * The number of successful operations in the request. + */ + successCount: number; + /** + * The number of unsuccessful operations in the request. + */ + failureCount: number; + /** + * An array of results for the batch operation. + */ + results: T[]; + } + + /** + * Describes the messages that should be published by a batch publish operation, and the channels to which they should be published. + */ + interface BatchPublishSpec { + /** + * The names of the channels to publish the `messages` to. + */ + channels: string[]; + /** + * An array of {@link Message} objects. + */ + messages: Message[]; + } + + /** + * Contains information about the result of successful publishes to a channel requested by a single {@link Types.BatchPublishSpec}. + */ + interface BatchPublishSuccessResult { + /** + * The name of the channel the message(s) was published to. + */ + channel: string; + /** + * A unique ID prefixed to the {@link Message.id} of each published message. + */ + messageId: string; + } + + /** + * Contains information about the result of unsuccessful publishes to a channel requested by a single {@link Types.BatchPublishSpec}. + */ + interface BatchPublishFailureResult { + /** + * The name of the channel the message(s) failed to be published to. + */ + channel: string; + /** + * Describes the reason for which the message(s) failed to publish to the channel as an {@link ErrorInfo} object. + */ + error: ErrorInfo; + } + // Common Listeners /** * A standard callback format used in most areas of the callback API. @@ -1707,6 +1767,26 @@ declare namespace Types { * @param callback - A function which, upon success, will be called with the time as milliseconds since the Unix epoch. Upon failure, the function will be called with information about the error. */ time(callback?: Types.timeCallback): void; + /** + * Publishes a {@link Types.BatchPublishSpec} object to one or more channels, up to a maximum of 100 channels. + * + * @param spec - A {@link Types.BatchPublishSpec} object. + * @param callback - A function which, upon success, will be called with a {@link Types.BatchResult} object containing information about the result of the batch publish for each requested channel. Upon failure, the function will be called with information about the error. + */ + batchPublish( + spec: BatchPublishSpec, + callback: StandardCallback> + ): void; + /** + * Publishes one or more {@link Types.BatchPublishSpec} objects to one or more channels, up to a maximum of 100 channels. + * + * @param specs - An array of {@link Types.BatchPublishSpec} objects. + * @param callback - A function which, upon success, will be called with an array of {@link Types.BatchResult} objects containing information about the result of the batch publish for each requested channel for each provided {@link Types.BatchPublishSpec}. This array is in the same order as the provided {@link Types.BatchPublishSpec} array. Upon failure, the function will be called with information about the error. + */ + batchPublish( + specs: BatchPublishSpec[], + callback: StandardCallback[]> + ): void; /** * A {@link Types.PushCallbacks} object. */ @@ -1763,6 +1843,23 @@ declare namespace Types { * @returns A promise which, upon success, will be fulfilled with the time as milliseconds since the Unix epoch. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. */ time(): Promise; + + /** + * Publishes a {@link Types.BatchPublishSpec} object to one or more channels, up to a maximum of 100 channels. + * + * @param spec - A {@link Types.BatchPublishSpec} object. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} object containing information about the result of the batch publish for each requested channel. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPublish(spec: BatchPublishSpec): Promise>; + /** + * Publishes one or more {@link Types.BatchPublishSpec} objects to one or more channels, up to a maximum of 100 channels. + * + * @param specs - An array of {@link Types.BatchPublishSpec} objects. + * @returns A promise which, upon success, will be fulfilled with an array of {@link Types.BatchResult} objects containing information about the result of the batch publish for each requested channel for each provided {@link Types.BatchPublishSpec}. This array is in the same order as the provided {@link Types.BatchPublishSpec} array. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPublish( + specs: BatchPublishSpec[] + ): Promise[]>; /** * A {@link Types.PushPromise} object. */ @@ -1848,6 +1945,26 @@ declare namespace Types { * @param callback - A function which, upon success, will be called with the time as milliseconds since the Unix epoch. Upon failure, the function will be called with information about the error. */ time(callback?: Types.timeCallback): void; + /** + * Publishes a {@link Types.BatchPublishSpec} object to one or more channels, up to a maximum of 100 channels. + * + * @param spec - A {@link Types.BatchPublishSpec} object. + * @param callback - A function which, upon success, will be called with a {@link Types.BatchResult} object containing information about the result of the batch publish for each requested channel. Upon failure, the function will be called with information about the error. + */ + batchPublish( + spec: BatchPublishSpec, + callback: StandardCallback> + ): void; + /** + * Publishes one or more {@link Types.BatchPublishSpec} objects to one or more channels, up to a maximum of 100 channels. + * + * @param specs - An array of {@link Types.BatchPublishSpec} objects. + * @param callback - A function which, upon success, will be called with an array of {@link Types.BatchResult} objects containing information about the result of the batch publish for each requested channel for each provided {@link Types.BatchPublishSpec}. This array is in the same order as the provided {@link Types.BatchPublishSpec} array. Upon failure, the function will be called with information about the error. + */ + batchPublish( + specs: BatchPublishSpec[], + callback: StandardCallback[]> + ): void; /** * A {@link Types.PushCallbacks} object. */ @@ -1900,6 +2017,22 @@ declare namespace Types { * @returns A promise which, upon success, will be fulfilled with the time as milliseconds since the Unix epoch. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. */ time(): Promise; + /** + * Publishes a {@link Types.BatchPublishSpec} object to one or more channels, up to a maximum of 100 channels. + * + * @param spec - A {@link Types.BatchPublishSpec} object. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} object containing information about the result of the batch publish for each requested channel. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPublish(spec: BatchPublishSpec): Promise>; + /** + * Publishes one or more {@link Types.BatchPublishSpec} objects to one or more channels, up to a maximum of 100 channels. + * + * @param specs - An array of {@link Types.BatchPublishSpec} objects. + * @returns A promise which, upon success, will be fulfilled with an array of {@link Types.BatchResult} objects containing information about the result of the batch publish for each requested channel for each provided {@link Types.BatchPublishSpec}. This array is in the same order as the provided {@link Types.BatchPublishSpec} array. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPublish( + specs: BatchPublishSpec[] + ): Promise[]>; /** * A {@link Types.PushPromise} object. */ diff --git a/src/common/lib/client/rest.ts b/src/common/lib/client/rest.ts index 1ee41d8dfc..85f53f0bcf 100644 --- a/src/common/lib/client/rest.ts +++ b/src/common/lib/client/rest.ts @@ -12,10 +12,18 @@ import { ChannelOptions } from '../../types/channel'; import { PaginatedResultCallback, StandardCallback } from '../../types/utils'; import { ErrnoException, IHttp, RequestParams } from '../../types/http'; import ClientOptions, { DeprecatedClientOptions, NormalisedClientOptions } from '../../types/ClientOptions'; +import * as API from '../../../../ably'; import Platform from '../../platform'; import Message from '../types/message'; import PresenceMessage from '../types/presencemessage'; +import Resource from './resource'; + +type BatchResult = API.Types.BatchResult; +type BatchPublishSpec = API.Types.BatchPublishSpec; +type BatchPublishSuccessResult = API.Types.BatchPublishSuccessResult; +type BatchPublishFailureResult = API.Types.BatchPublishFailureResult; +type BatchPublishResult = BatchResult; const noop = function () {}; class Rest { @@ -228,6 +236,68 @@ class Rest { } } + batchPublish( + specOrSpecs: T, + callback: API.Types.StandardCallback + ): void; + batchPublish( + specOrSpecs: T + ): Promise; + batchPublish( + specOrSpecs: T, + callbackArg?: API.Types.StandardCallback + ): void | Promise { + if (callbackArg === undefined) { + if (this.options.promises) { + return Utils.promisify(this, 'batchPublish', [specOrSpecs]); + } + callbackArg = noop; + } + + const callback = callbackArg; + + let requestBodyDTO: BatchPublishSpec[]; + let singleSpecMode: boolean; + if (Utils.isArray(specOrSpecs)) { + requestBodyDTO = specOrSpecs; + singleSpecMode = false; + } else { + requestBodyDTO = [specOrSpecs]; + singleSpecMode = true; + } + + const format = this.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + headers = Utils.defaultPostHeaders(this.options, format); + + if (this.options.headers) Utils.mixin(headers, this.options.headers); + + const requestBody = Utils.encodeBody(requestBodyDTO, format); + Resource.post( + this, + '/messages', + requestBody, + headers, + { newBatchResponse: 'true' }, + null, + (err, body, headers, unpacked) => { + if (err) { + // TODO remove this type assertion after fixing https://github.com/ably/ably-js/issues/1405 + callback(err as API.Types.ErrorInfo); + return; + } + + const batchResults = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPublishResult[]; + + // I don't love the below type assertions for `callback` but not sure how to avoid them + if (singleSpecMode) { + (callback as API.Types.StandardCallback)(null, batchResults[0]); + } else { + (callback as API.Types.StandardCallback)(null, batchResults); + } + } + ); + } + setLog(logOptions: LoggerOptions): void { Logger.setLog(logOptions.level, logOptions.handler); } diff --git a/test/rest/batch.test.js b/test/rest/batch.test.js new file mode 100644 index 0000000000..c20bb43cee --- /dev/null +++ b/test/rest/batch.test.js @@ -0,0 +1,343 @@ +'use strict'; + +// NOTE: All of the Promise-related tests in this file are intentionally a copy of the callback versions. This will allow us to simply remove the callback versions when merging this functionality into the integration/v2 branch (https://github.com/ably/ably-js/issues/1411). + +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var expect = chai.expect; + + describe('rest/batchPublish', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); + }); + + describe('when invoked with an array of specs', function () { + it('performs a batch publish and returns an array of results', function (done) { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + key: testApp.keys[2].keyStr /* we use this key so that some publishes fail due to capabilities */, + }); + const verificationRest = helper.AblyRest(); + + const specs = [ + { + channels: [ + 'channel0' /* key allows publishing to this channel */, + 'channel3' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message1' }, { data: 'message2' }], + }, + { + channels: [ + 'channel4' /* key allows publishing to this channel */, + 'channel5' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message3' }, { data: 'message4' }], + }, + ]; + + async.series( + [ + // First, we perform the batch publish request... + function (cb) { + rest.batchPublish(specs, function (err, batchResults) { + if (err) { + cb(err); + return; + } + + try { + expect(batchResults).to.have.lengthOf(specs.length); + + expect(batchResults[0].successCount).to.equal(1); + expect(batchResults[0].failureCount).to.equal(1); + + // Check the results of first BatchPublishSpec + + expect(batchResults[0].results).to.have.lengthOf(2); + + expect(batchResults[0].results[0].channel).to.equal('channel0'); + expect(batchResults[0].results[0].messageId).to.include(':0'); + expect('error' in batchResults[0].results[0]).to.be.false; + + expect(batchResults[0].results[1].channel).to.equal('channel3'); + expect('messageId' in batchResults[0].results[1]).to.be.false; + expect(batchResults[0].results[1].error.statusCode).to.equal(401); + + // Check the results of second BatchPublishSpec + + expect(batchResults[1].results).to.have.lengthOf(2); + + expect(batchResults[1].results[0].channel).to.equal('channel4'); + expect(batchResults[1].results[0].messageId).to.include(':0'); + expect('error' in batchResults[1].results[0]).to.be.false; + + expect(batchResults[1].results[1].channel).to.equal('channel5'); + expect('messageId' in batchResults[1].results[1]).to.be.false; + expect(batchResults[1].results[1].error.statusCode).to.equal(401); + } catch (err) { + cb(err); + return; + } + + cb(); + }); + }, + function (cb) { + // ...and now we use channel history to check that the expected messages have been published. + async.parallel( + [ + function (cb) { + const channel0 = verificationRest.channels.get('channel0'); + channel0.history({ limit: 2 }, function (err, result) { + if (err) { + cb(err); + return; + } + + const data = new Set([result.items[0].data, result.items[1].data]); + + try { + expect(data).to.deep.equal(new Set(['message1', 'message2'])); + } catch (err) { + cb(err); + return; + } + + cb(); + }); + }, + function (cb) { + const channel4 = verificationRest.channels.get('channel4'); + channel4.history({ limit: 2 }, function (err, result) { + if (err) { + cb(err); + return; + } + + const data = new Set([result.items[0].data, result.items[1].data]); + try { + expect(data).to.deep.equal(new Set(['message3', 'message4'])); + } catch (err) { + cb(err); + return; + } + + cb(); + }); + }, + ], + cb + ); + }, + ], + done + ); + }); + }); + + describe('when invoked with a single spec', function () { + it('performs a batch publish and returns a single result', function (done) { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + key: testApp.keys[2].keyStr /* we use this key so that some publishes fail due to capabilities */, + }); + const verificationRest = helper.AblyRest(); + + const spec = { + channels: [ + 'channel0' /* key allows publishing to this channel */, + 'channel3' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message1' }, { data: 'message2' }], + }; + + async.series( + [ + // First, we perform the batch publish request... + function (cb) { + rest.batchPublish(spec, function (err, batchResult) { + if (err) { + cb(err); + return; + } + + try { + expect(batchResult.successCount).to.equal(1); + expect(batchResult.failureCount).to.equal(1); + + expect(batchResult.results).to.have.lengthOf(2); + + expect(batchResult.results[0].channel).to.equal('channel0'); + expect(batchResult.results[0].messageId).to.include(':0'); + expect('error' in batchResult.results[0]).to.be.false; + + expect(batchResult.results[1].channel).to.equal('channel3'); + expect('messageId' in batchResult.results[1]).to.be.false; + expect(batchResult.results[1].error.statusCode).to.equal(401); + } catch (err) { + cb(err); + return; + } + + cb(); + }); + }, + function (cb) { + // ...and now we use channel history to check that the expected messages have been published. + const channel0 = verificationRest.channels.get('channel0'); + channel0.history({ limit: 2 }, function (err, result) { + if (err) { + cb(err); + return; + } + + const data = new Set([result.items[0].data, result.items[1].data]); + try { + expect(data).to.deep.equal(new Set(['message1', 'message2'])); + } catch (err) { + cb(err); + return; + } + + cb(); + }); + }, + ], + done + ); + }); + }); + + if (typeof Promise !== 'undefined') { + describe('using promises', function () { + describe('when invoked with an array of specs', function () { + it('performs a batch publish and returns an array of results', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[2].keyStr /* we use this key so that some publishes fail due to capabilities */, + }); + + const specs = [ + { + channels: [ + 'channel0' /* key allows publishing to this channel */, + 'channel3' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message1' }, { data: 'message2' }], + }, + { + channels: [ + 'channel4' /* key allows publishing to this channel */, + 'channel5' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message3' }, { data: 'message4' }], + }, + ]; + + // First, we perform the batch publish request... + const batchResults = await rest.batchPublish(specs); + + expect(batchResults).to.have.lengthOf(specs.length); + + expect(batchResults[0].successCount).to.equal(1); + expect(batchResults[0].failureCount).to.equal(1); + + // Check the results of first BatchPublishSpec + + expect(batchResults[0].results).to.have.lengthOf(2); + + expect(batchResults[0].results[0].channel).to.equal('channel0'); + expect(batchResults[0].results[0].messageId).to.include(':0'); + expect('error' in batchResults[0].results[0]).to.be.false; + + expect(batchResults[0].results[1].channel).to.equal('channel3'); + expect('messageId' in batchResults[0].results[1]).to.be.false; + expect(batchResults[0].results[1].error.statusCode).to.equal(401); + + // Check the results of second BatchPublishSpec + + expect(batchResults[1].results).to.have.lengthOf(2); + + expect(batchResults[1].results[0].channel).to.equal('channel4'); + expect(batchResults[1].results[0].messageId).to.include(':0'); + expect('error' in batchResults[1].results[0]).to.be.false; + + expect(batchResults[1].results[1].channel).to.equal('channel5'); + expect('messageId' in batchResults[1].results[1]).to.be.false; + expect(batchResults[1].results[1].error.statusCode).to.equal(401); + + // ...and now we use channel history to check that the expected messages have been published. + const verificationRest = helper.AblyRest({ promises: true }); + + const channel0 = verificationRest.channels.get('channel0'); + const channel0HistoryPromise = channel0.history({ limit: 2 }); + + const channel4 = verificationRest.channels.get('channel4'); + const channel4HistoryPromise = channel4.history({ limit: 2 }); + + const [channel0History, channel4History] = await Promise.all([ + channel0HistoryPromise, + channel4HistoryPromise, + ]); + + const channel0HistoryData = new Set([channel0History.items[0].data, channel0History.items[1].data]); + expect(channel0HistoryData).to.deep.equal(new Set(['message1', 'message2'])); + + const channel4HistoryData = new Set([channel4History.items[0].data, channel4History.items[1].data]); + expect(channel4HistoryData).to.deep.equal(new Set(['message3', 'message4'])); + }); + }); + + describe('when invoked with a single spec', function () { + it('performs a batch publish and returns a single result', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[2].keyStr /* we use this key so that some publishes fail due to capabilities */, + }); + + const spec = { + channels: [ + 'channel0' /* key allows publishing to this channel */, + 'channel3' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message1' }, { data: 'message2' }], + }; + + // First, we perform the batch publish request... + const batchResult = await rest.batchPublish(spec); + + expect(batchResult.successCount).to.equal(1); + expect(batchResult.failureCount).to.equal(1); + + expect(batchResult.results).to.have.lengthOf(2); + + expect(batchResult.results[0].channel).to.equal('channel0'); + expect(batchResult.results[0].messageId).to.include(':0'); + expect('error' in batchResult.results[0]).to.be.false; + + expect(batchResult.results[1].channel).to.equal('channel3'); + expect('messageId' in batchResult.results[1]).to.be.false; + expect(batchResult.results[1].error.statusCode).to.equal(401); + + // ...and now we use channel history to check that the expected messages have been published. + const verificationRest = helper.AblyRest({ promises: true }); + const channel0 = verificationRest.channels.get('channel0'); + const channel0History = await channel0.history({ limit: 2 }); + + const channel0HistoryData = new Set([channel0History.items[0].data, channel0History.items[1].data]); + expect(channel0HistoryData).to.deep.equal(new Set(['message1', 'message2'])); + }); + }); + }); + } + }); +}); diff --git a/test/support/browser_file_list.js b/test/support/browser_file_list.js index 31369d1f3a..6175faaf54 100644 --- a/test/support/browser_file_list.js +++ b/test/support/browser_file_list.js @@ -70,4 +70,5 @@ window.__testFiles__.files = { 'test/browser/simple.test.js': true, 'test/browser/http.test.js': true, 'test/rest/status.test.js': true, + 'test/rest/batch.test.js': true, }; From f52b1ab7a9e24e81c0d57e9fc10656f1e88dac13 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 17 Jul 2023 15:19:34 -0300 Subject: [PATCH 44/46] Implement REST batch presence As described by spec at commit 18ef967. Documentation based on sdk-api-reference repo at commit 652ef2f. Part of #989. --- ably.d.ts | 62 ++++++++++++++++ src/common/lib/client/rest.ts | 45 ++++++++++++ test/rest/batch.test.js | 131 ++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) diff --git a/ably.d.ts b/ably.d.ts index bf94252d61..f4b7ee5c13 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -1494,6 +1494,34 @@ declare namespace Types { error: ErrorInfo; } + /** + * Contains information about the result of a successful batch presence request for a single channel. + */ + interface BatchPresenceSuccessResult { + /** + * The channel name the presence state was retrieved for. + */ + channel: string; + /** + * An array of {@link PresenceMessage}s describing members present on the channel. + */ + presence: PresenceMessage[]; + } + + /** + * Contains information about the result of an unsuccessful batch presence request for a single channel. + */ + interface BatchPresenceFailureResult { + /** + * The channel name the presence state failed to be retrieved for. + */ + channel: string; + /** + * Describes the reason for which presence state could not be retrieved for the channel as an {@link ErrorInfo} object. + */ + error: ErrorInfo; + } + // Common Listeners /** * A standard callback format used in most areas of the callback API. @@ -1787,6 +1815,16 @@ declare namespace Types { specs: BatchPublishSpec[], callback: StandardCallback[]> ): void; + /** + * Retrieves the presence state for one or more channels, up to a maximum of 100 channels. Presence state includes the `clientId` of members and their current {@link Types.PresenceAction}. + * + * @param channels - An array of one or more channel names, up to a maximum of 100 channels. + * @param callback - A function which, upon success, will be called with a {@link Types.BatchResult} object containing information about the result of the batch presence request for each requested channel. Upon failure, the function will be called with information about the error. + */ + batchPresence( + channels: string[], + callback: StandardCallback> + ): void; /** * A {@link Types.PushCallbacks} object. */ @@ -1860,6 +1898,13 @@ declare namespace Types { batchPublish( specs: BatchPublishSpec[] ): Promise[]>; + /** + * Retrieves the presence state for one or more channels, up to a maximum of 100 channels. Presence state includes the `clientId` of members and their current {@link Types.PresenceAction}. + * + * @param channels - An array of one or more channel names, up to a maximum of 100 channels. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} object containing information about the result of the batch presence request for each requested channel. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPresence(channels: string[]): Promise[]>; /** * A {@link Types.PushPromise} object. */ @@ -1965,6 +2010,16 @@ declare namespace Types { specs: BatchPublishSpec[], callback: StandardCallback[]> ): void; + /** + * Retrieves the presence state for one or more channels, up to a maximum of 100 channels. Presence state includes the `clientId` of members and their current {@link Types.PresenceAction}. + * + * @param channels - An array of one or more channel names, up to a maximum of 100 channels. + * @param callback - A function which, upon success, will be called with a {@link Types.BatchResult} object containing information about the result of the batch presence request for each requested channel. Upon failure, the function will be called with information about the error. + */ + batchPresence( + channels: string[], + callback: StandardCallback[]> + ): void; /** * A {@link Types.PushCallbacks} object. */ @@ -2033,6 +2088,13 @@ declare namespace Types { batchPublish( specs: BatchPublishSpec[] ): Promise[]>; + /** + * Retrieves the presence state for one or more channels, up to a maximum of 100 channels. Presence state includes the `clientId` of members and their current {@link Types.PresenceAction}. + * + * @param channels - An array of one or more channel names, up to a maximum of 100 channels. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} object containing information about the result of the batch presence request for each requested channel. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPresence(channels: string[]): Promise[]>; /** * A {@link Types.PushPromise} object. */ diff --git a/src/common/lib/client/rest.ts b/src/common/lib/client/rest.ts index 85f53f0bcf..ef7dd31dcc 100644 --- a/src/common/lib/client/rest.ts +++ b/src/common/lib/client/rest.ts @@ -24,6 +24,9 @@ type BatchPublishSpec = API.Types.BatchPublishSpec; type BatchPublishSuccessResult = API.Types.BatchPublishSuccessResult; type BatchPublishFailureResult = API.Types.BatchPublishFailureResult; type BatchPublishResult = BatchResult; +type BatchPresenceSuccessResult = API.Types.BatchPresenceSuccessResult; +type BatchPresenceFailureResult = API.Types.BatchPresenceFailureResult; +type BatchPresenceResult = BatchResult; const noop = function () {}; class Rest { @@ -298,6 +301,48 @@ class Rest { ); } + batchPresence(channels: string[], callback: API.Types.StandardCallback): void; + batchPresence(channels: string[]): Promise; + batchPresence( + channels: string[], + callbackArg?: API.Types.StandardCallback + ): void | Promise { + if (callbackArg === undefined) { + if (this.options.promises) { + return Utils.promisify(this, 'batchPresence', [channels]); + } + callbackArg = noop; + } + + const callback = callbackArg; + + const format = this.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + headers = Utils.defaultPostHeaders(this.options, format); + + if (this.options.headers) Utils.mixin(headers, this.options.headers); + + const channelsParam = channels.join(','); + + Resource.get( + this, + '/presence', + headers, + { newBatchResponse: 'true', channels: channelsParam }, + null, + (err, body, headers, unpacked) => { + if (err) { + // TODO remove this type assertion after fixing https://github.com/ably/ably-js/issues/1405 + callback(err as API.Types.ErrorInfo); + return; + } + + const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPresenceResult; + + callback(null, batchResult); + } + ); + } + setLog(logOptions: LoggerOptions): void { Logger.setLog(logOptions.level, logOptions.handler); } diff --git a/test/rest/batch.test.js b/test/rest/batch.test.js index c20bb43cee..9af228375a 100644 --- a/test/rest/batch.test.js +++ b/test/rest/batch.test.js @@ -4,6 +4,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { var expect = chai.expect; + var closeAndFinish = helper.closeAndFinish; describe('rest/batchPublish', function () { this.timeout(60 * 1000); @@ -340,4 +341,134 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }); } }); + + describe('rest/batchPresence', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); + }); + + it('performs a batch presence fetch and returns a result', function (done) { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + key: testApp.keys[2].keyStr /* we use this key so that some presence fetches fail due to capabilities */, + }); + + const presenceEnterRealtime = helper.AblyRealtime( + { + clientId: 'batchPresenceTest', + } /* note that the key used here has no capability limitations, so that we can use this instance to enter presence below */ + ); + + const channelNames = [ + 'channel0' /* key does not allow presence on this channel */, + 'channel4' /* key allows presence on this channel */, + ]; + + async.series( + [ + // First, we enter presence on two channels... + function (cb) { + presenceEnterRealtime.channels.get('channel0').presence.enter(cb); + }, + function (cb) { + presenceEnterRealtime.channels.get('channel4').presence.enter(cb); + }, + // ...and now we perform the batch presence request. + function (cb) { + rest.batchPresence(channelNames, function (err, batchResult) { + if (err) { + cb(err); + } + + try { + expect(batchResult.successCount).to.equal(1); + expect(batchResult.failureCount).to.equal(1); + + // Check that the channel0 presence fetch request fails (due to key’s capabilities, as mentioned above) + + expect(batchResult.results[0].channel).to.equal('channel0'); + expect('presence' in batchResult.results[0]).to.be.false; + expect(batchResult.results[0].error.statusCode).to.equal(401); + + // Check that the channel4 presence fetch request reflects the presence enter performed above + + expect(batchResult.results[1].channel).to.equal('channel4'); + expect(batchResult.results[1].presence).to.have.lengthOf(1); + expect(batchResult.results[1].presence[0].clientId).to.equal('batchPresenceTest'); + expect('error' in batchResult.results[1]).to.be.false; + } catch (err) { + cb(err); + return; + } + + cb(); + }); + }, + function (cb) { + closeAndFinish(cb, presenceEnterRealtime); + }, + ], + done + ); + }); + + if (typeof Promise !== 'undefined') { + describe('using promises', function () { + it('performs a batch presence fetch and returns a result', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[2].keyStr /* we use this key so that some presence fetches fail due to capabilities */, + }); + + const presenceEnterRealtime = helper.AblyRealtime({ + promises: true, + clientId: + 'batchPresenceTest' /* note that the key used here has no capability limitations, so that we can use this instance to enter presence below */, + }); + + const channelNames = [ + 'channel0' /* key does not allow presence on this channel */, + 'channel4' /* key allows presence on this channel */, + ]; + + // First, we enter presence on two channels... + await presenceEnterRealtime.channels.get('channel0').presence.enter(); + await presenceEnterRealtime.channels.get('channel4').presence.enter(); + + // ...and now we perform the batch presence request. + const batchResult = await rest.batchPresence(channelNames); + + expect(batchResult.successCount).to.equal(1); + expect(batchResult.failureCount).to.equal(1); + + // Check that the channel0 presence fetch request fails (due to key’s capabilities, as mentioned above) + + expect(batchResult.results[0].channel).to.equal('channel0'); + expect('presence' in batchResult.results[0]).to.be.false; + expect(batchResult.results[0].error.statusCode).to.equal(401); + + // Check that the channel4 presence fetch request reflects the presence enter performed above + + expect(batchResult.results[1].channel).to.equal('channel4'); + expect(batchResult.results[1].presence).to.have.lengthOf(1); + expect(batchResult.results[1].presence[0].clientId).to.equal('batchPresenceTest'); + expect('error' in batchResult.results[1]).to.be.false; + + await new Promise((resolve, reject) => { + closeAndFinish((err) => { + err ? reject(err) : resolve(); + }, presenceEnterRealtime); + }); + }); + }); + } + }); }); From 3e05117c898f59b76e887cf1d1d67ab6f6066d7f Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 31 Jul 2023 08:40:31 -0300 Subject: [PATCH 45/46] Pull randomString function out into test helper --- test/common/modules/shared_helper.js | 5 +++++ test/realtime/channel.test.js | 6 +----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/common/modules/shared_helper.js b/test/common/modules/shared_helper.js index e226dc5f07..d6b6812910 100644 --- a/test/common/modules/shared_helper.js +++ b/test/common/modules/shared_helper.js @@ -212,6 +212,10 @@ define([ return res; }; + function randomString() { + return Math.random().toString().slice(2); + } + return (module.exports = { setupApp: testAppModule.setup, tearDownApp: testAppModule.tearDown, @@ -242,5 +246,6 @@ define([ unroutableAddress: unroutableAddress, arrFind: arrFind, arrFilter: arrFilter, + randomString: randomString, }); }); diff --git a/test/realtime/channel.test.js b/test/realtime/channel.test.js index 886548bdf9..252a9b1ef6 100644 --- a/test/realtime/channel.test.js +++ b/test/realtime/channel.test.js @@ -9,11 +9,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async var monitorConnection = helper.monitorConnection; var createPM = Ably.Realtime.ProtocolMessage.fromDeserialized; var testOnAllTransports = helper.testOnAllTransports; - - /* Helpers */ - function randomString() { - return Math.random().toString().slice(2); - } + var randomString = helper.randomString; function checkCanSubscribe(channel, testChannel) { return function (callback) { From 88bac53663c2adc3993cdd8a7a726d7732cefae0 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 17 Jul 2023 16:04:15 -0300 Subject: [PATCH 46/46] Implement REST token revocation As described by spec at commit 18ef967. Documentation based on sdk-api-reference repo at commit 652ef2f. Note that the tests which revoke tokens do so using a random clientId. This is to prevent the revocations performed in one test from having an effect on tokens issued in a subsequent test. (I observed this happening, and Simon confirmed that it's possible for a token issued very shortly after a revocation to be affected by that revocation.) Resolves #989. --- ably.d.ts | 86 ++++++++ src/common/lib/client/auth.ts | 78 +++++++ test/rest/batch.test.js | 393 ++++++++++++++++++++++++++++++++++ 3 files changed, 557 insertions(+) diff --git a/ably.d.ts b/ably.d.ts index f4b7ee5c13..cfeb56c63d 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -1522,6 +1522,69 @@ declare namespace Types { error: ErrorInfo; } + /** + * The `TokenRevocationOptions` interface describes the additional options accepted by the following methods: + * + * - {@link AuthCallbacks.revokeTokens} + * - {@link AuthPromise.revokeTokens} + */ + interface TokenRevocationOptions { + /** + * A Unix timestamp in milliseconds where only tokens issued before this time are revoked. The default is the current time. Requests with an `issuedBefore` in the future, or more than an hour in the past, will be rejected. + */ + issuedBefore?: number; + /** + * If true, permits a token renewal cycle to take place without needing established connections to be dropped, by postponing enforcement to 30 seconds in the future, and sending any existing connections a hint to obtain (and upgrade the connection to use) a new token. The default is `false`, meaning that the effect is near-immediate. + */ + allowReauthMargin?: boolean; + } + + /** + * Describes which tokens should be affected by a token revocation request. + */ + interface TokenRevocationTargetSpecifier { + /** + * The type of token revocation target specifier. Valid values include `clientId`, `revocationKey` and `channel`. + */ + type: string; + /** + * The value of the token revocation target specifier. + */ + value: string; + } + + /** + * Contains information about the result of a successful token revocation request for a single target specifier. + */ + interface TokenRevocationSuccessResult { + /** + * The target specifier. + */ + target: string; + /** + * The time at which the token revocation will take effect, as a Unix timestamp in milliseconds. + */ + appliesAt: number; + /** + * A Unix timestamp in milliseconds. Only tokens issued earlier than this time will be revoked. + */ + issuedBefore: number; + } + + /** + * Contains information about the result of an unsuccessful token revocation request for a single target specifier. + */ + interface TokenRevocationFailureResult { + /** + * The target specifier. + */ + target: string; + /** + * Describes the reason for which token revocation failed for the given `target` as an {@link ErrorInfo} object. + */ + error: ErrorInfo; + } + // Common Listeners /** * A standard callback format used in most areas of the callback API. @@ -2186,6 +2249,18 @@ declare namespace Types { * @param callback - A function which, upon success, will be called with a {@link TokenDetails} object. Upon failure, the function will be called with information about the error. */ requestToken(callback?: tokenDetailsCallback): void; + /** + * Revokes the tokens specified by the provided array of {@link TokenRevocationTargetSpecifier}s. Only tokens issued by an API key that had revocable tokens enabled before the token was issued can be revoked. See the [token revocation docs](https://ably.com/docs/core-features/authentication#token-revocation) for more information. + * + * @param specifiers - An array of {@link TokenRevocationTargetSpecifier} objects. + * @param options - A set of options which are used to modify the revocation request. + * @param callback - A function which, upon success, will be called with a {@link Types.BatchResult} containing information about the result of the token revocation request for each provided [`TokenRevocationTargetSpecifier`]{@link TokenRevocationTargetSpecifier}. Upon failure, the function will be called with information about the error. + */ + revokeTokens( + specifiers: TokenRevocationTargetSpecifier[], + options?: TokenRevocationOptions, + callback?: StandardCallback> + ): void; } /** @@ -2216,6 +2291,17 @@ declare namespace Types { * @returns A promise which, upon success, will be fulfilled with a {@link TokenDetails} object. Upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error. */ requestToken(TokenParams?: TokenParams, authOptions?: AuthOptions): Promise; + /** + * Revokes the tokens specified by the provided array of {@link TokenRevocationTargetSpecifier}s. Only tokens issued by an API key that had revocable tokens enabled before the token was issued can be revoked. See the [token revocation docs](https://ably.com/docs/core-features/authentication#token-revocation) for more information. + * + * @param specifiers - An array of {@link TokenRevocationTargetSpecifier} objects. + * @param options - A set of options which are used to modify the revocation request. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} containing information about the result of the token revocation request for each provided [`TokenRevocationTargetSpecifier`]{@link TokenRevocationTargetSpecifier}. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + revokeTokens( + specifiers: TokenRevocationTargetSpecifier[], + options?: TokenRevocationOptions + ): Promise>; } /** diff --git a/src/common/lib/client/auth.ts b/src/common/lib/client/auth.ts index b92a3668d8..5b0a855b63 100644 --- a/src/common/lib/client/auth.ts +++ b/src/common/lib/client/auth.ts @@ -14,6 +14,14 @@ import ClientOptions from '../../types/ClientOptions'; import HttpMethods from '../../constants/HttpMethods'; import HttpStatusCodes from 'common/constants/HttpStatusCodes'; import Platform from '../../platform'; +import Resource from './resource'; + +type BatchResult = API.Types.BatchResult; +type TokenRevocationTargetSpecifier = API.Types.TokenRevocationTargetSpecifier; +type TokenRevocationOptions = API.Types.TokenRevocationOptions; +type TokenRevocationSuccessResult = API.Types.TokenRevocationSuccessResult; +type TokenRevocationFailureResult = API.Types.TokenRevocationFailureResult; +type TokenRevocationResult = BatchResult; const MAX_TOKEN_LENGTH = Math.pow(2, 17); function noop() {} @@ -1054,6 +1062,76 @@ class Auth { static isTokenErr(error: IPartialErrorInfo) { return error.code && error.code >= 40140 && error.code < 40150; } + + revokeTokens( + specifiers: TokenRevocationTargetSpecifier[], + options?: TokenRevocationOptions, + callback?: API.Types.StandardCallback + ): void; + revokeTokens( + specifiers: TokenRevocationTargetSpecifier[], + options?: TokenRevocationOptions + ): Promise; + revokeTokens( + specifiers: TokenRevocationTargetSpecifier[], + optionsOrCallbackArg?: TokenRevocationOptions | API.Types.StandardCallback, + callbackArg?: API.Types.StandardCallback + ): void | Promise { + if (useTokenAuth(this.client.options)) { + throw new ErrorInfo('Cannot revoke tokens when using token auth', 40162, 401); + } + + const keyName = this.client.options.keyName!; + + let resolvedOptions: TokenRevocationOptions; + + if (typeof optionsOrCallbackArg === 'function') { + callbackArg = optionsOrCallbackArg; + resolvedOptions = {}; + } else { + resolvedOptions = optionsOrCallbackArg ?? {}; + } + + if (callbackArg === undefined) { + if (this.client.options.promises) { + return Utils.promisify(this, 'revokeTokens', [specifiers, resolvedOptions]); + } + callbackArg = noop; + } + + const callback = callbackArg; + + const requestBodyDTO = { + targets: specifiers.map((specifier) => `${specifier.type}:${specifier.value}`), + ...resolvedOptions, + }; + + const format = this.client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + headers = Utils.defaultPostHeaders(this.client.options, format); + + if (this.client.options.headers) Utils.mixin(headers, this.client.options.headers); + + const requestBody = Utils.encodeBody(requestBodyDTO, format); + Resource.post( + this.client, + `/keys/${keyName}/revokeTokens`, + requestBody, + headers, + { newBatchResponse: 'true' }, + null, + (err, body, headers, unpacked) => { + if (err) { + // TODO remove this type assertion after fixing https://github.com/ably/ably-js/issues/1405 + callback(err as API.Types.ErrorInfo); + return; + } + + const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as TokenRevocationResult; + + callback(null, batchResult); + } + ); + } } export default Auth; diff --git a/test/rest/batch.test.js b/test/rest/batch.test.js index 9af228375a..b4f747311f 100644 --- a/test/rest/batch.test.js +++ b/test/rest/batch.test.js @@ -5,6 +5,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { var expect = chai.expect; var closeAndFinish = helper.closeAndFinish; + var randomString = helper.randomString; describe('rest/batchPublish', function () { this.timeout(60 * 1000); @@ -471,4 +472,396 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }); } }); + + describe('rest/revokeTokens', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); + }); + + it('revokes tokens matching the given specifiers', function (done) { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + key: testApp.keys[4].keyStr /* this key has revocableTokens enabled */, + }); + + const clientId1 = `clientId1-${randomString()}`; + const clientId2 = `clientId2-${randomString()}`; + + let clientId1TokenDetails; + let clientId2TokenDetails; + + let clientId1Realtime; + let clientId2Realtime; + + // These (result, callback) pairings are a dance to simulate a Promise (specificially the fact that the order of the { resolve, then } operations doesn’t matter); see the promise-based version of this test + let clientId1RealtimeDisconnectedStateChange; + let onClientId1RealtimeDisconnected; + let clientId2RealtimeDisconnectedStateChange; + let onClientId2RealtimeDisconnected; + + async.series( + [ + function (cb) { + // First, we fetch tokens for a couple of different clientIds... + async.parallel( + [ + function (cb) { + rest.auth.requestToken({ clientId: clientId1 }, function (err, tokenDetails) { + if (err) { + cb(err); + return; + } + + clientId1TokenDetails = tokenDetails; + cb(); + }); + }, + function (cb) { + rest.auth.requestToken({ clientId: clientId2 }, function (err, tokenDetails) { + if (err) { + cb(err); + return; + } + + clientId2TokenDetails = tokenDetails; + cb(); + }); + }, + ], + cb + ); + }, + function (cb) { + // ...then, we set up Realtime instances that use these tokens and wait for them to become CONNECTED... + async.parallel( + [ + function (cb) { + clientId1Realtime = helper.AblyRealtime({ token: clientId1TokenDetails }); + clientId1Realtime.connection.once('connected', function () { + cb(); + }); + }, + function (cb) { + clientId2Realtime = helper.AblyRealtime({ token: clientId2TokenDetails }); + clientId2Realtime.connection.once('connected', function () { + cb(); + }); + }, + ], + cb + ); + }, + function (cb) { + // ...then, we set up listeners that will record the state change when these Realtime instances become DISCONNECTED (we need to set up these listeners here, before performing the revocation request, else we might miss the DISCONNECTED state changes that the token revocation provokes, and end up only seeing the subsequent RSA4a2-induced FAILED state change, which due to https://github.com/ably/ably-js/issues/1409 does not expose the 40141 "token revoked" error code)... + // + // Note: + // + // We use Realtime instances for verifying the side effects of a token revocation, as opposed to, say, trying to perform a REST request, because the nature of the Ably service is that token verification may take a small delay to become active, and so there's no guarantee that a REST request peformed immediately after a revocation request would fail. See discussion at https://ably-real-time.slack.com/archives/C030C5YLY/p1690322740850269?thread_ts=1690315022.372729&cid=C030C5YLY. + + clientId1Realtime.connection.once('disconnected', function (stateChange) { + clientId1RealtimeDisconnectedStateChange = stateChange; + if (onClientId1RealtimeDisconnected) { + onClientId1RealtimeDisconnected(); + } + }); + + clientId2Realtime.connection.once('disconnected', function (stateChange) { + clientId2RealtimeDisconnectedStateChange = stateChange; + if (onClientId2RealtimeDisconnected) { + onClientId2RealtimeDisconnected(); + } + }); + + cb(); + }, + function (cb) { + // ...then, we revoke all tokens for these clientIds... + + const specifiers = [ + { type: 'clientId', value: clientId1 }, + { type: 'clientId', value: clientId2 }, + { type: 'invalidType', value: 'abc' }, // we include an invalid specifier type to provoke a non-zero failureCount + ]; + + rest.auth.revokeTokens(specifiers, function (err, result) { + if (err) { + cb(err); + return; + } + + try { + // ...and check the response from the revocation request... + expect(result.successCount).to.equal(2); + expect(result.failureCount).to.equal(1); + expect(result.results).to.have.lengthOf(3); + + expect(result.results[0].target).to.equal(`clientId:${clientId1}`); + expect(typeof result.results[0].issuedBefore).to.equal('number'); + expect(typeof result.results[0].appliesAt).to.equal('number'); + expect('error' in result.results[0]).to.be.false; + + expect(result.results[1].target).to.equal(`clientId:${clientId2}`); + expect(typeof result.results[1].issuedBefore).to.equal('number'); + expect(typeof result.results[1].appliesAt).to.equal('number'); + expect('error' in result.results[1]).to.be.false; + + expect(result.results[2].target).to.equal('invalidType:abc'); + expect(result.results[2].error.statusCode).to.equal(400); + } catch (err) { + cb(err); + return; + } + + cb(); + }); + }, + + // ...and then, we check that the Realtime instances transition to the DISCONNECTED state due to a "token revoked" (40141) error. + function (cb) { + async.parallel( + [ + function (cb) { + onClientId1RealtimeDisconnected = function () { + try { + expect(clientId1RealtimeDisconnectedStateChange.reason.code).to.equal(40141 /* token revoked */); + } catch (err) { + cb(err); + return; + } + cb(); + }; + if (clientId1RealtimeDisconnectedStateChange) { + onClientId1RealtimeDisconnected(); + } + }, + function (cb) { + onClientId2RealtimeDisconnected = function () { + try { + expect(clientId2RealtimeDisconnectedStateChange.reason.code).to.equal(40141 /* token revoked */); + } catch (err) { + cb(err); + return; + } + cb(); + }; + if (clientId2RealtimeDisconnectedStateChange) { + onClientId2RealtimeDisconnected(); + } + }, + ], + cb + ); + }, + function (cb) { + async.parallel( + [ + function (cb) { + closeAndFinish(cb, clientId1Realtime); + }, + function (cb) { + closeAndFinish(cb, clientId2Realtime); + }, + ], + cb + ); + }, + ], + done + ); + }); + + it('accepts optional issuedBefore and allowReauthMargin parameters', function (done) { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + key: testApp.keys[4].keyStr /* this key has revocableTokens enabled */, + }); + + const clientId = `clientId-${randomString()}`; + + let serverTimeAtStartOfTest; + + async.series( + [ + function (cb) { + rest.time(function (err, time) { + if (err) { + cb(err); + return; + } + serverTimeAtStartOfTest = time; + cb(); + }); + }, + function (cb) { + const issuedBefore = serverTimeAtStartOfTest - 20 * 60 * 1000; // i.e. ~20 minutes ago (arbitrarily chosen) + + rest.auth.revokeTokens( + [{ type: 'clientId', value: clientId }], + { issuedBefore, allowReauthMargin: true }, + function (err, result) { + if (err) { + cb(err); + return; + } + + try { + expect(result.results[0].issuedBefore).to.equal(issuedBefore); + + // Verify the expected side effect of allowReauthMargin, which is to delay the revocation by 30 seconds + const serverTimeThirtySecondsAfterStartOfTest = serverTimeAtStartOfTest + 30 * 1000; + expect(result.results[0].appliesAt).to.be.greaterThan(serverTimeThirtySecondsAfterStartOfTest); + } catch (err) { + cb(err); + return; + } + + cb(); + } + ); + }, + ], + done + ); + }); + + it('throws an error when using token auth', function () { + const rest = helper.AblyRest({ + useTokenAuth: true, + }); + + let verifiedError = false; + try { + rest.auth.revokeTokens([{ type: 'clientId', value: 'clientId1' }], function () {}); + } catch (err) { + expect(err.statusCode).to.equal(401); + expect(err.code).to.equal(40162); + verifiedError = true; + } + + expect(verifiedError).to.be.true; + }); + + if (typeof Promise !== 'undefined') { + describe('using promises', function () { + it('revokes tokens matching the given specifiers', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[4].keyStr /* this key has revocableTokens enabled */, + }); + + const clientId1 = `clientId1-${randomString()}`; + const clientId2 = `clientId2-${randomString()}`; + + // First, we fetch tokens for a couple of different clientIds... + const [clientId1TokenDetails, clientId2TokenDetails] = await Promise.all([ + rest.auth.requestToken({ clientId: clientId1 }), + rest.auth.requestToken({ clientId: clientId2 }), + ]); + + // ...then, we set up Realtime instances that use these tokens and wait for them to become CONNECTED... + const clientId1Realtime = helper.AblyRealtime({ + promises: true, + token: clientId1TokenDetails, + }); + const clientId2Realtime = helper.AblyRealtime({ + promises: true, + token: clientId2TokenDetails, + }); + + await Promise.all([ + clientId1Realtime.connection.once('connected'), + clientId2Realtime.connection.once('connected'), + ]); + + // ...then, we set up listeners that will record the state change when these Realtime instances become DISCONNECTED (we need to set up these listeners here, before performing the revocation request, else we might miss the DISCONNECTED state changes that the token revocation provokes, and end up only seeing the subsequent RSA4a2-induced FAILED state change, which due to https://github.com/ably/ably-js/issues/1409 does not expose the 40141 "token revoked" error code)... + // + // Note: + // + // We use Realtime instances for verifying the side effects of a token revocation, as opposed to, say, trying to perform a REST request, because the nature of the Ably service is that token revocation may take a small delay to become active, and so there's no guarantee that a REST request peformed immediately after a revocation request would fail. See discussion at https://ably-real-time.slack.com/archives/C030C5YLY/p1690322740850269?thread_ts=1690315022.372729&cid=C030C5YLY. + const clientId1RealtimeDisconnectedStateChangePromise = clientId1Realtime.connection.once('disconnected'); + const clientId2RealtimeDisconnectedStateChangePromise = clientId2Realtime.connection.once('disconnected'); + + // ...then, we revoke all tokens for these clientIds... + + const specifiers = [ + { type: 'clientId', value: clientId1 }, + { type: 'clientId', value: clientId2 }, + { type: 'invalidType', value: 'abc' }, // we include an invalid specifier type to provoke a non-zero failureCount + ]; + + const result = await rest.auth.revokeTokens(specifiers); + + // ...and check the response from the revocation request... + expect(result.successCount).to.equal(2); + expect(result.failureCount).to.equal(1); + expect(result.results).to.have.lengthOf(3); + + expect(result.results[0].target).to.equal(`clientId:${clientId1}`); + expect(typeof result.results[0].issuedBefore).to.equal('number'); + expect(typeof result.results[0].appliesAt).to.equal('number'); + expect('error' in result.results[0]).to.be.false; + + expect(result.results[1].target).to.equal(`clientId:${clientId2}`); + expect(typeof result.results[1].issuedBefore).to.equal('number'); + expect(typeof result.results[1].appliesAt).to.equal('number'); + expect('error' in result.results[1]).to.be.false; + + expect(result.results[2].target).to.equal('invalidType:abc'); + expect(result.results[2].error.statusCode).to.equal(400); + + // ...and then, we check that the Realtime instances transition to the DISCONNECTED state due to a "token revoked" (40141) error. + const [clientId1RealtimeDisconnectedStateChange, clientId2RealtimeDisconnectedStateChange] = + await Promise.all([ + clientId1RealtimeDisconnectedStateChangePromise, + clientId2RealtimeDisconnectedStateChangePromise, + ]); + + expect(clientId1RealtimeDisconnectedStateChange.reason.code).to.equal(40141 /* token revoked */); + expect(clientId2RealtimeDisconnectedStateChange.reason.code).to.equal(40141 /* token revoked */); + + await Promise.all( + [clientId1Realtime, clientId2Realtime].map((realtime) => { + new Promise((resolve, reject) => { + closeAndFinish((err) => { + err ? reject(err) : resolve(); + }, realtime); + }); + }) + ); + }); + + it('accepts optional issuedBefore and allowReauthMargin parameters', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[4].keyStr /* this key has revocableTokens enabled */, + }); + + const clientId = `clientId-${randomString()}`; + + const serverTimeAtStartOfTest = await rest.time(); + const issuedBefore = serverTimeAtStartOfTest - 20 * 60 * 1000; // i.e. ~20 minutes ago (arbitrarily chosen) + + const result = await rest.auth.revokeTokens([{ type: 'clientId', value: clientId }], { + issuedBefore, + allowReauthMargin: true, + }); + + expect(result.results[0].issuedBefore).to.equal(issuedBefore); + + // Verify the expected side effect of allowReauthMargin, which is to delay the revocation by 30 seconds + const serverTimeThirtySecondsAfterStartOfTest = serverTimeAtStartOfTest + 30 * 1000; + expect(result.results[0].appliesAt).to.be.greaterThan(serverTimeThirtySecondsAfterStartOfTest); + }); + }); + } + }); });