Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support optional multiple recovery scopes #1762

Merged
merged 1 commit into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,18 @@
*/
recover?: string | recoverConnectionCallback;

/**
* If specified, the SDK's internal persistence mechanism for storing the recovery key
* over page loads (see the `recover` client option) will store the recovery key under
* this identifier (in sessionstorage), so only another library instance which specifies
* the same recoveryKeyStorageName will attempt to recover from it. This is useful if you have
* multiple ably-js instances sharing a given origin (the origin being the scope of
* sessionstorage), as otherwise the multiple instances will overwrite each other's
* recovery keys, and after a reload they will all try and recover the same connection,
* which is not permitted and will cause broken behaviour.
*/
recoveryKeyStorageName?: string;

/**
* When `false`, the client will use an insecure connection. The default is `true`, meaning a TLS connection will be used to connect to Ably.
*
Expand Down Expand Up @@ -2179,7 +2191,7 @@
* and {@link ChannelOptions}, or returns the existing channel object.
*
* @experimental This is a preview feature and may change in a future non-major release.
* This experimental method allows you to create custom realtime data feeds by selectively subscribing

Check warning on line 2194 in ably.d.ts

View workflow job for this annotation

GitHub Actions / lint

Expected no lines between tags
* to receive only part of the data from the channel.
* See the [announcement post](https://pages.ably.com/subscription-filters-preview) for more information.
*
Expand Down
36 changes: 21 additions & 15 deletions src/common/lib/transport/connectionmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,6 @@ const haveSessionStorage = () => typeof Platform.WebStorage !== 'undefined' && P
const noop = function () {};
const transportPreferenceName = 'ably-transport-preference';

const sessionRecoveryName = 'ably-connection-recovery';
function getSessionRecoverData() {
return haveSessionStorage() && Platform.WebStorage?.getSession?.(sessionRecoveryName);
}
function setSessionRecoverData(value: any) {
return haveSessionStorage() && Platform.WebStorage?.setSession?.(sessionRecoveryName, value);
}
function clearSessionRecoverData() {
return haveSessionStorage() && Platform.WebStorage?.removeSession?.(sessionRecoveryName);
}

function bundleWith(dest: ProtocolMessage, src: ProtocolMessage, maxSize: number) {
let action;
if (dest.channel !== src.channel) {
Expand Down Expand Up @@ -431,12 +420,15 @@ class ConnectionManager extends EventEmitter {
}

const recoverFn = this.options.recover,
lastSessionData = getSessionRecoverData();
lastSessionData = this.getSessionRecoverData(),
sessionRecoveryName = this.sessionRecoveryName();
if (lastSessionData && typeof recoverFn === 'function') {
Logger.logAction(
Logger.LOG_MINOR,
'ConnectionManager.getTransportParams()',
'Calling clientOptions-provided recover function with last session data',
'Calling clientOptions-provided recover function with last session data (recovery scope: ' +
sessionRecoveryName +
')',
);
recoverFn(lastSessionData, (shouldRecover?: boolean) => {
if (shouldRecover) {
Expand Down Expand Up @@ -893,7 +885,7 @@ class ConnectionManager extends EventEmitter {
if (haveSessionStorage()) {
const recoveryKey = this.createRecoveryKey();
if (recoveryKey) {
setSessionRecoverData({
this.setSessionRecoverData({
recoveryKey: recoveryKey,
disconnectedAt: Date.now(),
location: globalObject.location,
Expand All @@ -908,7 +900,7 @@ class ConnectionManager extends EventEmitter {
* state for later recovery. Only applicable in the browser context.
*/
unpersistConnection(): void {
clearSessionRecoverData();
this.clearSessionRecoverData();
}

/*********************
Expand Down Expand Up @@ -1960,6 +1952,20 @@ class ConnectionManager extends EventEmitter {
};
});
}

sessionRecoveryName() {
return this.options.recoveryKeyStorageName || 'ably-connection-recovery';
}

getSessionRecoverData() {
return haveSessionStorage() && Platform.WebStorage?.getSession?.(this.sessionRecoveryName());
}
setSessionRecoverData(value: any) {
return haveSessionStorage() && Platform.WebStorage?.setSession?.(this.sessionRecoveryName(), value);
}
clearSessionRecoverData() {
return haveSessionStorage() && Platform.WebStorage?.removeSession?.(this.sessionRecoveryName());
}
}

export default ConnectionManager;
Expand Down
32 changes: 31 additions & 1 deletion test/browser/connection.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

define(['shared_helper', 'chai'], function (helper, chai) {
var expect = chai.expect;
var { expect, assert } = chai;
var closeAndFinish = helper.closeAndFinish;
var monitorConnection = helper.monitorConnection;
var simulateDroppedConnection = helper.simulateDroppedConnection;
Expand Down Expand Up @@ -350,6 +350,36 @@ define(['shared_helper', 'chai'], function (helper, chai) {
});
});

it('page_refresh_with_multiple_recovery_scopes', async () => {
const realtimeOpts = { recover: (_, cb) => cb(true) },
opts1 = Object.assign({ recoveryKeyStorageName: 'recovery-1' }, realtimeOpts),
opts2 = Object.assign({ recoveryKeyStorageName: 'recovery-2' }, realtimeOpts),
realtime1 = helper.AblyRealtime(opts1),
realtime2 = helper.AblyRealtime(opts2),
refreshEvent = new Event('beforeunload', { bubbles: true });

await Promise.all([realtime1.connection.once('connected'), realtime2.connection.once('connected')]);
const connId1 = realtime1.connection.id;
const connId2 = realtime2.connection.id;

document.dispatchEvent(refreshEvent);

simulateDroppedConnection(realtime1);
simulateDroppedConnection(realtime2);

await new Promise((res) => setTimeout(res, 1000));

const newRealtime1 = helper.AblyRealtime(opts1);
const newRealtime2 = helper.AblyRealtime(opts2);
await Promise.all([newRealtime1.connection.once('connected'), newRealtime2.connection.once('connected')]);
assert.equal(connId1, newRealtime1.connection.id);
assert.equal(connId2, newRealtime2.connection.id);

await Promise.all(
[realtime1, realtime2, newRealtime1, newRealtime2].map((rt) => helper.closeAndFinishAsync(rt)),
);
});

it('persist_preferred_transport', function (done) {
var realtime = helper.AblyRealtime();

Expand Down
Loading