@tthr/vue 0.0.71 → 0.0.75

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.
@@ -0,0 +1,506 @@
1
+ /**
2
+ * Nuxt composables for Tether
3
+ *
4
+ * These are auto-imported when using the Tether Nuxt module.
5
+ * Queries and mutations are executed server-side to keep API keys secure.
6
+ * WebSocket subscriptions run client-side for realtime updates - automatically!
7
+ */
8
+
9
+ import { ref, onMounted, onUnmounted, computed, type Ref, type ComputedRef } from 'vue';
10
+ import { useAsyncData } from '#imports';
11
+
12
+ // ============================================================================
13
+ // Shared WebSocket Connection Manager
14
+ // ============================================================================
15
+
16
+ interface SubscriptionCallback {
17
+ onData: (data: unknown) => void;
18
+ onInvalidate: () => void;
19
+ }
20
+
21
+ interface ConnectionManager {
22
+ ws: WebSocket | null;
23
+ subscriptions: Map<string, SubscriptionCallback>;
24
+ isConnecting: boolean;
25
+ reconnectTimeout: ReturnType<typeof setTimeout> | null;
26
+ heartbeatInterval: ReturnType<typeof setInterval> | null;
27
+ heartbeatTimeout: ReturnType<typeof setTimeout> | null;
28
+ awaitingPong: boolean;
29
+ refCount: number;
30
+ }
31
+
32
+ // Singleton connection manager (client-side only)
33
+ let connectionManager: ConnectionManager | null = null;
34
+
35
+ function getConnectionManager(): ConnectionManager {
36
+ if (!connectionManager) {
37
+ connectionManager = {
38
+ ws: null,
39
+ subscriptions: new Map(),
40
+ isConnecting: false,
41
+ reconnectTimeout: null,
42
+ heartbeatInterval: null,
43
+ heartbeatTimeout: null,
44
+ awaitingPong: false,
45
+ refCount: 0,
46
+ };
47
+ }
48
+ return connectionManager;
49
+ }
50
+
51
+ const HEARTBEAT_INTERVAL = 25000;
52
+ const HEARTBEAT_TIMEOUT = 15000;
53
+
54
+ function startHeartbeat(cm: ConnectionManager) {
55
+ stopHeartbeat(cm);
56
+ cm.heartbeatInterval = setInterval(() => {
57
+ if (cm.ws?.readyState === WebSocket.OPEN) {
58
+ cm.awaitingPong = true;
59
+ cm.ws.send(JSON.stringify({ type: 'ping' }));
60
+ cm.heartbeatTimeout = setTimeout(() => {
61
+ if (cm.awaitingPong) {
62
+ console.warn('[Tether] Heartbeat timeout - forcing reconnect');
63
+ cm.ws?.close();
64
+ }
65
+ }, HEARTBEAT_TIMEOUT);
66
+ }
67
+ }, HEARTBEAT_INTERVAL);
68
+ }
69
+
70
+ function stopHeartbeat(cm: ConnectionManager) {
71
+ if (cm.heartbeatInterval) {
72
+ clearInterval(cm.heartbeatInterval);
73
+ cm.heartbeatInterval = null;
74
+ }
75
+ if (cm.heartbeatTimeout) {
76
+ clearTimeout(cm.heartbeatTimeout);
77
+ cm.heartbeatTimeout = null;
78
+ }
79
+ cm.awaitingPong = false;
80
+ }
81
+
82
+ function connectWebSocket(cm: ConnectionManager) {
83
+ if (cm.isConnecting) return;
84
+ if (cm.ws && (cm.ws.readyState === WebSocket.OPEN || cm.ws.readyState === WebSocket.CONNECTING)) {
85
+ return;
86
+ }
87
+
88
+ const config = (window as any).__TETHER_CONFIG__;
89
+ if (!config?.wsUrl || !config?.projectId) {
90
+ console.warn('[Tether] WebSocket config not available');
91
+ return;
92
+ }
93
+
94
+ cm.isConnecting = true;
95
+ const wsUrl = `${config.wsUrl}/ws/${config.projectId}`;
96
+ cm.ws = new WebSocket(wsUrl);
97
+
98
+ cm.ws.onopen = () => {
99
+ cm.isConnecting = false;
100
+ };
101
+
102
+ cm.ws.onmessage = (event) => {
103
+ try {
104
+ const message = JSON.parse(event.data);
105
+
106
+ if (message.type === 'connected') {
107
+ startHeartbeat(cm);
108
+ // Re-subscribe all active subscriptions
109
+ for (const [subscriptionId, _callback] of cm.subscriptions) {
110
+ const [queryName, argsJson] = subscriptionId.split('::');
111
+ const args = argsJson ? JSON.parse(argsJson) : undefined;
112
+ cm.ws?.send(JSON.stringify({
113
+ type: 'subscribe',
114
+ id: subscriptionId,
115
+ query: queryName,
116
+ args,
117
+ }));
118
+ }
119
+ } else if (message.type === 'data') {
120
+ const callback = cm.subscriptions.get(message.id);
121
+ if (callback) {
122
+ callback.onData(message.data);
123
+ }
124
+ } else if (message.type === 'invalidate') {
125
+ const callback = cm.subscriptions.get(message.id);
126
+ if (callback) {
127
+ callback.onInvalidate();
128
+ }
129
+ } else if (message.type === 'pong') {
130
+ cm.awaitingPong = false;
131
+ if (cm.heartbeatTimeout) {
132
+ clearTimeout(cm.heartbeatTimeout);
133
+ cm.heartbeatTimeout = null;
134
+ }
135
+ }
136
+ } catch {
137
+ // Ignore parse errors
138
+ }
139
+ };
140
+
141
+ cm.ws.onclose = () => {
142
+ cm.isConnecting = false;
143
+ stopHeartbeat(cm);
144
+ // Reconnect if there are still active subscriptions
145
+ if (cm.subscriptions.size > 0) {
146
+ cm.reconnectTimeout = setTimeout(() => connectWebSocket(cm), 3000);
147
+ }
148
+ };
149
+
150
+ cm.ws.onerror = () => {
151
+ cm.isConnecting = false;
152
+ cm.ws?.close();
153
+ };
154
+ }
155
+
156
+ function subscribe(
157
+ queryName: string,
158
+ args: Record<string, unknown> | undefined,
159
+ callbacks: SubscriptionCallback
160
+ ): () => void {
161
+ const cm = getConnectionManager();
162
+ const subscriptionId = `${queryName}::${JSON.stringify(args ?? {})}`;
163
+
164
+ cm.subscriptions.set(subscriptionId, callbacks);
165
+ cm.refCount++;
166
+
167
+ // Connect if not already connected
168
+ if (!cm.ws || cm.ws.readyState === WebSocket.CLOSED) {
169
+ connectWebSocket(cm);
170
+ } else if (cm.ws.readyState === WebSocket.OPEN) {
171
+ // Already connected - subscribe immediately
172
+ cm.ws.send(JSON.stringify({
173
+ type: 'subscribe',
174
+ id: subscriptionId,
175
+ query: queryName,
176
+ args,
177
+ }));
178
+ }
179
+
180
+ // Return cleanup function
181
+ return () => {
182
+ cm.subscriptions.delete(subscriptionId);
183
+ cm.refCount--;
184
+
185
+ // Unsubscribe from server
186
+ if (cm.ws?.readyState === WebSocket.OPEN) {
187
+ cm.ws.send(JSON.stringify({
188
+ type: 'unsubscribe',
189
+ id: subscriptionId,
190
+ }));
191
+ }
192
+
193
+ // Close connection if no more subscriptions
194
+ if (cm.refCount === 0) {
195
+ if (cm.reconnectTimeout) {
196
+ clearTimeout(cm.reconnectTimeout);
197
+ cm.reconnectTimeout = null;
198
+ }
199
+ stopHeartbeat(cm);
200
+ cm.ws?.close();
201
+ cm.ws = null;
202
+ }
203
+ };
204
+ }
205
+
206
+ // Handle visibility changes globally
207
+ if (typeof window !== 'undefined') {
208
+ document.addEventListener('visibilitychange', () => {
209
+ if (document.visibilityState === 'visible') {
210
+ const cm = connectionManager;
211
+ if (cm && cm.subscriptions.size > 0) {
212
+ if (!cm.ws || cm.ws.readyState === WebSocket.CLOSED) {
213
+ if (cm.reconnectTimeout) {
214
+ clearTimeout(cm.reconnectTimeout);
215
+ cm.reconnectTimeout = null;
216
+ }
217
+ connectWebSocket(cm);
218
+ }
219
+ }
220
+ }
221
+ });
222
+ }
223
+
224
+ // ============================================================================
225
+ // Query Composable
226
+ // ============================================================================
227
+
228
+ /**
229
+ * Query state returned by useQuery
230
+ */
231
+ export interface QueryState<T> {
232
+ data: Ref<T | undefined>;
233
+ error: Ref<Error | null>;
234
+ isLoading: ComputedRef<boolean>;
235
+ isConnected: Ref<boolean>;
236
+ refetch: () => Promise<void>;
237
+ }
238
+
239
+ /**
240
+ * Query function reference
241
+ */
242
+ export interface QueryFunction<TArgs = void, TResult = unknown> {
243
+ _name: string;
244
+ _args?: TArgs;
245
+ _result?: TResult;
246
+ }
247
+
248
+ /**
249
+ * Reactive query composable with automatic realtime updates
250
+ *
251
+ * Uses Nuxt's useAsyncData internally for proper SSR support.
252
+ * Data is fetched on the server and hydrated on the client.
253
+ * Automatically subscribes to WebSocket updates for realtime sync.
254
+ *
255
+ * @example
256
+ * ```vue
257
+ * <script setup>
258
+ * // That's it! Data automatically updates when it changes anywhere
259
+ * const { data: posts, isLoading, isConnected } = useQuery(api.posts.list);
260
+ * </script>
261
+ * ```
262
+ */
263
+ export function useQuery<TArgs, TResult>(
264
+ query: QueryFunction<TArgs, TResult> | string,
265
+ args?: TArgs
266
+ ): QueryState<TResult> {
267
+ const queryName = typeof query === 'string' ? query : query._name;
268
+ const queryArgs = args as Record<string, unknown> | undefined;
269
+
270
+ // Create a unique key for this query based on name and args
271
+ const queryKey = `tether-${queryName}-${JSON.stringify(args ?? {})}`;
272
+
273
+ // Track WebSocket connection state
274
+ const isConnected = ref(false);
275
+
276
+ // Use Nuxt's useAsyncData for proper SSR support
277
+ const { data, error: asyncError, status, refresh } = useAsyncData<TResult>(
278
+ queryKey,
279
+ async () => {
280
+ const response = await $fetch<{ data: TResult }>('/api/_tether/query', {
281
+ method: 'POST',
282
+ body: {
283
+ function: queryName,
284
+ args,
285
+ },
286
+ });
287
+ return response.data;
288
+ },
289
+ {
290
+ dedupe: 'cancel',
291
+ }
292
+ );
293
+
294
+ // Wrap the error as a computed ref
295
+ const error = computed(() => {
296
+ if (asyncError.value) {
297
+ return asyncError.value instanceof Error
298
+ ? asyncError.value
299
+ : new Error(String(asyncError.value));
300
+ }
301
+ return null;
302
+ });
303
+
304
+ // Compute isLoading from status
305
+ const isLoading = computed(() => status.value === 'pending');
306
+
307
+ // Refetch function
308
+ const refetch = async () => {
309
+ await refresh();
310
+ };
311
+
312
+ // Auto-subscribe on client side
313
+ if (import.meta.client) {
314
+ let unsubscribe: (() => void) | null = null;
315
+
316
+ onMounted(() => {
317
+ unsubscribe = subscribe(queryName, queryArgs, {
318
+ onData: (newData) => {
319
+ data.value = newData as TResult;
320
+ },
321
+ onInvalidate: () => {
322
+ refresh();
323
+ },
324
+ });
325
+
326
+ // Track connection state
327
+ const checkConnection = () => {
328
+ const cm = connectionManager;
329
+ isConnected.value = cm?.ws?.readyState === WebSocket.OPEN ?? false;
330
+ };
331
+ checkConnection();
332
+ const interval = setInterval(checkConnection, 1000);
333
+ onUnmounted(() => clearInterval(interval));
334
+ });
335
+
336
+ onUnmounted(() => {
337
+ unsubscribe?.();
338
+ });
339
+ }
340
+
341
+ return {
342
+ data: data as Ref<TResult | undefined>,
343
+ error: error as unknown as Ref<Error | null>,
344
+ isLoading,
345
+ isConnected,
346
+ refetch,
347
+ };
348
+ }
349
+
350
+ // ============================================================================
351
+ // Mutation Composable
352
+ // ============================================================================
353
+
354
+ /**
355
+ * Mutation state returned by useMutation
356
+ */
357
+ export interface MutationState<TArgs, TResult> {
358
+ data: Ref<TResult | undefined>;
359
+ error: Ref<Error | null>;
360
+ isPending: Ref<boolean>;
361
+ mutate: (args: TArgs) => Promise<TResult>;
362
+ reset: () => void;
363
+ }
364
+
365
+ /**
366
+ * Mutation function reference
367
+ */
368
+ export interface MutationFunction<TArgs = void, TResult = unknown> {
369
+ _name: string;
370
+ _args?: TArgs;
371
+ _result?: TResult;
372
+ }
373
+
374
+ /**
375
+ * Mutation composable
376
+ *
377
+ * @example
378
+ * ```vue
379
+ * <script setup>
380
+ * const { mutate: createPost, isPending } = useMutation(api.posts.create);
381
+ *
382
+ * async function handleSubmit() {
383
+ * await createPost({ title: 'Hello', content: '...' });
384
+ * // All useQuery subscribers automatically update!
385
+ * }
386
+ * </script>
387
+ * ```
388
+ */
389
+ export function useMutation<TArgs, TResult>(
390
+ mutation: MutationFunction<TArgs, TResult> | string
391
+ ): MutationState<TArgs, TResult> {
392
+ const mutationName = typeof mutation === 'string' ? mutation : mutation._name;
393
+ const data = ref<TResult>();
394
+ const error = ref<Error | null>(null);
395
+ const isPending = ref(false);
396
+
397
+ const mutate = async (args: TArgs): Promise<TResult> => {
398
+ try {
399
+ isPending.value = true;
400
+ error.value = null;
401
+
402
+ const response = await $fetch<{ data: TResult }>('/api/_tether/mutation', {
403
+ method: 'POST',
404
+ body: {
405
+ function: mutationName,
406
+ args,
407
+ },
408
+ });
409
+
410
+ data.value = response.data;
411
+ return response.data;
412
+ } catch (e) {
413
+ error.value = e instanceof Error ? e : new Error(String(e));
414
+ throw e;
415
+ } finally {
416
+ isPending.value = false;
417
+ }
418
+ };
419
+
420
+ const reset = () => {
421
+ data.value = undefined;
422
+ error.value = null;
423
+ isPending.value = false;
424
+ };
425
+
426
+ return {
427
+ data: data as Ref<TResult | undefined>,
428
+ error,
429
+ isPending,
430
+ mutate,
431
+ reset,
432
+ };
433
+ }
434
+
435
+ // ============================================================================
436
+ // Manual Subscription (for advanced use cases)
437
+ // ============================================================================
438
+
439
+ /**
440
+ * Subscription handler options
441
+ */
442
+ export interface SubscriptionHandlers {
443
+ /** Called when fresh data is received from the server */
444
+ onData?: (data: unknown) => void;
445
+ /** Called when server invalidates the subscription (signals to refetch) */
446
+ onInvalidate?: () => void;
447
+ }
448
+
449
+ /**
450
+ * Manual WebSocket subscription composable
451
+ *
452
+ * NOTE: You typically don't need this! useQuery automatically subscribes to updates.
453
+ * This is only for advanced use cases where you need custom subscription handling.
454
+ *
455
+ * @example
456
+ * ```vue
457
+ * <script setup>
458
+ * // For custom subscription handling (rare)
459
+ * const { isConnected } = useTetherSubscription('custom.query', {}, {
460
+ * onData: (data) => console.log('Got data:', data),
461
+ * onInvalidate: () => console.log('Data invalidated'),
462
+ * });
463
+ * </script>
464
+ * ```
465
+ */
466
+ export function useTetherSubscription(
467
+ queryName: string,
468
+ args: Record<string, unknown> | undefined,
469
+ handlers: SubscriptionHandlers | ((data?: unknown) => void)
470
+ ): { isConnected: Ref<boolean> } {
471
+ const isConnected = ref(false);
472
+
473
+ // Normalize handlers
474
+ const onData = typeof handlers === 'function'
475
+ ? handlers
476
+ : handlers.onData ?? (() => {});
477
+ const onInvalidate = typeof handlers === 'function'
478
+ ? () => handlers(undefined)
479
+ : handlers.onInvalidate ?? (() => {});
480
+
481
+ if (import.meta.client) {
482
+ let unsubscribe: (() => void) | null = null;
483
+
484
+ onMounted(() => {
485
+ unsubscribe = subscribe(queryName, args, {
486
+ onData,
487
+ onInvalidate,
488
+ });
489
+
490
+ // Track connection state
491
+ const checkConnection = () => {
492
+ const cm = connectionManager;
493
+ isConnected.value = cm?.ws?.readyState === WebSocket.OPEN ?? false;
494
+ };
495
+ checkConnection();
496
+ const interval = setInterval(checkConnection, 1000);
497
+ onUnmounted(() => clearInterval(interval));
498
+ });
499
+
500
+ onUnmounted(() => {
501
+ unsubscribe?.();
502
+ });
503
+ }
504
+
505
+ return { isConnected };
506
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Client-side Tether plugin
3
+ *
4
+ * Sets up the WebSocket configuration for realtime subscriptions.
5
+ * This only exposes the project ID and WebSocket URL - no secrets.
6
+ */
7
+
8
+ import { defineNuxtPlugin, useRuntimeConfig } from '#imports';
9
+
10
+ declare global {
11
+ interface Window {
12
+ __TETHER_CONFIG__?: {
13
+ projectId: string;
14
+ wsUrl: string;
15
+ };
16
+ }
17
+ }
18
+
19
+ export default defineNuxtPlugin(() => {
20
+ const config = useRuntimeConfig();
21
+
22
+ // Make config available for WebSocket connections
23
+ // This is safe - no secrets are exposed
24
+ const tetherConfig = {
25
+ projectId: config.public.tether?.projectId || '',
26
+ wsUrl: config.public.tether?.wsUrl || '',
27
+ };
28
+
29
+ window.__TETHER_CONFIG__ = tetherConfig;
30
+
31
+ if (!tetherConfig.wsUrl || !tetherConfig.projectId) {
32
+ console.warn('[Tether] Config incomplete - WebSocket subscriptions will not work');
33
+ }
34
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAiC7B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAC3C,IAAI,CAGN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEhE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;;AA6SD,wBAkCG"}
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAqC7B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAC3C,IAAI,CAGN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEhE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;;AAslBD,wBAmCG"}
@@ -372,7 +372,8 @@ async function executeViaApi(config, functionName, functionType, args) {
372
372
  try {
373
373
  const error = JSON.parse(responseText);
374
374
  errorMsg = error.error || error.message || `Function '${functionName}' failed with status ${response.status}`;
375
- } catch {
375
+ }
376
+ catch {
376
377
  errorMsg = responseText || `Function '${functionName}' failed with status ${response.status}`;
377
378
  }
378
379
  log.debug(`/${type} endpoint returned ${response.status}: ${errorMsg}`);
@@ -427,10 +428,11 @@ async function handleCronTrigger(config, trigger) {
427
428
  };
428
429
  // Create tether context with env vars from process.env
429
430
  // Note: For cron execution, env vars should be set in the Nuxt app's environment
431
+ const processEnv = globalThis.process?.env ?? {};
430
432
  const tether = {
431
433
  env: new Proxy({}, {
432
434
  get(_target, key) {
433
- return process.env[key];
435
+ return processEnv[key];
434
436
  },
435
437
  }),
436
438
  };
@@ -469,7 +471,8 @@ async function handleCronTrigger(config, trigger) {
469
471
  function startHeartbeat() {
470
472
  stopHeartbeat();
471
473
  heartbeatTimer = setInterval(() => {
472
- if (ws?.readyState === WebSocket.OPEN) {
474
+ // WebSocket.OPEN = 1
475
+ if (ws?.readyState === 1) {
473
476
  awaitingPong = true;
474
477
  log.debug('Sending heartbeat ping');
475
478
  ws.send(JSON.stringify({ type: 'ping' }));
@@ -496,16 +499,29 @@ function stopHeartbeat() {
496
499
  }
497
500
  awaitingPong = false;
498
501
  }
499
- function connect(config) {
500
- if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {
502
+ // WebSocket implementation - loaded dynamically for Node.js
503
+ let WebSocketImpl = null;
504
+ async function getWebSocketImpl() {
505
+ if (WebSocketImpl)
506
+ return WebSocketImpl;
507
+ if (typeof WebSocket !== 'undefined') {
508
+ WebSocketImpl = WebSocket;
509
+ }
510
+ else {
511
+ // Dynamic import for Node.js ESM compatibility
512
+ const wsModule = await import('ws');
513
+ WebSocketImpl = wsModule.default;
514
+ }
515
+ return WebSocketImpl;
516
+ }
517
+ async function connect(config) {
518
+ const WS = await getWebSocketImpl();
519
+ if (ws?.readyState === WS.OPEN || ws?.readyState === WS.CONNECTING) {
501
520
  return;
502
521
  }
503
522
  try {
504
523
  const url = getWsUrl(config);
505
- // Use ws package for Node.js
506
- // eslint-disable-next-line @typescript-eslint/no-require-imports
507
- const WebSocketImpl = typeof WebSocket !== 'undefined' ? WebSocket : require('ws');
508
- ws = new WebSocketImpl(url);
524
+ ws = new WS(url);
509
525
  ws.onopen = () => {
510
526
  reconnectAttempts = 0;
511
527
  log.debug('WebSocket connected');
@@ -568,7 +584,7 @@ function handleReconnect(config) {
568
584
  const delay = Math.min(RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1), 30000);
569
585
  log.debug(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
570
586
  setTimeout(() => {
571
- connect(config);
587
+ connect(config).catch((err) => log.error('Reconnect failed:', err));
572
588
  }, delay);
573
589
  }
574
590
  export default defineNitroPlugin((nitro) => {
@@ -589,7 +605,7 @@ export default defineNitroPlugin((nitro) => {
589
605
  const config = { url, projectId, apiKey, environment };
590
606
  // Connect on next tick to allow the server to fully initialise
591
607
  process.nextTick(() => {
592
- connect(config);
608
+ connect(config).catch((err) => log.error('Initial connect failed:', err));
593
609
  });
594
610
  // Clean up on shutdown
595
611
  nitro.hooks.hook('close', () => {
@@ -602,4 +618,4 @@ export default defineNitroPlugin((nitro) => {
602
618
  log.debug('Connection closed');
603
619
  });
604
620
  });
605
- //# sourceMappingURL=cron.js.map
621
+ //# sourceMappingURL=cron.js.map