@firstperson/firstperson 2026.1.33 → 2026.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/relay-client.ts +52 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.1.34
|
|
4
|
+
|
|
5
|
+
- Improve relay connection stability for production use:
|
|
6
|
+
- Add client-side heartbeat ping every 30s to keep connections alive and detect dead connections faster
|
|
7
|
+
- Reset reconnect backoff counter after 60s of stable connection
|
|
8
|
+
- Remove max reconnect attempt limit - gateway will always keep trying to reconnect
|
|
9
|
+
|
|
3
10
|
## 2026.1.33
|
|
4
11
|
|
|
5
12
|
- Fix npm package name to `@firstperson/firstperson` (must match plugin ID)
|
package/package.json
CHANGED
package/src/relay-client.ts
CHANGED
|
@@ -142,9 +142,23 @@ export async function startRelayConnection(params: RelayConnectionParams): Promi
|
|
|
142
142
|
|
|
143
143
|
let ws: WebSocket | null = null;
|
|
144
144
|
let reconnectAttempts = 0;
|
|
145
|
-
const maxReconnectAttempts = 10;
|
|
146
145
|
const baseReconnectDelay = 1000;
|
|
147
146
|
|
|
147
|
+
// Timers for heartbeat and stability tracking
|
|
148
|
+
let pingInterval: ReturnType<typeof setInterval> | null = null;
|
|
149
|
+
let stabilityTimer: ReturnType<typeof setTimeout> | null = null;
|
|
150
|
+
|
|
151
|
+
const clearTimers = () => {
|
|
152
|
+
if (pingInterval) {
|
|
153
|
+
clearInterval(pingInterval);
|
|
154
|
+
pingInterval = null;
|
|
155
|
+
}
|
|
156
|
+
if (stabilityTimer) {
|
|
157
|
+
clearTimeout(stabilityTimer);
|
|
158
|
+
stabilityTimer = null;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
148
162
|
const connect = () => {
|
|
149
163
|
if (abortSignal.aborted) {
|
|
150
164
|
log("info", "[relay-client] connect() called but abortSignal already aborted");
|
|
@@ -156,8 +170,27 @@ export async function startRelayConnection(params: RelayConnectionParams): Promi
|
|
|
156
170
|
|
|
157
171
|
ws.on("open", () => {
|
|
158
172
|
log("info", `[relay-client] WebSocket opened, readyState: ${ws?.readyState}`);
|
|
159
|
-
reconnectAttempts = 0;
|
|
160
173
|
activeWs = ws; // Store as active connection for sending
|
|
174
|
+
|
|
175
|
+
// Clear any existing timers from previous connection attempts
|
|
176
|
+
clearTimers();
|
|
177
|
+
|
|
178
|
+
// Start client-side heartbeat: send ping every 30s to keep connection alive
|
|
179
|
+
pingInterval = setInterval(() => {
|
|
180
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
181
|
+
log("info", "[relay-client] Sending heartbeat ping");
|
|
182
|
+
ws.send(JSON.stringify({ type: "ping" }));
|
|
183
|
+
}
|
|
184
|
+
}, 30000);
|
|
185
|
+
|
|
186
|
+
// Reset reconnect counter after connection is stable for 60s
|
|
187
|
+
stabilityTimer = setTimeout(() => {
|
|
188
|
+
if (ws?.readyState === WebSocket.OPEN && reconnectAttempts > 0) {
|
|
189
|
+
log("info", `[relay-client] Connection stable for 60s, resetting reconnect counter (was ${reconnectAttempts})`);
|
|
190
|
+
reconnectAttempts = 0;
|
|
191
|
+
}
|
|
192
|
+
}, 60000);
|
|
193
|
+
|
|
161
194
|
onConnected();
|
|
162
195
|
});
|
|
163
196
|
|
|
@@ -172,6 +205,11 @@ export async function startRelayConnection(params: RelayConnectionParams): Promi
|
|
|
172
205
|
return;
|
|
173
206
|
}
|
|
174
207
|
|
|
208
|
+
if (msg.type === "pong") {
|
|
209
|
+
// Server acknowledged our heartbeat ping - connection is healthy
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
175
213
|
// Handle send acknowledgments for replies sent through this connection
|
|
176
214
|
if ((msg.type === "message_sent" || msg.type === "ack") && msg.messageId) {
|
|
177
215
|
const pending = pendingReplies.get(msg.messageId);
|
|
@@ -213,6 +251,9 @@ export async function startRelayConnection(params: RelayConnectionParams): Promi
|
|
|
213
251
|
);
|
|
214
252
|
log("info", `[relay-client] abortSignal.aborted: ${abortSignal.aborted}`);
|
|
215
253
|
|
|
254
|
+
// Clear timers on disconnect
|
|
255
|
+
clearTimers();
|
|
256
|
+
|
|
216
257
|
// Clear active connection and reject pending replies
|
|
217
258
|
if (activeWs === ws) {
|
|
218
259
|
activeWs = null;
|
|
@@ -228,19 +269,15 @@ export async function startRelayConnection(params: RelayConnectionParams): Promi
|
|
|
228
269
|
return;
|
|
229
270
|
}
|
|
230
271
|
|
|
272
|
+
// Always reconnect - never give up (exponential backoff capped at 30s)
|
|
231
273
|
reconnectAttempts++;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
onDisconnected(new Error(`Connection closed, reconnecting in ${delay}ms...`));
|
|
240
|
-
} else {
|
|
241
|
-
log("warn", "[relay-client] Max reconnection attempts reached");
|
|
242
|
-
onDisconnected(new Error("Max reconnection attempts reached"));
|
|
243
|
-
}
|
|
274
|
+
const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts - 1), 30000);
|
|
275
|
+
log(
|
|
276
|
+
"info",
|
|
277
|
+
`[relay-client] Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`
|
|
278
|
+
);
|
|
279
|
+
setTimeout(connect, delay);
|
|
280
|
+
onDisconnected(new Error(`Connection closed, reconnecting in ${delay}ms...`));
|
|
244
281
|
});
|
|
245
282
|
|
|
246
283
|
ws.on("error", (err) => {
|
|
@@ -252,6 +289,7 @@ export async function startRelayConnection(params: RelayConnectionParams): Promi
|
|
|
252
289
|
// Handle abort signal
|
|
253
290
|
abortSignal.addEventListener("abort", () => {
|
|
254
291
|
log("info", "[relay-client] Abort signal received, closing WebSocket");
|
|
292
|
+
clearTimers();
|
|
255
293
|
if (activeWs === ws) {
|
|
256
294
|
activeWs = null;
|
|
257
295
|
activeLogger = null;
|