@donkeylabs/adapter-sveltekit 2.0.13 → 2.0.15
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 +15 -14
- package/src/client/index.ts +0 -659
- package/src/generator/index.ts +0 -351
- package/src/hooks/index.ts +0 -124
- package/src/index.ts +0 -391
- package/src/vite.ts +0 -729
package/package.json
CHANGED
|
@@ -1,38 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@donkeylabs/adapter-sveltekit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SvelteKit adapter for @donkeylabs/server - seamless SSR/browser API integration",
|
|
6
|
-
"main": "./
|
|
7
|
-
"types": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"import": "./
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./client": {
|
|
14
|
-
"types": "./
|
|
15
|
-
"import": "./
|
|
14
|
+
"types": "./dist/client/index.d.ts",
|
|
15
|
+
"import": "./dist/client/index.js"
|
|
16
16
|
},
|
|
17
17
|
"./hooks": {
|
|
18
|
-
"types": "./
|
|
19
|
-
"import": "./
|
|
18
|
+
"types": "./dist/hooks/index.d.ts",
|
|
19
|
+
"import": "./dist/hooks/index.js"
|
|
20
20
|
},
|
|
21
21
|
"./generator": {
|
|
22
|
-
"types": "./
|
|
23
|
-
"import": "./
|
|
22
|
+
"types": "./dist/generator/index.d.ts",
|
|
23
|
+
"import": "./dist/generator/index.js"
|
|
24
24
|
},
|
|
25
25
|
"./vite": {
|
|
26
|
-
"types": "./
|
|
27
|
-
"import": "./
|
|
26
|
+
"types": "./dist/vite.d.ts",
|
|
27
|
+
"import": "./dist/vite.js"
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
|
-
"
|
|
31
|
+
"dist",
|
|
32
32
|
"LICENSE",
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
37
|
"typecheck": "bun --bun tsc --noEmit"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
package/src/client/index.ts
DELETED
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified API client for @donkeylabs/adapter-sveltekit
|
|
3
|
-
*
|
|
4
|
-
* Auto-detects environment:
|
|
5
|
-
* - SSR: Direct service calls through locals (no HTTP)
|
|
6
|
-
* - Browser: HTTP calls to API routes
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface RequestOptions {
|
|
10
|
-
headers?: Record<string, string>;
|
|
11
|
-
signal?: AbortSignal;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ClientOptions {
|
|
15
|
-
/** Base URL for HTTP calls. Defaults to empty string (relative URLs). */
|
|
16
|
-
baseUrl?: string;
|
|
17
|
-
/** SvelteKit locals object for SSR direct calls. */
|
|
18
|
-
locals?: any;
|
|
19
|
-
/** Custom fetch function. In SSR, pass event.fetch to handle relative URLs. */
|
|
20
|
-
fetch?: typeof fetch;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface SSESubscription {
|
|
24
|
-
unsubscribe: () => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* SSE options for connection configuration.
|
|
29
|
-
* Compatible with @donkeylabs/server/client SSEOptions.
|
|
30
|
-
*/
|
|
31
|
-
export interface SSEOptions {
|
|
32
|
-
/** Called when connection is established */
|
|
33
|
-
onConnect?: () => void;
|
|
34
|
-
/** Called when connection is lost */
|
|
35
|
-
onDisconnect?: () => void;
|
|
36
|
-
/** Called on connection error */
|
|
37
|
-
onError?: (error: Event) => void;
|
|
38
|
-
/** Auto-reconnect on disconnect (default: true) */
|
|
39
|
-
autoReconnect?: boolean;
|
|
40
|
-
/** Reconnect delay in ms (default: 3000) */
|
|
41
|
-
reconnectDelay?: number;
|
|
42
|
-
}
|
|
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
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Type-safe SSE connection wrapper.
|
|
60
|
-
* Provides typed event handlers with automatic JSON parsing.
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```ts
|
|
64
|
-
* // With auto-reconnect (default)
|
|
65
|
-
* const connection = api.notifications.subscribe({ userId: "123" });
|
|
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
|
-
*
|
|
77
|
-
* // Typed event handler - returns unsubscribe function
|
|
78
|
-
* const unsubscribe = connection.on("notification", (data) => {
|
|
79
|
-
* console.log(data.message); // Fully typed!
|
|
80
|
-
* });
|
|
81
|
-
*
|
|
82
|
-
* // Later: unsubscribe from this specific handler
|
|
83
|
-
* unsubscribe();
|
|
84
|
-
*
|
|
85
|
-
* // Close entire connection
|
|
86
|
-
* connection.close();
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
export class SSEConnection<TEvents extends Record<string, any> = Record<string, any>> {
|
|
90
|
-
private eventSource: EventSource | null;
|
|
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
|
-
}
|
|
155
|
-
|
|
156
|
-
for (const h of handlers) {
|
|
157
|
-
h(data);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Register a typed event handler.
|
|
163
|
-
* @returns Unsubscribe function to remove this specific handler
|
|
164
|
-
*/
|
|
165
|
-
on<K extends keyof TEvents>(
|
|
166
|
-
event: K & string,
|
|
167
|
-
handler: (data: TEvents[K]) => void
|
|
168
|
-
): () => void {
|
|
169
|
-
const isNewEvent = !this.handlers.has(event);
|
|
170
|
-
|
|
171
|
-
if (isNewEvent) {
|
|
172
|
-
this.handlers.set(event, new Set());
|
|
173
|
-
|
|
174
|
-
// Add EventSource listener for this event type
|
|
175
|
-
this.eventSource?.addEventListener(event, (e: MessageEvent) => {
|
|
176
|
-
this.dispatchEvent(event, e.data);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
this.handlers.get(event)!.add(handler);
|
|
181
|
-
|
|
182
|
-
// Return unsubscribe function
|
|
183
|
-
return () => {
|
|
184
|
-
const handlers = this.handlers.get(event);
|
|
185
|
-
if (handlers) {
|
|
186
|
-
handlers.delete(handler);
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Register a typed event handler that fires only once.
|
|
193
|
-
* @returns Unsubscribe function to remove this specific handler
|
|
194
|
-
*/
|
|
195
|
-
once<K extends keyof TEvents>(
|
|
196
|
-
event: K & string,
|
|
197
|
-
handler: (data: TEvents[K]) => void
|
|
198
|
-
): () => void {
|
|
199
|
-
const wrappedHandler = (data: TEvents[K]) => {
|
|
200
|
-
unsubscribe();
|
|
201
|
-
handler(data);
|
|
202
|
-
};
|
|
203
|
-
const unsubscribe = this.on(event, wrappedHandler);
|
|
204
|
-
return unsubscribe;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Remove all handlers for an event.
|
|
209
|
-
*/
|
|
210
|
-
off<K extends keyof TEvents>(event: K & string): void {
|
|
211
|
-
this.handlers.delete(event);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Register error handler.
|
|
216
|
-
* Note: With autoReconnect enabled, errors trigger automatic reconnection.
|
|
217
|
-
*/
|
|
218
|
-
onError(handler: (event: Event) => void): () => void {
|
|
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
|
-
}
|
|
230
|
-
return () => {
|
|
231
|
-
// Can't easily remove, but handler will be replaced on reconnect
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Register open handler (connection established)
|
|
237
|
-
*/
|
|
238
|
-
onOpen(handler: (event: Event) => void): () => void {
|
|
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
|
-
}
|
|
246
|
-
return () => {
|
|
247
|
-
if (this.eventSource) this.eventSource.onopen = null;
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Get connection state
|
|
253
|
-
*/
|
|
254
|
-
get readyState(): number {
|
|
255
|
-
return this.eventSource?.readyState ?? EventSource.CLOSED;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Check if connected
|
|
260
|
-
*/
|
|
261
|
-
get connected(): boolean {
|
|
262
|
-
return this.eventSource?.readyState === EventSource.OPEN;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
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
|
|
274
|
-
*/
|
|
275
|
-
close(): void {
|
|
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;
|
|
283
|
-
this.handlers.clear();
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Base class for unified API clients.
|
|
289
|
-
* Extend this class with your generated route methods.
|
|
290
|
-
*/
|
|
291
|
-
export class UnifiedApiClientBase {
|
|
292
|
-
protected baseUrl: string;
|
|
293
|
-
protected locals?: any;
|
|
294
|
-
protected isSSR: boolean;
|
|
295
|
-
protected customFetch?: typeof fetch;
|
|
296
|
-
|
|
297
|
-
constructor(options?: ClientOptions) {
|
|
298
|
-
this.baseUrl = options?.baseUrl ?? "";
|
|
299
|
-
this.locals = options?.locals;
|
|
300
|
-
this.isSSR = typeof window === "undefined";
|
|
301
|
-
this.customFetch = options?.fetch;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Make a request to an API route.
|
|
306
|
-
* Automatically uses direct calls in SSR (when locals.handleRoute is available), HTTP otherwise.
|
|
307
|
-
*/
|
|
308
|
-
protected async request<TInput, TOutput>(
|
|
309
|
-
route: string,
|
|
310
|
-
input: TInput,
|
|
311
|
-
options?: RequestOptions
|
|
312
|
-
): Promise<TOutput> {
|
|
313
|
-
// Use direct route handler if available (SSR with locals)
|
|
314
|
-
if (this.locals?.handleRoute) {
|
|
315
|
-
return this.locals.handleRoute(route, input);
|
|
316
|
-
}
|
|
317
|
-
// Fall back to HTTP (browser or SSR without locals)
|
|
318
|
-
return this.httpCall<TInput, TOutput>(route, input, options);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* HTTP call to API endpoint (browser or SSR with event.fetch).
|
|
323
|
-
*/
|
|
324
|
-
private async httpCall<TInput, TOutput>(
|
|
325
|
-
route: string,
|
|
326
|
-
input: TInput,
|
|
327
|
-
options?: RequestOptions
|
|
328
|
-
): Promise<TOutput> {
|
|
329
|
-
const url = `${this.baseUrl}/${route}`;
|
|
330
|
-
const fetchFn = this.customFetch ?? fetch;
|
|
331
|
-
|
|
332
|
-
const response = await fetchFn(url, {
|
|
333
|
-
method: "POST",
|
|
334
|
-
headers: {
|
|
335
|
-
"Content-Type": "application/json",
|
|
336
|
-
...options?.headers,
|
|
337
|
-
},
|
|
338
|
-
body: JSON.stringify(input),
|
|
339
|
-
signal: options?.signal,
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
if (!response.ok) {
|
|
343
|
-
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
344
|
-
throw new Error(error.message || error.error || `HTTP ${response.status}`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return response.json();
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Make a raw request (for non-JSON endpoints like streaming).
|
|
352
|
-
* Returns the raw Response object without processing.
|
|
353
|
-
*/
|
|
354
|
-
protected async rawRequest(
|
|
355
|
-
route: string,
|
|
356
|
-
init?: RequestInit
|
|
357
|
-
): Promise<Response> {
|
|
358
|
-
const url = `${this.baseUrl}/${route}`;
|
|
359
|
-
const fetchFn = this.customFetch ?? fetch;
|
|
360
|
-
|
|
361
|
-
return fetchFn(url, {
|
|
362
|
-
method: "POST",
|
|
363
|
-
...init,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Make a stream request (validated input, Response output).
|
|
369
|
-
* For streaming, binary data, or custom content-type responses.
|
|
370
|
-
*
|
|
371
|
-
* By default uses POST with JSON body. For browser compatibility
|
|
372
|
-
* (video src, image src, download links), use streamUrl() instead.
|
|
373
|
-
*/
|
|
374
|
-
protected async streamRequest<TInput>(
|
|
375
|
-
route: string,
|
|
376
|
-
input: TInput,
|
|
377
|
-
options?: RequestOptions
|
|
378
|
-
): Promise<Response> {
|
|
379
|
-
const url = `${this.baseUrl}/${route}`;
|
|
380
|
-
const fetchFn = this.customFetch ?? fetch;
|
|
381
|
-
|
|
382
|
-
const response = await fetchFn(url, {
|
|
383
|
-
method: "POST",
|
|
384
|
-
headers: {
|
|
385
|
-
"Content-Type": "application/json",
|
|
386
|
-
...options?.headers,
|
|
387
|
-
},
|
|
388
|
-
body: JSON.stringify(input),
|
|
389
|
-
signal: options?.signal,
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// Unlike typed requests, we return the raw Response
|
|
393
|
-
// Error handling is left to the caller
|
|
394
|
-
return response;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Get the URL for a stream endpoint (for browser src attributes).
|
|
399
|
-
* Returns a URL with query params that can be used in:
|
|
400
|
-
* - <video src={url}>
|
|
401
|
-
* - <img src={url}>
|
|
402
|
-
* - <a href={url} download>
|
|
403
|
-
* - window.open(url)
|
|
404
|
-
*/
|
|
405
|
-
protected streamUrl<TInput>(route: string, input?: TInput): string {
|
|
406
|
-
let url = `${this.baseUrl}/${route}`;
|
|
407
|
-
|
|
408
|
-
if (input && typeof input === "object") {
|
|
409
|
-
const params = new URLSearchParams();
|
|
410
|
-
for (const [key, value] of Object.entries(input)) {
|
|
411
|
-
params.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
412
|
-
}
|
|
413
|
-
url += `?${params.toString()}`;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return url;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Fetch a stream via GET with query params.
|
|
421
|
-
* Alternative to streamRequest() for cases where GET is preferred.
|
|
422
|
-
*/
|
|
423
|
-
protected async streamGet<TInput>(
|
|
424
|
-
route: string,
|
|
425
|
-
input?: TInput,
|
|
426
|
-
options?: RequestOptions
|
|
427
|
-
): Promise<Response> {
|
|
428
|
-
const url = this.streamUrl(route, input);
|
|
429
|
-
const fetchFn = this.customFetch ?? fetch;
|
|
430
|
-
|
|
431
|
-
return fetchFn(url, {
|
|
432
|
-
method: "GET",
|
|
433
|
-
headers: options?.headers,
|
|
434
|
-
signal: options?.signal,
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Connect to an SSE endpoint.
|
|
440
|
-
* Returns a typed SSEConnection for handling server-sent events.
|
|
441
|
-
*/
|
|
442
|
-
protected sseConnect<TInput, TEvents extends Record<string, any> = Record<string, any>>(
|
|
443
|
-
route: string,
|
|
444
|
-
input?: TInput,
|
|
445
|
-
options?: SSEConnectionOptions
|
|
446
|
-
): SSEConnection<TEvents> {
|
|
447
|
-
let url = `${this.baseUrl}/${route}`;
|
|
448
|
-
|
|
449
|
-
// Add input as query params for GET request
|
|
450
|
-
if (input && typeof input === "object") {
|
|
451
|
-
const params = new URLSearchParams();
|
|
452
|
-
for (const [key, value] of Object.entries(input)) {
|
|
453
|
-
params.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
454
|
-
}
|
|
455
|
-
url += `?${params.toString()}`;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return new SSEConnection<TEvents>(url, options);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Connect to a specific SSE route endpoint.
|
|
463
|
-
* Alias for sseConnect() - provides compatibility with @donkeylabs/server generated clients.
|
|
464
|
-
* @returns SSE connection with typed event handlers
|
|
465
|
-
*/
|
|
466
|
-
protected connectToSSERoute<TEvents extends Record<string, any>>(
|
|
467
|
-
route: string,
|
|
468
|
-
input: Record<string, any> = {},
|
|
469
|
-
options?: Omit<SSEOptions, "endpoint" | "channels">
|
|
470
|
-
): SSEConnection<TEvents> {
|
|
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);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Make a formData request (file uploads with validated fields).
|
|
483
|
-
*/
|
|
484
|
-
protected async formDataRequest<TFields, TOutput>(
|
|
485
|
-
route: string,
|
|
486
|
-
fields: TFields,
|
|
487
|
-
files: File[],
|
|
488
|
-
options?: RequestOptions
|
|
489
|
-
): Promise<TOutput> {
|
|
490
|
-
const url = `${this.baseUrl}/${route}`;
|
|
491
|
-
const fetchFn = this.customFetch ?? fetch;
|
|
492
|
-
|
|
493
|
-
const formData = new FormData();
|
|
494
|
-
|
|
495
|
-
// Add fields
|
|
496
|
-
if (fields && typeof fields === "object") {
|
|
497
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
498
|
-
formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Add files
|
|
503
|
-
for (const file of files) {
|
|
504
|
-
formData.append("file", file);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const response = await fetchFn(url, {
|
|
508
|
-
method: "POST",
|
|
509
|
-
headers: options?.headers,
|
|
510
|
-
body: formData,
|
|
511
|
-
signal: options?.signal,
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
if (!response.ok) {
|
|
515
|
-
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
516
|
-
throw new Error(error.message || error.error || `HTTP ${response.status}`);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return response.json();
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Make an HTML request (returns HTML string).
|
|
524
|
-
*/
|
|
525
|
-
protected async htmlRequest<TInput>(
|
|
526
|
-
route: string,
|
|
527
|
-
input?: TInput,
|
|
528
|
-
options?: RequestOptions
|
|
529
|
-
): Promise<string> {
|
|
530
|
-
let url = `${this.baseUrl}/${route}`;
|
|
531
|
-
const fetchFn = this.customFetch ?? fetch;
|
|
532
|
-
|
|
533
|
-
// Add input as query params for GET request
|
|
534
|
-
if (input && typeof input === "object") {
|
|
535
|
-
const params = new URLSearchParams();
|
|
536
|
-
for (const [key, value] of Object.entries(input)) {
|
|
537
|
-
params.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
538
|
-
}
|
|
539
|
-
url += `?${params.toString()}`;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const response = await fetchFn(url, {
|
|
543
|
-
method: "GET",
|
|
544
|
-
headers: {
|
|
545
|
-
Accept: "text/html",
|
|
546
|
-
...options?.headers,
|
|
547
|
-
},
|
|
548
|
-
signal: options?.signal,
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
if (!response.ok) {
|
|
552
|
-
throw new Error(`HTTP ${response.status}`);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return response.text();
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* SSE (Server-Sent Events) subscription.
|
|
560
|
-
* Only works in the browser.
|
|
561
|
-
*/
|
|
562
|
-
sse = {
|
|
563
|
-
/**
|
|
564
|
-
* Subscribe to SSE channels.
|
|
565
|
-
* Returns a function to unsubscribe.
|
|
566
|
-
*
|
|
567
|
-
* @example
|
|
568
|
-
* const unsub = api.sse.subscribe(["notifications"], (event, data) => {
|
|
569
|
-
* console.log(event, data);
|
|
570
|
-
* });
|
|
571
|
-
* // Later: unsub();
|
|
572
|
-
*/
|
|
573
|
-
subscribe: (
|
|
574
|
-
channels: string[],
|
|
575
|
-
callback: (event: string, data: any) => void,
|
|
576
|
-
options?: { reconnect?: boolean }
|
|
577
|
-
): (() => void) => {
|
|
578
|
-
if (typeof window === "undefined") {
|
|
579
|
-
// SSR - return no-op
|
|
580
|
-
return () => {};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const url = `${this.baseUrl}/sse?channels=${channels.join(",")}`;
|
|
584
|
-
let eventSource: EventSource | null = new EventSource(url);
|
|
585
|
-
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
586
|
-
|
|
587
|
-
// Known event types from the server
|
|
588
|
-
const eventTypes = [
|
|
589
|
-
'cron-event', 'job-completed', 'internal-event', 'manual', 'message',
|
|
590
|
-
// Workflow events
|
|
591
|
-
'workflow.started', 'workflow.progress', 'workflow.completed',
|
|
592
|
-
'workflow.failed', 'workflow.cancelled',
|
|
593
|
-
'workflow.step.started', 'workflow.step.completed', 'workflow.step.failed',
|
|
594
|
-
];
|
|
595
|
-
|
|
596
|
-
const handleMessage = (e: MessageEvent) => {
|
|
597
|
-
try {
|
|
598
|
-
const data = JSON.parse(e.data);
|
|
599
|
-
callback(e.type || "message", data);
|
|
600
|
-
} catch {
|
|
601
|
-
callback(e.type || "message", e.data);
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
const handleError = () => {
|
|
606
|
-
if (options?.reconnect !== false && eventSource) {
|
|
607
|
-
eventSource.close();
|
|
608
|
-
reconnectTimeout = setTimeout(() => {
|
|
609
|
-
eventSource = new EventSource(url);
|
|
610
|
-
// Re-attach all listeners on reconnect
|
|
611
|
-
eventSource.onmessage = handleMessage;
|
|
612
|
-
eventSource.onerror = handleError;
|
|
613
|
-
for (const type of eventTypes) {
|
|
614
|
-
eventSource.addEventListener(type, handleMessage);
|
|
615
|
-
}
|
|
616
|
-
}, 1000);
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
|
|
620
|
-
// Listen for unnamed messages
|
|
621
|
-
eventSource.onmessage = handleMessage;
|
|
622
|
-
eventSource.onerror = handleError;
|
|
623
|
-
|
|
624
|
-
// Listen for named event types (SSE sends "event: type-name")
|
|
625
|
-
for (const type of eventTypes) {
|
|
626
|
-
eventSource.addEventListener(type, handleMessage);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Return unsubscribe function
|
|
630
|
-
return () => {
|
|
631
|
-
if (reconnectTimeout) {
|
|
632
|
-
clearTimeout(reconnectTimeout);
|
|
633
|
-
}
|
|
634
|
-
if (eventSource) {
|
|
635
|
-
eventSource.close();
|
|
636
|
-
eventSource = null;
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
},
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Create an API client instance.
|
|
645
|
-
* Call with locals and fetch in SSR, without in browser.
|
|
646
|
-
*
|
|
647
|
-
* @example
|
|
648
|
-
* // +page.server.ts (SSR)
|
|
649
|
-
* const api = createApiClient({ locals, fetch });
|
|
650
|
-
*
|
|
651
|
-
* // +page.svelte (browser)
|
|
652
|
-
* const api = createApiClient();
|
|
653
|
-
*/
|
|
654
|
-
export function createApiClient<T extends UnifiedApiClientBase>(
|
|
655
|
-
ClientClass: new (options?: ClientOptions) => T,
|
|
656
|
-
options?: ClientOptions
|
|
657
|
-
): T {
|
|
658
|
-
return new ClientClass(options);
|
|
659
|
-
}
|