@tthr/vue 0.0.26 → 0.0.28
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/nuxt/runtime/composables.ts +87 -3
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* WebSocket subscriptions run client-side for realtime updates.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { ref, onMounted, onUnmounted,
|
|
9
|
+
import { ref, onMounted, onUnmounted, type Ref } from 'vue';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Query state returned by useQuery
|
|
@@ -195,10 +195,56 @@ export function useTetherSubscription(
|
|
|
195
195
|
if (import.meta.client) {
|
|
196
196
|
let ws: WebSocket | null = null;
|
|
197
197
|
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
198
|
+
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
199
|
+
let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
200
|
+
let awaitingPong = false;
|
|
201
|
+
let isConnecting = false;
|
|
202
|
+
let isMounted = false;
|
|
203
|
+
|
|
204
|
+
// Heartbeat configuration
|
|
205
|
+
const HEARTBEAT_INTERVAL = 25000; // 25 seconds (well under server's 90s timeout)
|
|
206
|
+
const HEARTBEAT_TIMEOUT = 15000; // 15 seconds to receive pong (generous for slow networks)
|
|
207
|
+
|
|
198
208
|
// Generate a unique subscription ID
|
|
199
209
|
const subscriptionId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
200
210
|
|
|
211
|
+
const startHeartbeat = () => {
|
|
212
|
+
stopHeartbeat();
|
|
213
|
+
heartbeatInterval = setInterval(() => {
|
|
214
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
215
|
+
awaitingPong = true;
|
|
216
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
217
|
+
|
|
218
|
+
heartbeatTimeout = setTimeout(() => {
|
|
219
|
+
if (awaitingPong && isMounted) {
|
|
220
|
+
console.warn('[Tether] Heartbeat timeout - forcing reconnect');
|
|
221
|
+
ws?.close();
|
|
222
|
+
}
|
|
223
|
+
}, HEARTBEAT_TIMEOUT);
|
|
224
|
+
}
|
|
225
|
+
}, HEARTBEAT_INTERVAL);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const stopHeartbeat = () => {
|
|
229
|
+
if (heartbeatInterval) {
|
|
230
|
+
clearInterval(heartbeatInterval);
|
|
231
|
+
heartbeatInterval = null;
|
|
232
|
+
}
|
|
233
|
+
if (heartbeatTimeout) {
|
|
234
|
+
clearTimeout(heartbeatTimeout);
|
|
235
|
+
heartbeatTimeout = null;
|
|
236
|
+
}
|
|
237
|
+
awaitingPong = false;
|
|
238
|
+
};
|
|
239
|
+
|
|
201
240
|
const connect = () => {
|
|
241
|
+
// Don't connect if unmounted or already connecting/connected
|
|
242
|
+
if (!isMounted) return;
|
|
243
|
+
if (isConnecting) return;
|
|
244
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
202
248
|
// Get config from window (set by plugin)
|
|
203
249
|
const config = (window as any).__TETHER_CONFIG__;
|
|
204
250
|
if (!config?.wsUrl || !config?.projectId) {
|
|
@@ -206,10 +252,12 @@ export function useTetherSubscription(
|
|
|
206
252
|
return;
|
|
207
253
|
}
|
|
208
254
|
|
|
255
|
+
isConnecting = true;
|
|
209
256
|
const wsUrl = `${config.wsUrl}/ws/${config.projectId}`;
|
|
210
257
|
ws = new WebSocket(wsUrl);
|
|
211
258
|
|
|
212
259
|
ws.onopen = () => {
|
|
260
|
+
isConnecting = false;
|
|
213
261
|
// Wait for connected message before subscribing
|
|
214
262
|
};
|
|
215
263
|
|
|
@@ -219,6 +267,7 @@ export function useTetherSubscription(
|
|
|
219
267
|
|
|
220
268
|
if (message.type === 'connected') {
|
|
221
269
|
isConnected.value = true;
|
|
270
|
+
startHeartbeat();
|
|
222
271
|
// Subscribe to the query with our ID
|
|
223
272
|
ws?.send(JSON.stringify({
|
|
224
273
|
type: 'subscribe',
|
|
@@ -229,6 +278,13 @@ export function useTetherSubscription(
|
|
|
229
278
|
} else if (message.type === 'data' && message.id === subscriptionId) {
|
|
230
279
|
// Call onUpdate with the fresh data
|
|
231
280
|
onUpdate(message.data);
|
|
281
|
+
} else if (message.type === 'pong') {
|
|
282
|
+
// Heartbeat acknowledged
|
|
283
|
+
awaitingPong = false;
|
|
284
|
+
if (heartbeatTimeout) {
|
|
285
|
+
clearTimeout(heartbeatTimeout);
|
|
286
|
+
heartbeatTimeout = null;
|
|
287
|
+
}
|
|
232
288
|
}
|
|
233
289
|
} catch {
|
|
234
290
|
// Ignore parse errors
|
|
@@ -237,25 +293,53 @@ export function useTetherSubscription(
|
|
|
237
293
|
|
|
238
294
|
ws.onclose = () => {
|
|
239
295
|
isConnected.value = false;
|
|
240
|
-
|
|
241
|
-
|
|
296
|
+
isConnecting = false;
|
|
297
|
+
stopHeartbeat();
|
|
298
|
+
// Only reconnect if still mounted
|
|
299
|
+
if (isMounted) {
|
|
300
|
+
reconnectTimeout = setTimeout(connect, 3000);
|
|
301
|
+
}
|
|
242
302
|
};
|
|
243
303
|
|
|
244
304
|
ws.onerror = () => {
|
|
305
|
+
isConnecting = false;
|
|
245
306
|
ws?.close();
|
|
246
307
|
};
|
|
247
308
|
};
|
|
248
309
|
|
|
310
|
+
// Handle page visibility changes - reconnect when tab becomes visible
|
|
311
|
+
const handleVisibilityChange = () => {
|
|
312
|
+
if (!isMounted) return;
|
|
313
|
+
if (document.visibilityState === 'visible') {
|
|
314
|
+
// Tab became visible - check connection health
|
|
315
|
+
if (!ws || ws.readyState === WebSocket.CLOSED) {
|
|
316
|
+
// Clear any pending reconnect and connect immediately
|
|
317
|
+
if (reconnectTimeout) {
|
|
318
|
+
clearTimeout(reconnectTimeout);
|
|
319
|
+
reconnectTimeout = null;
|
|
320
|
+
}
|
|
321
|
+
connect();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
249
326
|
onMounted(() => {
|
|
327
|
+
isMounted = true;
|
|
250
328
|
connect();
|
|
329
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
251
330
|
});
|
|
252
331
|
|
|
253
332
|
onUnmounted(() => {
|
|
333
|
+
isMounted = false;
|
|
334
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
335
|
+
stopHeartbeat();
|
|
254
336
|
if (reconnectTimeout) {
|
|
255
337
|
clearTimeout(reconnectTimeout);
|
|
338
|
+
reconnectTimeout = null;
|
|
256
339
|
}
|
|
257
340
|
if (ws) {
|
|
258
341
|
ws.close();
|
|
342
|
+
ws = null;
|
|
259
343
|
}
|
|
260
344
|
});
|
|
261
345
|
}
|