Skip to content
Open
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: 6 additions & 6 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
*/
type DETACHED = 'detached';
/**
* The channel, having previously been `ATTACHED`, has lost continuity, usually due to the client being disconnected from Ably for longer than two minutes. It will automatically attempt to reattach as soon as connectivity is restored.
* The channel, having previously been `ATTACHED`, is waiting for the client to reconnect to Ably after an extended period of disconnection. It will automatically reattach once connectivity is restored. Message continuity may have been lost, but the authoritative source for this is the {@link ChannelStateChange.resumed | `resumed`} flag on the subsequent `attached` state change.
*/
type SUSPENDED = 'suspended';
/**
Expand Down Expand Up @@ -81,7 +81,7 @@
*/
type DETACHED = 'detached';
/**
* The channel, having previously been `ATTACHED`, has lost continuity, usually due to the client being disconnected from Ably for longer than two minutes. It will automatically attempt to reattach as soon as connectivity is restored.
* The channel, having previously been `ATTACHED`, is waiting for the client to reconnect to Ably after an extended period of disconnection. It will automatically reattach once connectivity is restored. Message continuity may have been lost, but the authoritative source for this is the {@link ChannelStateChange.resumed | `resumed`} flag on the subsequent `attached` state change.
*/
type SUSPENDED = 'suspended';
/**
Expand Down Expand Up @@ -123,11 +123,11 @@
*/
type CONNECTED = 'connected';
/**
* A temporary failure condition. No current connection exists because there is no network connectivity or no host is available. The disconnected state is entered if an established connection is dropped, or if a connection attempt was unsuccessful. In the disconnected state the library will periodically attempt to open a new connection (approximately every 15 seconds), anticipating that the connection will be re-established soon and thus connection and channel continuity will be possible. In this state, developers can continue to publish messages as they are automatically placed in a local queue, to be sent as soon as a connection is reestablished. Messages published by other clients while this client is disconnected will be delivered to it upon reconnection, so long as the connection was resumed within 2 minutes. After 2 minutes have elapsed, recovery is no longer possible and the connection will move to the `SUSPENDED` state.
* A temporary failure condition. The client has been disconnected from Ably, usually due to network connectivity. It will periodically attempt to open a new connection (about every 15 seconds). In this state, you can continue to publish messages, which will be queued to be sent once a connection is reestablished. Messages published by other clients while this client is disconnected will be delivered to it upon reconnection if the server is able to preserve continuity.
*/
type DISCONNECTED = 'disconnected';
/**
* A long term failure condition. No current connection exists because there is no network connectivity or no host is available. The suspended state is entered after a failed connection attempt if there has then been no connection for a period of two minutes. In the suspended state, the library will periodically attempt to open a new connection every 30 seconds. Developers are unable to publish messages in this state. A new connection attempt can also be triggered by an explicit call to {@link Connection.connect | `connect()`}. Once the connection has been re-established, channels will be automatically re-attached. The client has been disconnected for too long for them to resume from where they left off, so if it wants to catch up on messages published by other clients while it was disconnected, it needs to use the [History API](https://ably.com/docs/realtime/history).
* A long term failure condition. A client moves to this state once it has been in the `disconnected` state for over two minutes. Developers are unable to publish messages in this state, and any queued messages are discarded. The library will attempt to reconnect every 30 seconds; an attempt can also be triggered by calling {@link Connection.connect | `connect()`}.
*/
type SUSPENDED = 'suspended';
/**
Expand Down Expand Up @@ -173,11 +173,11 @@
*/
type CONNECTED = 'connected';
/**
* A temporary failure condition. No current connection exists because there is no network connectivity or no host is available. The disconnected state is entered if an established connection is dropped, or if a connection attempt was unsuccessful. In the disconnected state the library will periodically attempt to open a new connection (approximately every 15 seconds), anticipating that the connection will be re-established soon and thus connection and channel continuity will be possible. In this state, developers can continue to publish messages as they are automatically placed in a local queue, to be sent as soon as a connection is reestablished. Messages published by other clients while this client is disconnected will be delivered to it upon reconnection, so long as the connection was resumed within 2 minutes. After 2 minutes have elapsed, recovery is no longer possible and the connection will move to the `SUSPENDED` state.
* A temporary failure condition. The client has been disconnected from Ably, usually due to network connectivity. It will periodically attempt to open a new connection (about every 15 seconds). In this state, you can continue to publish messages, which will be queued to be sent once a connection is reestablished. Messages published by other clients while this client is disconnected will be delivered to it upon reconnection if the server is able to preserve continuity.
*/
type DISCONNECTED = 'disconnected';
/**
* A long term failure condition. No current connection exists because there is no network connectivity or no host is available. The suspended state is entered after a failed connection attempt if there has then been no connection for a period of two minutes. In the suspended state, the library will periodically attempt to open a new connection every 30 seconds. Developers are unable to publish messages in this state. A new connection attempt can also be triggered by an explicit call to {@link Connection.connect | `connect()`}. Once the connection has been re-established, channels will be automatically re-attached. The client has been disconnected for too long for them to resume from where they left off, so if it wants to catch up on messages published by other clients while it was disconnected, it needs to use the [History API](https://ably.com/docs/realtime/history).
* A long term failure condition. A client moves to this state once it has been in the `disconnected` state for over two minutes. Developers are unable to publish messages in this state, and any queued messages are discarded. The library will attempt to reconnect every 30 seconds; an attempt can also be triggered by calling {@link Connection.connect | `connect()`}.
*/
type SUSPENDED = 'suspended';
/**
Expand Down Expand Up @@ -3752,7 +3752,7 @@
*
* @experimental This is a preview feature and may change in a future non-major release.
*
* @param params - The recipient channels, the broadcast `id`, and a valid APNs Live Activity start payload.

Check warning on line 3755 in ably.d.ts

View workflow job for this annotation

GitHub Actions / lint

Expected no lines between tags
* @returns A promise which resolves upon success of the operation and rejects with an {@link ErrorInfo} object upon its failure.
*/
start(params: PushLiveActivityStartParams): Promise<void>;
Expand All @@ -3775,7 +3775,7 @@
*/
end(params: PushLiveActivityEndParams): Promise<void>;
}

