[Web runtime] Add reconnect-on-close to the Browser SDK - #1569
Open
minggangw wants to merge 3 commits into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances the rclnodejs/web Browser SDK’s resilience by adding WebSocket reconnect-on-close with backoff and HTTP retry-with-backoff behavior, plus corresponding public typings and tests.
Changes:
- Implement WebSocket reconnect-on-close with exponential backoff + jitter, subscription replay, and lifecycle events (
disconnected/reconnecting/reconnected). - Add HTTP retry-with-backoff for
call()/publish()on network errors and 5xx responses viahttpRetries. - Add a new test suite covering WS reconnect behavior and HTTP retries.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| web/index.d.ts | Updates public types/docs to expose reconnect, httpRetries, and lifecycle event on/off APIs. |
| web/client.js | Implements reconnect/backoff logic (WS + HTTP), lifecycle event listeners, and wires new options into transports. |
| test/test-web-reconnect.js | Adds coverage for WS reconnect semantics and HTTP retry behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
131
to
135
| const onError = (err) => { | ||
| if (this._pending.size === 0 && !this._closed) { | ||
| // Ignore post-open: 'close' always follows and _handleClose() owns failing pending requests. | ||
| if (!settled) { | ||
| settled = true; | ||
| reject(err && err.error ? err.error : err); |
Comment on lines
+163
to
+178
| _handleClose() { | ||
| this._failAll(_connectionLostError()); | ||
| if (this._userClosed) { | ||
| this._closed = true; | ||
| this._subs.clear(); | ||
| return; | ||
| } | ||
| this._onEvent('disconnected', undefined); | ||
| if (!this._reconnect) { | ||
| this._closed = true; | ||
| this._subs.clear(); | ||
| return; | ||
| } | ||
| this._reconnecting = true; | ||
| this._scheduleReconnect(); | ||
| } |
Comment on lines
+517
to
+520
| /** | ||
| * Subscribe to an SDK lifecycle event: 'disconnected', 'reconnecting' | ||
| * ({attempt, delay}), or 'reconnected'. Only fires with {reconnect: true}. | ||
| */ |
Comment on lines
+244
to
+247
| if (!this._ws || this._closed) return; | ||
| const ws = this._ws; | ||
| // Already closed (e.g. mid-backoff) means 'close' won't fire again. | ||
| if (ws.readyState === 3) return; |
minggangw
force-pushed
the
feat/web-sdk-reconnect
branch
from
August 12, 2026 03:15
1c11e47 to
4662989
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (3)
web/client.js:147
- When
close()aborts a reconnect attempt before its socket opens,openedis false, so this handler only rejects the open promise and never reaches_handleClose()/_finalizeClosed(). The retry stops because_userClosedis set, but_closedremains false and_reconnectingremains true, causing later operations to reportconnection_lostindefinitely afterclose()has resolved. Finalize the link when this pre-open close was user initiated.
const onClose = () => {
if (opened) {
this._handleClose();
return;
}
if (!settled) {
settled = true;
reject(new Error('connection closed before it was established'));
}
web/client.js:505
- The PR promises configurable HTTP retries via
httpRetries, but the newly added option plumbing only forwardsreconnect;_HttpLinkis still constructed without options and performs exactly onefetch()for network errors and 5xx responses.ConnectOptionsalso exposes nohttpRetries, and the added suite contains only WebSocket tests. Implement and type the advertised HTTP retry option (with its retry tests), or remove HTTP retry from this PR's stated scope.
this._wsOptions = {
reconnect: !!options.reconnect,
onEvent: (name, detail) => this._emit(name, detail),
};
web/client.js:180
disconnectedlisteners run synchronously before the link enters either_reconnectingor_closed. If a listener immediately starts a request, both guards still pass and_sendRawtargets the already-closed socket, producing a transport-specific send error instead of the expectedconnection_lost/terminal-close result. Transition the state and fail in-flight work before emitting the event, while keepingdisconnectedbefore_scheduleReconnect()so lifecycle ordering remains intact.
this._onEvent('disconnected', undefined);
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
RosClient's previously-reservedreconnectoption (web/client.js,_WsLink).{reconnect: true}reopens the WS link with exponential backoff (500ms\u201330s, half-jitter) after a drop that follows a successful connect. The first connect attempt always rejects once rather than retrying forever, even when a connection error is followed by a close event.subId, so existingSubscriptionhandles keep working transparently.RosClient.on/offlifecycle events:'disconnected'(any unexpected drop),'reconnecting'({attempt, delay}),'reconnected'.code: 'connection_lost', with wording that reflects whether reconnecting is actually happening.ros.close()always wins over reconnect and finalizes cleanly, including when called mid-backoff.web/index.d.ts:reconnectdocumented as implemented; newRosClientEventMap/ReconnectingDetail;on/offdeclared onRosClient.test/test-web-reconnect.js(new, 8 tests): reconnect + subscription replay, in-flight/new-request rejection during a drop, default behavior withreconnectunset, first-connect-rejects-once, close() during backoff, close() never triggers a reconnect.Fix: #1510