Skip to content

Commit 415c3d6

Browse files
authored
fix: handle transient ShareDB errors and inactive-tab reconnection (#1965)
- fix event name mismatch: 'realtime:asset:error' → 'realtime:assets:error' - add 3s grace period before showing fatal error overlay - detect zombie sockets when tab returns from background - pause keep-alive pings while tab is hidden - always emit asset errors instead of silently swallowing them - log swallowed ShareDB errors for debugging
1 parent f838ed2 commit 415c3d6

File tree

5 files changed

+74
-23
lines changed

5 files changed

+74
-23
lines changed

src/editor-api/realtime/asset.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,8 @@ class RealtimeAsset extends Events {
107107
}
108108

109109
_onError(err: string) {
110-
if (this._connection.connected) {
111-
console.log(err);
112-
} else {
113-
this._realtime.emit('error:asset', err, this._uniqueId);
114-
}
110+
console.warn('asset error:', this._uniqueId, err);
111+
this._realtime.emit('error:asset', err, this._uniqueId);
115112
}
116113

117114
_onLoad() {

src/editor-api/realtime/connection.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,54 @@ class RealtimeConnection extends Events {
5555
this._realtime = realtime;
5656
this._domEvtVisibilityChange = () => {
5757
if (document.hidden) {
58+
// pause keep-alive while tab is hidden
59+
if (this._alive) {
60+
clearInterval(this._alive);
61+
this._alive = null;
62+
}
63+
return;
64+
}
65+
66+
// tab became visible — check if socket is still alive
67+
if (this._state === 'connected' && this._socket?.readyState !== WebSocket.OPEN) {
68+
// socket died while tab was hidden but close event hasn't fired yet
69+
this._socket?.close();
5870
return;
5971
}
72+
73+
// restart keep-alive if connected
74+
if (this._state === 'connected' && !this._alive) {
75+
this._startKeepAlive();
76+
}
77+
6078
if (this._state === 'disconnected' && this._url) {
6179
this.connect(this._url);
6280
}
6381
};
6482
}
6583

84+
private _startKeepAlive() {
85+
if (this._alive) {
86+
clearInterval(this._alive);
87+
}
88+
this._alive = setInterval(() => {
89+
if (!this._sharedb) {
90+
return;
91+
}
92+
if (this._sharedb.state === 'connected') {
93+
this._sharedb.ping();
94+
}
95+
}, 1000);
96+
}
97+
6698
private _onauth(socket: WebSocket) {
6799
if (this._sharedb) {
68100
this._sharedb.bindToSocket(socket);
69101
} else {
70102
this._sharedb = new share.Connection(socket) as ShareDb;
71103
this._sharedb.on('error', (err) => {
72104
if (this._sharedb?.state === 'connected') {
105+
console.warn('sharedb error while connected:', err);
73106
return;
74107
}
75108
this._realtime.emit('error', err);
@@ -79,18 +112,7 @@ class RealtimeConnection extends Events {
79112
});
80113
}
81114

82-
// reset keep alive
83-
if (this._alive) {
84-
clearInterval(this._alive);
85-
}
86-
this._alive = setInterval(() => {
87-
if (!this._sharedb) {
88-
return;
89-
}
90-
if (this._sharedb.state === 'connected') {
91-
this._sharedb.ping();
92-
}
93-
}, 1000);
115+
this._startKeepAlive();
94116

95117
// intercept messages
96118
const onmessage = socket.onmessage;

src/editor/alerts/alert-connection.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ editor.once('load', () => {
2828
overlay.domContent.appendChild(content);
2929

3030
editor.on('realtime:connected', () => {
31+
clearErrorGrace();
32+
3133
if (viewportError) {
3234
return;
3335
}
@@ -67,6 +69,8 @@ editor.once('load', () => {
6769
});
6870

6971
editor.on('realtime:connecting', (attempt) => {
72+
clearErrorGrace();
73+
7074
if (viewportError) {
7175
return;
7276
}
@@ -81,11 +85,37 @@ editor.once('load', () => {
8185
content.innerHTML = 'Cannot connect to the server. Please try again later.';
8286
});
8387

88+
const ERROR_GRACE_MS = 3000;
89+
let errorGraceTimeout: ReturnType<typeof setTimeout> | null = null;
90+
91+
const clearErrorGrace = () => {
92+
if (errorGraceTimeout) {
93+
clearTimeout(errorGraceTimeout);
94+
errorGraceTimeout = null;
95+
}
96+
};
97+
8498
const onError = function (err: unknown) {
85-
console.log(err);
86-
console.trace();
87-
content.innerHTML = 'Error while saving changes. Please refresh the editor.';
88-
overlay.hidden = false;
99+
console.warn('realtime error:', err);
100+
101+
// debounce — only one grace period at a time
102+
if (errorGraceTimeout) {
103+
return;
104+
}
105+
106+
errorGraceTimeout = setTimeout(() => {
107+
errorGraceTimeout = null;
108+
109+
// if connection recovered during grace period, skip the fatal overlay
110+
const conn = editor.call('realtime:connection');
111+
if (conn?.state === 'connected') {
112+
console.warn('realtime error suppressed — connection recovered during grace period');
113+
return;
114+
}
115+
116+
content.innerHTML = 'Error while saving changes. Please refresh the editor.';
117+
overlay.hidden = false;
118+
}, ERROR_GRACE_MS);
89119
};
90120

91121
editor.on('viewport:error', (err) => {
@@ -122,6 +152,8 @@ editor.once('load', () => {
122152
});
123153

124154
editor.on('scene:unload', () => {
155+
clearErrorGrace();
156+
125157
if (viewportError) {
126158
return;
127159
}

src/editor/realtime/realtime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ editor.once('start', () => {
3333
});
3434

3535
realtime.on('error:asset', (err: unknown) => {
36-
editor.emit('realtime:asset:error', err);
36+
editor.emit('realtime:assets:error', err);
3737
});
3838

3939
realtime.on('disconnect', (reason: string) => {

src/launch/assets/assets-sync.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ editor.once('load', () => {
4949
// error
5050
doc.on('error', (err: unknown) => {
5151
if (connection.state === 'connected') {
52-
console.log(err);
52+
console.warn('asset doc error while connected:', err);
5353
return;
5454
}
5555

0 commit comments

Comments
 (0)