@donkeylabs/adapter-sveltekit 2.0.2 → 2.0.3

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/client/index.ts +148 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/adapter-sveltekit",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "type": "module",
5
5
  "description": "SvelteKit adapter for @donkeylabs/server - seamless SSR/browser API integration",
6
6
  "main": "./src/index.ts",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@sveltejs/kit": "^2.0.0",
40
- "@donkeylabs/server": "^2.0.2"
40
+ "@donkeylabs/server": "^2.0.3"
41
41
  },
42
42
  "keywords": [
43
43
  "sveltekit",
@@ -41,14 +41,39 @@ export interface SSEOptions {
41
41
  reconnectDelay?: number;
42
42
  }
43
43
 
44
+ /**
45
+ * Options for SSE connection behavior.
46
+ */
47
+ export interface SSEConnectionOptions {
48
+ /** Enable automatic reconnection on disconnect (default: true) */
49
+ autoReconnect?: boolean;
50
+ /** Delay in ms before attempting reconnect (default: 3000) */
51
+ reconnectDelay?: number;
52
+ /** Called when connection is established */
53
+ onConnect?: () => void;
54
+ /** Called when connection is lost */
55
+ onDisconnect?: () => void;
56
+ }
57
+
44
58
  /**
45
59
  * Type-safe SSE connection wrapper.
46
60
  * Provides typed event handlers with automatic JSON parsing.
47
61
  *
48
62
  * @example
49
63
  * ```ts
64
+ * // With auto-reconnect (default)
50
65
  * const connection = api.notifications.subscribe({ userId: "123" });
51
66
  *
67
+ * // Disable auto-reconnect for manual control
68
+ * const connection = api.notifications.subscribe({ userId: "123" }, {
69
+ * autoReconnect: false
70
+ * });
71
+ *
72
+ * // Custom reconnect delay
73
+ * const connection = api.notifications.subscribe({ userId: "123" }, {
74
+ * reconnectDelay: 5000 // 5 seconds
75
+ * });
76
+ *
52
77
  * // Typed event handler - returns unsubscribe function
53
78
  * const unsubscribe = connection.on("notification", (data) => {
54
79
  * console.log(data.message); // Fully typed!
@@ -62,11 +87,75 @@ export interface SSEOptions {
62
87
  * ```
63
88
  */
64
89
  export class SSEConnection<TEvents extends Record<string, any> = Record<string, any>> {
65
- private eventSource: EventSource;
90
+ private eventSource: EventSource | null;
66
91
  private handlers = new Map<string, Set<(data: any) => void>>();
92
+ private url: string;
93
+ private options: SSEConnectionOptions;
94
+ private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
95
+ private closed = false;
96
+
97
+ constructor(url: string, options: SSEConnectionOptions = {}) {
98
+ this.url = url;
99
+ this.options = {
100
+ autoReconnect: true,
101
+ reconnectDelay: 3000,
102
+ ...options,
103
+ };
104
+ this.eventSource = this.createEventSource();
105
+ }
106
+
107
+ private createEventSource(): EventSource {
108
+ const es = new EventSource(this.url);
109
+
110
+ es.onopen = () => {
111
+ this.options.onConnect?.();
112
+ };
113
+
114
+ es.onerror = () => {
115
+ // Native EventSource auto-reconnects, but we want control
116
+ // Close it and handle reconnection ourselves
117
+ if (this.options.autoReconnect && !this.closed) {
118
+ this.options.onDisconnect?.();
119
+ es.close();
120
+ this.scheduleReconnect();
121
+ }
122
+ };
123
+
124
+ // Re-attach existing handlers to new EventSource
125
+ for (const [event] of this.handlers) {
126
+ es.addEventListener(event, (e: MessageEvent) => {
127
+ this.dispatchEvent(event, e.data);
128
+ });
129
+ }
130
+
131
+ return es;
132
+ }
133
+
134
+ private scheduleReconnect(): void {
135
+ if (this.reconnectTimeout || this.closed) return;
136
+
137
+ this.reconnectTimeout = setTimeout(() => {
138
+ this.reconnectTimeout = null;
139
+ if (!this.closed) {
140
+ this.eventSource = this.createEventSource();
141
+ }
142
+ }, this.options.reconnectDelay);
143
+ }
144
+
145
+ private dispatchEvent(event: string, rawData: string): void {
146
+ const handlers = this.handlers.get(event);
147
+ if (!handlers) return;
148
+
149
+ let data: any;
150
+ try {
151
+ data = JSON.parse(rawData);
152
+ } catch {
153
+ data = rawData;
154
+ }
67
155
 
68
- constructor(url: string) {
69
- this.eventSource = new EventSource(url);
156
+ for (const h of handlers) {
157
+ h(data);
158
+ }
70
159
  }
71
160
 
72
161
  /**
@@ -77,23 +166,14 @@ export class SSEConnection<TEvents extends Record<string, any> = Record<string,
77
166
  event: K & string,
78
167
  handler: (data: TEvents[K]) => void
79
168
  ): () => void {
80
- if (!this.handlers.has(event)) {
169
+ const isNewEvent = !this.handlers.has(event);
170
+
171
+ if (isNewEvent) {
81
172
  this.handlers.set(event, new Set());
82
173
 
83
174
  // Add EventSource listener for this event type
84
- this.eventSource.addEventListener(event, (e: MessageEvent) => {
85
- const handlers = this.handlers.get(event);
86
- if (handlers) {
87
- let data: any;
88
- try {
89
- data = JSON.parse(e.data);
90
- } catch {
91
- data = e.data;
92
- }
93
- for (const h of handlers) {
94
- h(data);
95
- }
96
- }
175
+ this.eventSource?.addEventListener(event, (e: MessageEvent) => {
176
+ this.dispatchEvent(event, e.data);
97
177
  });
98
178
  }
99
179
 
@@ -132,12 +212,23 @@ export class SSEConnection<TEvents extends Record<string, any> = Record<string,
132
212
  }
133
213
 
134
214
  /**
135
- * Register error handler
215
+ * Register error handler.
216
+ * Note: With autoReconnect enabled, errors trigger automatic reconnection.
136
217
  */
137
218
  onError(handler: (event: Event) => void): () => void {
138
- this.eventSource.onerror = handler;
219
+ const wrappedHandler = (e: Event) => {
220
+ handler(e);
221
+ };
222
+ if (this.eventSource) {
223
+ const existingHandler = this.eventSource.onerror;
224
+ this.eventSource.onerror = (e) => {
225
+ // Call existing handler (for reconnection logic)
226
+ if (existingHandler) (existingHandler as (e: Event) => void)(e);
227
+ wrappedHandler(e);
228
+ };
229
+ }
139
230
  return () => {
140
- this.eventSource.onerror = null;
231
+ // Can't easily remove, but handler will be replaced on reconnect
141
232
  };
142
233
  }
143
234
 
@@ -145,9 +236,15 @@ export class SSEConnection<TEvents extends Record<string, any> = Record<string,
145
236
  * Register open handler (connection established)
146
237
  */
147
238
  onOpen(handler: (event: Event) => void): () => void {
148
- this.eventSource.onopen = handler;
239
+ if (this.eventSource) {
240
+ const existingHandler = this.eventSource.onopen;
241
+ this.eventSource.onopen = (e) => {
242
+ if (existingHandler) (existingHandler as (e: Event) => void)(e);
243
+ handler(e);
244
+ };
245
+ }
149
246
  return () => {
150
- this.eventSource.onopen = null;
247
+ if (this.eventSource) this.eventSource.onopen = null;
151
248
  };
152
249
  }
153
250
 
@@ -155,21 +252,34 @@ export class SSEConnection<TEvents extends Record<string, any> = Record<string,
155
252
  * Get connection state
156
253
  */
157
254
  get readyState(): number {
158
- return this.eventSource.readyState;
255
+ return this.eventSource?.readyState ?? EventSource.CLOSED;
159
256
  }
160
257
 
161
258
  /**
162
259
  * Check if connected
163
260
  */
164
261
  get connected(): boolean {
165
- return this.eventSource.readyState === EventSource.OPEN;
262
+ return this.eventSource?.readyState === EventSource.OPEN;
166
263
  }
167
264
 
168
265
  /**
169
- * Close the SSE connection
266
+ * Check if reconnecting
267
+ */
268
+ get reconnecting(): boolean {
269
+ return this.reconnectTimeout !== null;
270
+ }
271
+
272
+ /**
273
+ * Close the SSE connection and stop reconnection attempts
170
274
  */
171
275
  close(): void {
172
- this.eventSource.close();
276
+ this.closed = true;
277
+ if (this.reconnectTimeout) {
278
+ clearTimeout(this.reconnectTimeout);
279
+ this.reconnectTimeout = null;
280
+ }
281
+ this.eventSource?.close();
282
+ this.eventSource = null;
173
283
  this.handlers.clear();
174
284
  }
175
285
  }
@@ -331,7 +441,8 @@ export class UnifiedApiClientBase {
331
441
  */
332
442
  protected sseConnect<TInput, TEvents extends Record<string, any> = Record<string, any>>(
333
443
  route: string,
334
- input?: TInput
444
+ input?: TInput,
445
+ options?: SSEConnectionOptions
335
446
  ): SSEConnection<TEvents> {
336
447
  let url = `${this.baseUrl}/${route}`;
337
448
 
@@ -344,7 +455,7 @@ export class UnifiedApiClientBase {
344
455
  url += `?${params.toString()}`;
345
456
  }
346
457
 
347
- return new SSEConnection<TEvents>(url);
458
+ return new SSEConnection<TEvents>(url, options);
348
459
  }
349
460
 
350
461
  /**
@@ -355,11 +466,16 @@ export class UnifiedApiClientBase {
355
466
  protected connectToSSERoute<TEvents extends Record<string, any>>(
356
467
  route: string,
357
468
  input: Record<string, any> = {},
358
- _options?: Omit<SSEOptions, "endpoint" | "channels">
469
+ options?: Omit<SSEOptions, "endpoint" | "channels">
359
470
  ): SSEConnection<TEvents> {
360
- // Note: options (onConnect, onDisconnect, etc.) are not used by SSEConnection
361
- // but we accept them for API compatibility with @donkeylabs/server/client
362
- return this.sseConnect<Record<string, any>, TEvents>(route, input);
471
+ // Map SSEOptions to SSEConnectionOptions
472
+ const connectionOptions: SSEConnectionOptions | undefined = options ? {
473
+ autoReconnect: options.autoReconnect,
474
+ reconnectDelay: options.reconnectDelay,
475
+ onConnect: options.onConnect,
476
+ onDisconnect: options.onDisconnect,
477
+ } : undefined;
478
+ return this.sseConnect<Record<string, any>, TEvents>(route, input, connectionOptions);
363
479
  }
364
480
 
365
481
  /**