@tthr/vue 0.0.27 → 0.0.29

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/module.ts CHANGED
@@ -43,12 +43,20 @@ export default defineNuxtModule<TetherModuleOptions>({
43
43
  setup(options, nuxt) {
44
44
  const resolver = createResolver(import.meta.url);
45
45
 
46
- // Read env vars at build time
46
+ // Get values from options or env vars (build-time defaults)
47
+ // These can be overridden at runtime via NUXT_TETHER_* and NUXT_PUBLIC_TETHER_* env vars
47
48
  const apiKey = process.env.TETHER_API_KEY || '';
48
49
  const url = options.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
49
50
  const projectId = options.projectId || process.env.TETHER_PROJECT_ID || '';
50
51
 
52
+ // Calculate WebSocket URL from HTTP URL
53
+ const wsUrl = url.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
54
+
51
55
  // Server-side config (includes API key - never exposed to client)
56
+ // Can be overridden at runtime via:
57
+ // - NUXT_TETHER_API_KEY
58
+ // - NUXT_TETHER_URL
59
+ // - NUXT_TETHER_PROJECT_ID
52
60
  nuxt.options.runtimeConfig.tether = {
53
61
  apiKey,
54
62
  url,
@@ -56,9 +64,12 @@ export default defineNuxtModule<TetherModuleOptions>({
56
64
  };
57
65
 
58
66
  // Public config (safe for client - no secrets)
67
+ // Can be overridden at runtime via:
68
+ // - NUXT_PUBLIC_TETHER_PROJECT_ID
69
+ // - NUXT_PUBLIC_TETHER_WS_URL
59
70
  nuxt.options.runtimeConfig.public.tether = {
60
71
  projectId,
61
- wsUrl: url.replace('http', 'ws'),
72
+ wsUrl,
62
73
  };
63
74
 
64
75
  // Add server API routes for proxying Tether requests
@@ -198,10 +198,12 @@ export function useTetherSubscription(
198
198
  let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
199
199
  let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;
200
200
  let awaitingPong = false;
201
+ let isConnecting = false;
202
+ let isMounted = false;
201
203
 
202
204
  // Heartbeat configuration
203
- const HEARTBEAT_INTERVAL = 30000; // 30 seconds
204
- const HEARTBEAT_TIMEOUT = 10000; // 10 seconds to receive pong
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)
205
207
 
206
208
  // Generate a unique subscription ID
207
209
  const subscriptionId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
@@ -214,7 +216,7 @@ export function useTetherSubscription(
214
216
  ws.send(JSON.stringify({ type: 'ping' }));
215
217
 
216
218
  heartbeatTimeout = setTimeout(() => {
217
- if (awaitingPong) {
219
+ if (awaitingPong && isMounted) {
218
220
  console.warn('[Tether] Heartbeat timeout - forcing reconnect');
219
221
  ws?.close();
220
222
  }
@@ -236,6 +238,13 @@ export function useTetherSubscription(
236
238
  };
237
239
 
238
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
+
239
248
  // Get config from window (set by plugin)
240
249
  const config = (window as any).__TETHER_CONFIG__;
241
250
  if (!config?.wsUrl || !config?.projectId) {
@@ -243,10 +252,12 @@ export function useTetherSubscription(
243
252
  return;
244
253
  }
245
254
 
255
+ isConnecting = true;
246
256
  const wsUrl = `${config.wsUrl}/ws/${config.projectId}`;
247
257
  ws = new WebSocket(wsUrl);
248
258
 
249
259
  ws.onopen = () => {
260
+ isConnecting = false;
250
261
  // Wait for connected message before subscribing
251
262
  };
252
263
 
@@ -282,21 +293,26 @@ export function useTetherSubscription(
282
293
 
283
294
  ws.onclose = () => {
284
295
  isConnected.value = false;
296
+ isConnecting = false;
285
297
  stopHeartbeat();
286
- // Reconnect after 3 seconds
287
- reconnectTimeout = setTimeout(connect, 3000);
298
+ // Only reconnect if still mounted
299
+ if (isMounted) {
300
+ reconnectTimeout = setTimeout(connect, 3000);
301
+ }
288
302
  };
289
303
 
290
304
  ws.onerror = () => {
305
+ isConnecting = false;
291
306
  ws?.close();
292
307
  };
293
308
  };
294
309
 
295
310
  // Handle page visibility changes - reconnect when tab becomes visible
296
311
  const handleVisibilityChange = () => {
312
+ if (!isMounted) return;
297
313
  if (document.visibilityState === 'visible') {
298
314
  // Tab became visible - check connection health
299
- if (!ws || ws.readyState !== WebSocket.OPEN) {
315
+ if (!ws || ws.readyState === WebSocket.CLOSED) {
300
316
  // Clear any pending reconnect and connect immediately
301
317
  if (reconnectTimeout) {
302
318
  clearTimeout(reconnectTimeout);
@@ -308,18 +324,22 @@ export function useTetherSubscription(
308
324
  };
309
325
 
310
326
  onMounted(() => {
327
+ isMounted = true;
311
328
  connect();
312
329
  document.addEventListener('visibilitychange', handleVisibilityChange);
313
330
  });
314
331
 
315
332
  onUnmounted(() => {
333
+ isMounted = false;
316
334
  document.removeEventListener('visibilitychange', handleVisibilityChange);
317
335
  stopHeartbeat();
318
336
  if (reconnectTimeout) {
319
337
  clearTimeout(reconnectTimeout);
338
+ reconnectTimeout = null;
320
339
  }
321
340
  if (ws) {
322
341
  ws.close();
342
+ ws = null;
323
343
  }
324
344
  });
325
345
  }
@@ -21,8 +21,20 @@ export default defineNuxtPlugin(() => {
21
21
 
22
22
  // Make config available for WebSocket connections
23
23
  // This is safe - no secrets are exposed
24
- window.__TETHER_CONFIG__ = {
25
- projectId: config.public.tether.projectId,
26
- wsUrl: config.public.tether.wsUrl,
24
+ const tetherConfig = {
25
+ projectId: config.public.tether?.projectId || '',
26
+ wsUrl: config.public.tether?.wsUrl || '',
27
27
  };
28
+
29
+ window.__TETHER_CONFIG__ = tetherConfig;
30
+
31
+ // Debug logging
32
+ console.log('[Tether] Client plugin loaded with config:', {
33
+ projectId: tetherConfig.projectId ? `${tetherConfig.projectId.slice(0, 8)}...` : '(empty)',
34
+ wsUrl: tetherConfig.wsUrl || '(empty)',
35
+ });
36
+
37
+ if (!tetherConfig.wsUrl || !tetherConfig.projectId) {
38
+ console.warn('[Tether] Config incomplete - WebSocket subscriptions will not work');
39
+ }
28
40
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tthr/vue",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "Tether Vue/Nuxt SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",