@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 CHANGED
@@ -1,38 +1,39 @@
1
1
  {
2
2
  "name": "@donkeylabs/adapter-sveltekit",
3
- "version": "2.0.13",
3
+ "version": "2.0.15",
4
4
  "type": "module",
5
5
  "description": "SvelteKit adapter for @donkeylabs/server - seamless SSR/browser API integration",
6
- "main": "./src/index.ts",
7
- "types": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./src/index.ts",
11
- "import": "./src/index.ts"
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
12
  },
13
13
  "./client": {
14
- "types": "./src/client/index.ts",
15
- "import": "./src/client/index.ts"
14
+ "types": "./dist/client/index.d.ts",
15
+ "import": "./dist/client/index.js"
16
16
  },
17
17
  "./hooks": {
18
- "types": "./src/hooks/index.ts",
19
- "import": "./src/hooks/index.ts"
18
+ "types": "./dist/hooks/index.d.ts",
19
+ "import": "./dist/hooks/index.js"
20
20
  },
21
21
  "./generator": {
22
- "types": "./src/generator/index.ts",
23
- "import": "./src/generator/index.ts"
22
+ "types": "./dist/generator/index.d.ts",
23
+ "import": "./dist/generator/index.js"
24
24
  },
25
25
  "./vite": {
26
- "types": "./src/vite.ts",
27
- "import": "./src/vite.ts"
26
+ "types": "./dist/vite.d.ts",
27
+ "import": "./dist/vite.js"
28
28
  }
29
29
  },
30
30
  "files": [
31
- "src",
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": {
@@ -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
- }