@tthr/vue 0.0.26 → 0.0.27

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.
@@ -6,7 +6,7 @@
6
6
  * WebSocket subscriptions run client-side for realtime updates.
7
7
  */
8
8
 
9
- import { ref, onMounted, onUnmounted, watch, type Ref } from 'vue';
9
+ import { ref, onMounted, onUnmounted, type Ref } from 'vue';
10
10
 
11
11
  /**
12
12
  * Query state returned by useQuery
@@ -195,9 +195,46 @@ 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
+
202
+ // Heartbeat configuration
203
+ const HEARTBEAT_INTERVAL = 30000; // 30 seconds
204
+ const HEARTBEAT_TIMEOUT = 10000; // 10 seconds to receive pong
205
+
198
206
  // Generate a unique subscription ID
199
207
  const subscriptionId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
200
208
 
209
+ const startHeartbeat = () => {
210
+ stopHeartbeat();
211
+ heartbeatInterval = setInterval(() => {
212
+ if (ws?.readyState === WebSocket.OPEN) {
213
+ awaitingPong = true;
214
+ ws.send(JSON.stringify({ type: 'ping' }));
215
+
216
+ heartbeatTimeout = setTimeout(() => {
217
+ if (awaitingPong) {
218
+ console.warn('[Tether] Heartbeat timeout - forcing reconnect');
219
+ ws?.close();
220
+ }
221
+ }, HEARTBEAT_TIMEOUT);
222
+ }
223
+ }, HEARTBEAT_INTERVAL);
224
+ };
225
+
226
+ const stopHeartbeat = () => {
227
+ if (heartbeatInterval) {
228
+ clearInterval(heartbeatInterval);
229
+ heartbeatInterval = null;
230
+ }
231
+ if (heartbeatTimeout) {
232
+ clearTimeout(heartbeatTimeout);
233
+ heartbeatTimeout = null;
234
+ }
235
+ awaitingPong = false;
236
+ };
237
+
201
238
  const connect = () => {
202
239
  // Get config from window (set by plugin)
203
240
  const config = (window as any).__TETHER_CONFIG__;
@@ -219,6 +256,7 @@ export function useTetherSubscription(
219
256
 
220
257
  if (message.type === 'connected') {
221
258
  isConnected.value = true;
259
+ startHeartbeat();
222
260
  // Subscribe to the query with our ID
223
261
  ws?.send(JSON.stringify({
224
262
  type: 'subscribe',
@@ -229,6 +267,13 @@ export function useTetherSubscription(
229
267
  } else if (message.type === 'data' && message.id === subscriptionId) {
230
268
  // Call onUpdate with the fresh data
231
269
  onUpdate(message.data);
270
+ } else if (message.type === 'pong') {
271
+ // Heartbeat acknowledged
272
+ awaitingPong = false;
273
+ if (heartbeatTimeout) {
274
+ clearTimeout(heartbeatTimeout);
275
+ heartbeatTimeout = null;
276
+ }
232
277
  }
233
278
  } catch {
234
279
  // Ignore parse errors
@@ -237,6 +282,7 @@ export function useTetherSubscription(
237
282
 
238
283
  ws.onclose = () => {
239
284
  isConnected.value = false;
285
+ stopHeartbeat();
240
286
  // Reconnect after 3 seconds
241
287
  reconnectTimeout = setTimeout(connect, 3000);
242
288
  };
@@ -246,11 +292,29 @@ export function useTetherSubscription(
246
292
  };
247
293
  };
248
294
 
295
+ // Handle page visibility changes - reconnect when tab becomes visible
296
+ const handleVisibilityChange = () => {
297
+ if (document.visibilityState === 'visible') {
298
+ // Tab became visible - check connection health
299
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
300
+ // Clear any pending reconnect and connect immediately
301
+ if (reconnectTimeout) {
302
+ clearTimeout(reconnectTimeout);
303
+ reconnectTimeout = null;
304
+ }
305
+ connect();
306
+ }
307
+ }
308
+ };
309
+
249
310
  onMounted(() => {
250
311
  connect();
312
+ document.addEventListener('visibilitychange', handleVisibilityChange);
251
313
  });
252
314
 
253
315
  onUnmounted(() => {
316
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
317
+ stopHeartbeat();
254
318
  if (reconnectTimeout) {
255
319
  clearTimeout(reconnectTimeout);
256
320
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tthr/vue",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "description": "Tether Vue/Nuxt SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",