@donkeylabs/adapter-sveltekit 2.0.2 → 2.0.4
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/package.json +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.
|
|
3
|
+
"version": "2.0.4",
|
|
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.
|
|
40
|
+
"@donkeylabs/server": "^2.0.4"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"sveltekit",
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
262
|
+
return this.eventSource?.readyState === EventSource.OPEN;
|
|
166
263
|
}
|
|
167
264
|
|
|
168
265
|
/**
|
|
169
|
-
*
|
|
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.
|
|
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
|
-
|
|
469
|
+
options?: Omit<SSEOptions, "endpoint" | "channels">
|
|
359
470
|
): SSEConnection<TEvents> {
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
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
|
/**
|