Check warning on line 3778 in ably.d.ts

View workflow job for this annotation

GitHub Actions / lint

Expected no lines between tags
/**
* Parameters for {@link PushLiveActivity.start}.
*/
Expand All @@ -3784,7 +3784,7 @@
* The targeted recipients of the push-to-start notification.
*/
recipient:
| {

Check warning on line 3787 in ably.d.ts

View workflow job for this annotation

GitHub Actions / lint

Expected no lines between tags
/**
* One or more Ably channel names. Devices subscribed to any of these channels will receive the push-to-start notification. Provide either `channels` or `deviceId`.
*/
Expand All @@ -3793,7 +3793,7 @@
| {
/**
* Restrict the push-to-start notification to a single device. Provide either `channels` or `deviceId`.
*/

Check warning on line 3796 in ably.d.ts

View workflow job for this annotation

GitHub Actions / lint

Expected no lines between tags
deviceId: string;
};
/**
Expand Down
18 changes: 5 additions & 13 deletions src/common/lib/client/realtimechannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ class RealtimeChannel extends EventEmitter {
errorReason: ErrorInfo | null;
_mode = 0;
_silentSubscribeWarned = false;
_attachResume: boolean;
_decodingContext: EncodingDecodingContext;
_lastPayload: {
messageId?: string | null;
Expand Down Expand Up @@ -149,7 +148,6 @@ class RealtimeChannel extends EventEmitter {
};
this.setOptions(options);
this.errorReason = null;
this._attachResume = false;
this._decodingContext = {
channelOptions: this.channelOptions,
plugins: client.options.plugins || {},
Expand Down Expand Up @@ -426,9 +424,6 @@ class RealtimeChannel extends EventEmitter {
if (this.channelOptions.modes) {
attachMsg.encodeModesToFlags(Utils.allToUpperCase(this.channelOptions.modes) as API.ChannelMode[]);
}
if (this._attachResume) {
attachMsg.setFlag('ATTACH_RESUME');
}
if (this._lastPayload.decodeFailureRecoveryInProgress) {
attachMsg.channelSerial = this._lastPayload.protocolMessageChannelSerial;
}
Expand Down Expand Up @@ -914,8 +909,11 @@ class RealtimeChannel extends EventEmitter {
);
this.clearStateTimer();

// RTP5a1
if (['detached', 'suspended', 'failed'].includes(state)) {
// RTL15b2: clear channelSerial only when entering DETACHED or FAILED.
// Unlike previous spec versions it is retained through SUSPENDED, so that
// the channelSerial is available on the subsequent ATTACH (RTL4c1) for the
// server to decide whether continuity can be preserved.
if (['detached', 'failed'].includes(state)) {
this.properties.channelSerial = null;
}

Expand Down Expand Up @@ -954,12 +952,6 @@ class RealtimeChannel extends EventEmitter {
this.onAttached();
}

if (state === 'attached') {
this._attachResume = true;
} else if (state === 'detaching' || state === 'failed') {
this._attachResume = false;
}

this.state = state;
this.emit(state, change);
}
Expand Down
30 changes: 5 additions & 25 deletions src/common/lib/transport/connectionmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ class ConnectionManager extends EventEmitter {
proposedTransport?: Transport;
host: string | null;
lastAutoReconnectAttempt: number | null;
lastActivity: number | null;
forceFallbackHost: boolean;
transitionTimer?: number | NodeJS.Timeout | null;
suspendTimer?: number | NodeJS.Timeout | null;
Expand Down Expand Up @@ -255,7 +254,6 @@ class ConnectionManager extends EventEmitter {
this.activeProtocol = null;
this.host = null;
this.lastAutoReconnectAttempt = null;
this.lastActivity = null;
this.forceFallbackHost = false;
this.connectCounter = 0;
this.wsCheckResult = null;
Expand Down Expand Up @@ -865,24 +863,6 @@ class ConnectionManager extends EventEmitter {
});
}

checkConnectionStateFreshness(): void {
if (!this.lastActivity || !this.connectionId) {
return;
}

const sinceLast = Platform.Config.now() - this.lastActivity;
if (sinceLast > this.connectionStateTtl + (this.maxIdleInterval as number)) {
Logger.logAction(
this.logger,
Logger.LOG_MINOR,
'ConnectionManager.checkConnectionStateFreshness()',
'Last known activity from realtime was ' + sinceLast + 'ms ago; discarding connection state',
);
this.clearConnection();
this.states.connecting.failState = 'suspended';
}
}

/**
* Called when the connectionmanager wants to persist transport
* state for later recovery. Only applicable in the browser context.
Expand Down Expand Up @@ -960,10 +940,11 @@ class ConnectionManager extends EventEmitter {
// TODO remove this type assertion after fixing https://github.com/ably/ably-js/issues/1405
this.realtime.connection.errorReason = stateChange.reason as ErrorInfo;
}
if (newState.terminal || newState.state === 'suspended') {
/* suspended is nonterminal, but once in the suspended state, realtime
* will have discarded our connection state, so futher connection
* attempts should start from scratch */
if (newState.terminal) {
// RTN8d, RTN9d: connectionId and connectionKey are only cleared in the
// terminal states. In particular, they are retained in the suspended
// state: since RTN14h/spec-6.1.0 the client always attempts to resume on
// reconnecting and lets the server decide whether continuity is possible.
this.clearConnection();
}
this.emit('connectionstate', stateChange);
Expand Down Expand Up @@ -1333,7 +1314,6 @@ class ConnectionManager extends EventEmitter {
const connectCount = ++this.connectCounter;

const connect = () => {
this.checkConnectionStateFreshness();
this.getTransportParams((transportParams: TransportParams) => {
if (transportParams.mode === 'recover' && transportParams.options.recover) {
const recoveryContext = decodeRecoveryKey(transportParams.options.recover);
Expand Down
2 changes: 1 addition & 1 deletion src/common/lib/transport/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ abstract class Transport extends EventEmitter {
if (!this.maxIdleInterval) {
return;
}
this.lastActivity = this.connectionManager.lastActivity = Platform.Config.now();
this.lastActivity = Platform.Config.now();
this.setIdleTimer(this.maxIdleInterval + 100);
}

Expand Down
1 change: 0 additions & 1 deletion test/common/modules/private_api_recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ define(['test/support/output_directory_paths'], function (outputDirectoryPaths)
'write.connectionManager.connectionDetails.maxMessageSize',
'write.connectionManager.connectionId',
'write.connectionManager.connectionKey',
'write.connectionManager.lastActivity',
'write.connectionManager.msgSerial',
'write.connectionManager.domains',
'write.realtime.options.echoMessages',
Expand Down
19 changes: 16 additions & 3 deletions test/common/modules/shared_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,18 +261,31 @@ define([
realtime.connection.connectionManager.requestState({ state: 'disconnected' });
}

becomeSuspended(realtime, cb) {
/**
* Force the connection into the suspended state. Since spec version 6.1.0
* the client retains its connection state through suspended and will attempt
* to resume on the next connect (RTN14h); pass clearConnectionState: true to
* additionally discard the connection state, modelling the case where the
* connection can no longer be resumed (e.g. the device was asleep for longer
* than the server preserves state), so the next connect makes a fresh
* connection with a new connectionId.
*/
becomeSuspended(realtime, cb, clearConnectionState) {
const helper = this.addingHelperFunction('becomeSuspended');
return helper._becomeSuspended(realtime, cb);
return helper._becomeSuspended(realtime, cb, clearConnectionState);
}

_becomeSuspended(realtime, cb) {
_becomeSuspended(realtime, cb, clearConnectionState) {
this.recordPrivateApi('call.connectionManager.disconnectAllTransports');
realtime.connection.connectionManager.disconnectAllTransports();
const self = this;
realtime.connection.once('disconnected', function () {
self.recordPrivateApi('call.connectionManager.notifyState');
realtime.connection.connectionManager.notifyState({ state: 'suspended' });
if (clearConnectionState) {
self.recordPrivateApi('call.connectionManager.clearConnection');
realtime.connection.connectionManager.clearConnection();
}
Comment on lines 283 to +288

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)test/common/modules/shared_helper\.js$|connectionManager|connect|clearConnection|notifyState'

echo "== file outline =="
ast-grep outline test/common/modules/shared_helper.js --view compact || true

echo "== relevant lines =="
nl -ba test/common/modules/shared_helper.js | sed -n '240,330p'

echo "== search connectionManager definitions/usages =="
rg -n "connectionManager|clearConnection|notifyState|connectionState|suspended|clearConnectionState" test/common/modules/shared_helper.js -C 3

Repository: ably/ably-js

Length of output: 1980


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== shared_helper relevant section =="
awk 'NR>=250 && NR<=315 { printf "%5d\t%s\n", NR, $0 }' test/common/modules/shared_helper.js

echo "== connectionmanager outline/sections =="
wc -l src/common/lib/transport/connectionmanager.ts
grep -n "class ConnectionManager\|clearConnection\|notifyState\|emit\|eventEmitter\|stateChange\|tryTransition" src/common/lib/transport/connectionmanager.ts | head -200

echo "== implementation sections =="
awk 'NR>=1 && NR<=220 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

Repository: ably/ably-js

Length of output: 15400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== clearConnection implementation =="
awk 'NR>=820 && NR<=905 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

echo "== enactStateChange implementation =="
awk 'NR>=920 && NR<=970 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

echo "== notifyState implementation =="
awk 'NR>=1136 && NR<=1260 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

echo "== relevant callback listeners =="
awk 'NR>=1280 && NR<=1365 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

echo "== call sites around becomeSuspended tests usage =="
rg -n "becomeSuspended|clearConnectionState|notifyState\\(\\{ state: 'suspended'|clearConnection\\(" test src/common -C 2

Repository: ably/ably-js

Length of output: 22170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== resume tests around becomeSuspended with clearConnectionState =="
awk 'NR>=470 && NR<=580 { printf "%5d\t%s\n", NR, $0 }' test/realtime/resume.test.js

echo "== static behavioral probe for _becomeSuspended ordering =="
node - <<'JS'
const events = [];
const realtime = {
  connection: {
    once(event, listener) {
      events.push({ event, type: 'listener registered', eventListeners: realtime.connection._listeners?.[event] || [] });
      realtime.connection._listeners = realtime.connection._listeners || {};
      (realtime.connection._listeners[event] = realtime.connection._listeners[event] || []).push(listener);
    },
    emit(event, ...args) {
      events.push({ event, type: 'emit', arg: args[0], listeners: realtime.connection._listeners?.[event]?.length || 0 });
      for (const listener of realtime.connection._listeners?.[event] || []) listener(...args);
    },
    _listeners: {},
  },
  connectionManager: {
    clearConnectionState: false,
    disconnectAllTransports() {},
    notifyState(ind) {
      if (ind.state === 'suspended') {
        this.emit('disconnected');
        this.emit='suspended;';
        this.inSuspendedCallback = true;
      }
    },
    emit(...args){}
  },
};
let cbCalledWithIdKey;
function clearConnectionStateWithSameValues() {
  realtime.becameSuspended = true;
}
try {
  const helperPrototype = { addingHelperFunction: () => ({ disconnectAllTransports() {}, _becomeSuspended(realtime, cb, clearConnectionState) {
    realtime.connection.connectionManager.disconnectAllTransports();
    const self = this;
    self.recordPrivateApi = () => {};
    realtime.connection.once('disconnected', function () {
      self.recordPrivateApi('call.connectionManager.notifyState');
      realtime.connection.connectionManager.notifyState({ state: 'suspended' });
      if (clearConnectionState) {
        self.recordPrivateApi('call.connectionManager.clearConnection');
        realtime.connection.connectionManager.clearConnection();
      }
    });
    if (cb) {
      realtime.connection.once('suspended', function () {
        cb();
      });
    }
  }}); }
}
JS

echo "== behavioral probe from source text =="
python3 - <<'PY'
from pathlib import Path
source = Path("src/common/lib/transport/connectionmanager.ts").read_text()
notify = source[source.index("  notifyState(indicated: ConnectionState): void {"):source.index("  requestState(request: any): void {")]
print("notifyState calls ConnectionManager.emitter emit:", "this.emit('connectionstate'" in notify)
print("clearConnection only invoked for terminal states in enactStateChange:", "this.clearConnection()" in source[source.index("  enactStateChange"):source.index("  startTransitionTimer")])
print("suspended state non-terminal:", "terminal: false,\n      state: 'suspended'," in source)
print("suspended failState is suspended:", source[source.index("suspended"):].split("\n")[9])
PY

Repository: ably/ably-js

Length of output: 5128


Clear state before notifying suspended.

For clearConnectionState: true, notifyState({ state: 'suspended' }) enacts suspended, emits connectionstate synchronously, then clearConnection() runs in _becomeSuspended, so pending reconnect callbacks can still use the old recovery credentials. Move the clearing into ConnectionManager.enactStateChange() with non-terminal handling before emitting connectionstate, or clear in becomeSuspended before notifying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/common/modules/shared_helper.js` around lines 283 - 288, Ensure the
clearConnectionState path clears the connection before the suspended state
notification is emitted, so reconnect callbacks cannot use stale recovery
credentials. Update the relevant ConnectionManager state-transition logic, such
as enactStateChange or _becomeSuspended, and preserve the existing conditional
behavior and private API recording.

});
if (cb) {
realtime.connection.once('suspended', function () {
Expand Down
95 changes: 0 additions & 95 deletions test/realtime/connection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,101 +354,6 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, Helper, async
helper.monitorConnection(done, realtime);
});

/**
* When a connection becomes stale (checkConnectionStateFreshness detects
* lastActivity exceeds connectionStateTtl), clearConnection() is called.
* Any queued messages with sendAttempted=true must have sendAttempted reset
* so they get new msgSerial values on the new connection.
*
* Scenario: Client sends a message, transport disconnects before ACK,
* connection becomes stale while in disconnected state.
* Without the fix, old messages keep their serials while new messages start
* from 0, causing non-monotonic msgSerial values.
*
* @spec RTN19a2
*/
it('stale_connection_resets_msgSerial_and_sendAttempted', function (done) {
var helper = this.test.helper,
realtime = helper.AblyRealtime({ transports: [helper.bestTransport] }),
channel = realtime.channels.get('stale_connection_test'),
connectionManager = realtime.connection.connectionManager;

realtime.connection.once('connected', function () {
helper.recordPrivateApi('read.connectionManager.activeProtocol.transport');
var transport = connectionManager.activeProtocol.transport;

Helper.whenPromiseSettles(channel.attach(), function (err) {
if (err) {
helper.closeAndFinish(done, realtime, err);
return;
}

// Intercept transport.send to prevent ACKs
helper.recordPrivateApi('replace.transport.send');
transport.send = function () {
// Don't forward - message stays un-ACKed in pending queue
};

// Publish a message (will not be ACKed)
channel.publish('test', 'data');

setTimeout(function () {
try {
helper.recordPrivateApi('read.connectionManager.msgSerial');
expect(connectionManager.msgSerial).to.equal(1, 'msgSerial should be 1 after publish');

// Get the pending message details before disconnection
helper.recordPrivateApi('read.connectionManager.activeProtocol');
var pendingMessages = connectionManager.activeProtocol.getPendingMessages();
expect(pendingMessages.length).to.equal(1, 'Should have 1 pending message');
expect(pendingMessages[0].sendAttempted).to.equal(true, 'Message should have sendAttempted=true');

// Disconnect transport - this requeues pending messages
helper.recordPrivateApi('call.connectionManager.disconnectAllTransports');
connectionManager.disconnectAllTransports();

realtime.connection.once('disconnected', function () {
// Verify message was requeued
helper.recordPrivateApi('read.connectionManager.queuedMessages');
expect(connectionManager.queuedMessages.count()).to.equal(1, 'Should have 1 queued message');

// Simulate stale connection by setting lastActivity to the past.
// This will cause checkConnectionStateFreshness to call clearConnection
// when we attempt to reconnect.
helper.recordPrivateApi('write.connectionManager.lastActivity');
connectionManager.lastActivity = Date.now() - connectionManager.connectionStateTtl - 100000;

// When we reconnect, checkConnectionStateFreshness will detect stale
// connection and call clearConnection before the transport is established
helper.recordPrivateApi('listen.connectionManager.transport.pending');
connectionManager.once('transport.pending', function () {
try {
// Verify msgSerial was reset by checkConnectionStateFreshness → clearConnection
helper.recordPrivateApi('read.connectionManager.msgSerial');
expect(connectionManager.msgSerial).to.equal(0, 'msgSerial should be 0 after stale connection');

// Verify sendAttempted was reset on the queued message
expect(pendingMessages[0].sendAttempted).to.equal(
false,
'sendAttempted should be reset so message gets new msgSerial on reconnect',
);

helper.closeAndFinish(done, realtime);
} catch (err) {
helper.closeAndFinish(done, realtime, err);
}
});

realtime.connection.connect();
});
} catch (err) {
helper.closeAndFinish(done, realtime, err);
}
}, 0);
});
});
});

/**
* @spec RTN26a
* @spec RTN26b
Expand Down
Loading
Loading