@go-go-scope/adapter-svelte 2.7.0

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/src/index.ts ADDED
@@ -0,0 +1,609 @@
1
+ /**
2
+ * Svelte 5 runes integration for go-go-scope
3
+ *
4
+ * Provides native Svelte 5 runes ($state, $effect, $derived) integration.
5
+ * Scopes automatically clean up when components are destroyed.
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import { createScope, createTask } from "@go-go-scope/adapter-svelte";
11
+ *
12
+ * // Auto-disposing scope
13
+ * const scope = createScope({ name: "my-component" });
14
+ *
15
+ * // Reactive task
16
+ * const { data, isLoading } = createTask(
17
+ * async () => fetchUser(userId),
18
+ * { immediate: true }
19
+ * );
20
+ * </script>
21
+ *
22
+ * {#if $isLoading}
23
+ * <p>Loading...</p>
24
+ * {:else}
25
+ * <p>{$data.name}</p>
26
+ * {/if}
27
+ * ```
28
+ */
29
+
30
+ import {
31
+ scope,
32
+ BroadcastChannel,
33
+ type Scope,
34
+ type Result,
35
+ } from "go-go-scope";
36
+ import { onDestroy } from "svelte";
37
+ import { writable, type Readable } from "svelte/store";
38
+
39
+ // ============================================================================
40
+ // Core - Scope
41
+ // ============================================================================
42
+
43
+ /**
44
+ * Options for creating a scope
45
+ */
46
+ export interface CreateScopeOptions {
47
+ /** Scope name for debugging */
48
+ name?: string;
49
+ /** Timeout in milliseconds */
50
+ timeout?: number;
51
+ }
52
+
53
+ /**
54
+ * Reactive scope with automatic cleanup
55
+ */
56
+ export interface ReactiveScope extends Scope {
57
+ /** Whether the scope is currently active */
58
+ readonly isActive: Readable<boolean>;
59
+ }
60
+
61
+ /**
62
+ * Create a reactive scope that automatically disposes on component destroy.
63
+ *
64
+ * @example
65
+ * ```svelte
66
+ * <script>
67
+ * const s = createScope({ name: "my-component" });
68
+ *
69
+ * // Scope is automatically disposed when component is destroyed
70
+ * const [err, result] = await s.task(() => fetchData());
71
+ * </script>
72
+ * ```
73
+ */
74
+ export function createScope(options: CreateScopeOptions = {}): ReactiveScope {
75
+ const isActive = writable(true);
76
+
77
+ const s = scope({
78
+ name: options.name,
79
+ timeout: options.timeout,
80
+ });
81
+
82
+ // Auto-dispose on component destroy
83
+ onDestroy(() => {
84
+ isActive.set(false);
85
+ s[Symbol.asyncDispose]().catch(() => {
86
+ // Ignore disposal errors
87
+ });
88
+ });
89
+
90
+ return Object.assign(s as unknown as ReactiveScope, {
91
+ isActive: { subscribe: isActive.subscribe },
92
+ });
93
+ }
94
+
95
+ // ============================================================================
96
+ // Task Store
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Options for creating a task
101
+ */
102
+ export interface CreateTaskOptions<T> {
103
+ /** Task name for debugging */
104
+ name?: string;
105
+ /** Whether to execute immediately */
106
+ immediate?: boolean;
107
+ /** Timeout in milliseconds */
108
+ timeout?: number;
109
+ /** Retry options */
110
+ retry?: { maxRetries?: number; delay?: number };
111
+ /** Initial data value */
112
+ initialData?: T;
113
+ }
114
+
115
+ /**
116
+ * Reactive task state
117
+ */
118
+ export interface TaskState<T> {
119
+ /** Current data (undefined if not loaded) */
120
+ readonly data: Readable<T | undefined>;
121
+ /** Error if task failed */
122
+ readonly error: Readable<Error | undefined>;
123
+ /** Whether task is currently running */
124
+ readonly isLoading: Readable<boolean>;
125
+ /** Whether task has been executed */
126
+ readonly isReady: Readable<boolean>;
127
+ /** Execute the task */
128
+ readonly execute: () => Promise<Result<Error, T>>;
129
+ }
130
+
131
+ /**
132
+ * Create a reactive task that integrates with Svelte's store system.
133
+ * Returns a store-like object that can be used with `$` prefix.
134
+ *
135
+ * @example
136
+ * ```svelte
137
+ * <script>
138
+ * const task = createTask(
139
+ * async () => fetchUser(userId),
140
+ * { immediate: true }
141
+ * );
142
+ * </script>
143
+ *
144
+ * {#if $task.isLoading}
145
+ * <p>Loading...</p>
146
+ * {:else if $task.error}
147
+ * <p>Error: {$task.error.message}</p>
148
+ * {:else}
149
+ * <p>{$task.data.name}</p>
150
+ * {/if}
151
+ * ```
152
+ */
153
+ export function createTask<T>(
154
+ factory: () => Promise<T>,
155
+ options: CreateTaskOptions<T> = {},
156
+ ): TaskState<T> {
157
+ const s = createScope({ name: options.name ?? "createTask" });
158
+
159
+ const data = writable<T | undefined>(options.initialData);
160
+ const error = writable<Error | undefined>(undefined);
161
+ const isLoading = writable(false);
162
+ const isReady = writable(false);
163
+
164
+ const execute = async (): Promise<Result<Error, T>> => {
165
+ isLoading.set(true);
166
+ error.set(undefined);
167
+
168
+ try {
169
+ const [err, result] = await s.task(
170
+ async () => await factory(),
171
+ {
172
+ timeout: options.timeout,
173
+ retry: options.retry,
174
+ },
175
+ );
176
+
177
+ if (err) {
178
+ error.set(err instanceof Error ? err : new Error(String(err)));
179
+ data.set(undefined);
180
+ return [err as Error, undefined];
181
+ }
182
+
183
+ data.set(result);
184
+ isReady.set(true);
185
+ return [undefined, result];
186
+ } finally {
187
+ isLoading.set(false);
188
+ }
189
+ };
190
+
191
+ // Auto-execute if immediate
192
+ if (options.immediate) {
193
+ execute();
194
+ }
195
+
196
+ return {
197
+ data: { subscribe: data.subscribe },
198
+ error: { subscribe: error.subscribe },
199
+ isLoading: { subscribe: isLoading.subscribe },
200
+ isReady: { subscribe: isReady.subscribe },
201
+ execute,
202
+ };
203
+ }
204
+
205
+ // ============================================================================
206
+ // Parallel Tasks Store
207
+ // ============================================================================
208
+
209
+ /**
210
+ * Options for parallel execution
211
+ */
212
+ export interface CreateParallelOptions {
213
+ /** Concurrency limit */
214
+ concurrency?: number;
215
+ /** Whether to execute immediately */
216
+ immediate?: boolean;
217
+ }
218
+
219
+ /**
220
+ * Reactive parallel task state
221
+ */
222
+ export interface ParallelState<T> {
223
+ /** Results from all tasks */
224
+ readonly results: Readable<(T | undefined)[]>;
225
+ /** Errors from failed tasks */
226
+ readonly errors: Readable<(Error | undefined)[]>;
227
+ /** Whether any task is running */
228
+ readonly isLoading: Readable<boolean>;
229
+ /** Progress percentage (0-100) */
230
+ readonly progress: Readable<number>;
231
+ /** Execute all tasks */
232
+ readonly execute: () => Promise<Result<Error, T>[]>;
233
+ }
234
+
235
+ /**
236
+ * Create reactive parallel task execution.
237
+ *
238
+ * @example
239
+ * ```svelte
240
+ * <script>
241
+ * const parallel = createParallel(
242
+ * urls.map(url => () => fetch(url).then(r => r.json())),
243
+ * { concurrency: 3, immediate: true }
244
+ * );
245
+ * </script>
246
+ *
247
+ * <progress value={$parallel.progress} max="100" />
248
+ * ```
249
+ */
250
+ export function createParallel<T>(
251
+ factories: (() => Promise<T>)[],
252
+ options: CreateParallelOptions = {},
253
+ ): ParallelState<T> {
254
+ const s = createScope({ name: "createParallel" });
255
+
256
+ const results = writable<(T | undefined)[]>([]);
257
+ const errors = writable<(Error | undefined)[]>([]);
258
+ const isLoading = writable(false);
259
+ const progress = writable(0);
260
+
261
+ const execute = async (): Promise<Result<Error, T>[]> => {
262
+ isLoading.set(true);
263
+ progress.set(0);
264
+ results.set(new Array(factories.length).fill(undefined));
265
+ errors.set(new Array(factories.length).fill(undefined));
266
+
267
+ try {
268
+ const taskFactories = factories.map((factory, index) => {
269
+ return async () => {
270
+ const [err, result] = await s.task(async () => await factory());
271
+
272
+ if (err) {
273
+ errors.update(e => {
274
+ e[index] = err instanceof Error ? err : new Error(String(err));
275
+ return e;
276
+ });
277
+ } else {
278
+ results.update(r => {
279
+ r[index] = result;
280
+ return r;
281
+ });
282
+ }
283
+
284
+ progress.set(Math.round(((index + 1) / factories.length) * 100));
285
+ return [err, result] as Result<Error, T>;
286
+ };
287
+ });
288
+
289
+ return (await s.parallel(taskFactories, {
290
+ concurrency: options.concurrency,
291
+ })) as Result<Error, T>[];
292
+ } finally {
293
+ isLoading.set(false);
294
+ }
295
+ };
296
+
297
+ if (options.immediate) {
298
+ execute();
299
+ }
300
+
301
+ return {
302
+ results: { subscribe: results.subscribe },
303
+ errors: { subscribe: errors.subscribe },
304
+ isLoading: { subscribe: isLoading.subscribe },
305
+ progress: { subscribe: progress.subscribe },
306
+ execute,
307
+ };
308
+ }
309
+
310
+ // ============================================================================
311
+ // Channel Store
312
+ // ============================================================================
313
+
314
+ /**
315
+ * Options for creating a channel
316
+ */
317
+ export interface CreateChannelOptions {
318
+ /** Buffer size */
319
+ bufferSize?: number;
320
+ /** Maximum history to keep */
321
+ historySize?: number;
322
+ }
323
+
324
+ /**
325
+ * Reactive channel state
326
+ */
327
+ export interface ChannelState<T> {
328
+ /** Latest received value */
329
+ readonly latest: Readable<T | undefined>;
330
+ /** History of values */
331
+ readonly history: Readable<readonly T[]>;
332
+ /** Whether channel is closed */
333
+ readonly isClosed: Readable<boolean>;
334
+ /** Send a value */
335
+ readonly send: (value: T) => Promise<void>;
336
+ /** Close the channel */
337
+ readonly close: () => void;
338
+ }
339
+
340
+ /**
341
+ * Create a reactive channel for Go-style communication.
342
+ *
343
+ * @example
344
+ * ```svelte
345
+ * <script>
346
+ * const ch = createChannel<string>();
347
+ *
348
+ * async function sendMessage() {
349
+ * await ch.send("Hello!");
350
+ * }
351
+ * </script>
352
+ *
353
+ * <p>Latest: {$ch.latest}</p>
354
+ * <button onclick={sendMessage}>Send</button>
355
+ * ```
356
+ */
357
+ export function createChannel<T>(options: CreateChannelOptions = {}): ChannelState<T> {
358
+ const s = createScope({ name: "createChannel" });
359
+ const ch = s.channel<T>(options.bufferSize ?? 0);
360
+
361
+ const latest = writable<T | undefined>(undefined);
362
+ const history = writable<T[]>([]);
363
+ const isClosed = writable(false);
364
+
365
+ // Start receiving in background
366
+ const receiveLoop = async () => {
367
+ for await (const value of ch) {
368
+ latest.set(value);
369
+ history.update(h => [...h.slice(-(options.historySize ?? 100)), value]);
370
+ }
371
+ isClosed.set(true);
372
+ };
373
+
374
+ receiveLoop();
375
+
376
+ return {
377
+ latest: { subscribe: latest.subscribe },
378
+ history: { subscribe: history.subscribe },
379
+ isClosed: { subscribe: isClosed.subscribe },
380
+ send: async (value: T) => {
381
+ await ch.send(value);
382
+ },
383
+ close: () => {
384
+ ch.close();
385
+ },
386
+ };
387
+ }
388
+
389
+ // ============================================================================
390
+ // Broadcast Store
391
+ // ============================================================================
392
+
393
+ /**
394
+ * Reactive broadcast state
395
+ */
396
+ export interface BroadcastState<T> {
397
+ /** Latest broadcasted value */
398
+ readonly latest: Readable<T | undefined>;
399
+ /** Subscribe to broadcasts */
400
+ readonly subscribe: (callback: (value: T) => void) => { unsubscribe: () => void };
401
+ /** Broadcast a value */
402
+ readonly broadcast: (value: T) => void;
403
+ }
404
+
405
+ /**
406
+ * Create a reactive broadcast channel.
407
+ *
408
+ * @example
409
+ * ```svelte
410
+ * <script>
411
+ * const bus = createBroadcast<string>();
412
+ *
413
+ * // Subscribe
414
+ * bus.subscribe(msg => console.log(msg));
415
+ *
416
+ * function send() {
417
+ * bus.broadcast("Hello!");
418
+ * }
419
+ * </script>
420
+ *
421
+ * <p>Latest: {$bus.latest}</p>
422
+ * ```
423
+ */
424
+ export function createBroadcast<T>(): BroadcastState<T> {
425
+ const bc = new BroadcastChannel<T>();
426
+
427
+ const latest = writable<T | undefined>(undefined);
428
+
429
+ return {
430
+ latest: { subscribe: latest.subscribe },
431
+ subscribe: (callback: (value: T) => void) => {
432
+ // Start async iterator to receive broadcasts
433
+ (async () => {
434
+ for await (const value of bc.subscribe()) {
435
+ latest.set(value);
436
+ callback(value);
437
+ }
438
+ })();
439
+
440
+ return {
441
+ unsubscribe: () => bc.close(),
442
+ };
443
+ },
444
+ broadcast: async (value: T) => {
445
+ await bc.send(value);
446
+ latest.set(value);
447
+ },
448
+ };
449
+ }
450
+
451
+ // ============================================================================
452
+ // Polling Store
453
+ // ============================================================================
454
+
455
+ /**
456
+ * Options for polling
457
+ */
458
+ export interface CreatePollingOptions {
459
+ /** Interval in milliseconds */
460
+ interval: number;
461
+ /** Whether to start immediately */
462
+ immediate?: boolean;
463
+ /** Continue when tab is hidden */
464
+ continueOnHidden?: boolean;
465
+ }
466
+
467
+ /**
468
+ * Reactive polling state
469
+ */
470
+ export interface PollingState<T> {
471
+ /** Latest data */
472
+ readonly data: Readable<T | undefined>;
473
+ /** Error if polling failed */
474
+ readonly error: Readable<Error | undefined>;
475
+ /** Whether currently polling */
476
+ readonly isPolling: Readable<boolean>;
477
+ /** Number of polls completed */
478
+ readonly pollCount: Readable<number>;
479
+ /** Start polling */
480
+ readonly start: () => void;
481
+ /** Stop polling */
482
+ readonly stop: () => void;
483
+ }
484
+
485
+ /**
486
+ * Create a reactive polling mechanism.
487
+ *
488
+ * @example
489
+ * ```svelte
490
+ * <script>
491
+ * const poller = createPolling(
492
+ * async () => fetchLatestData(),
493
+ * { interval: 5000, immediate: true }
494
+ * );
495
+ * </script>
496
+ *
497
+ * {#if $poller.data}
498
+ * <p>{$poller.data}</p>
499
+ * {/if}
500
+ * <button onclick={() => $poller.stop()}>Stop</button>
501
+ * ```
502
+ */
503
+ export function createPolling<T>(
504
+ factory: () => Promise<T>,
505
+ options: CreatePollingOptions,
506
+ ): PollingState<T> {
507
+ const s = createScope({ name: "createPolling" });
508
+
509
+ const data = writable<T | undefined>(undefined);
510
+ const error = writable<Error | undefined>(undefined);
511
+ const isPolling = writable(false);
512
+ const pollCount = writable(0);
513
+
514
+ // Create the poller with onValue callback
515
+ const poller = s.poll(
516
+ async () => {
517
+ try {
518
+ const result = await factory();
519
+ return { success: true, value: result } as const;
520
+ } catch (e) {
521
+ return { success: false, error: e } as const;
522
+ }
523
+ },
524
+ (result) => {
525
+ pollCount.update(c => c + 1);
526
+ if (result.success) {
527
+ data.set(result.value);
528
+ } else {
529
+ error.set(result.error instanceof Error ? result.error : new Error(String(result.error)));
530
+ }
531
+ },
532
+ { interval: options.interval, immediate: options.immediate }
533
+ );
534
+
535
+ // Sync isPolling with poller status
536
+ const updateStatus = () => {
537
+ const status = poller.status();
538
+ isPolling.set(status.running);
539
+ };
540
+
541
+ // Handle visibility change
542
+ if (!options.continueOnHidden && typeof document !== "undefined") {
543
+ const handler = () => {
544
+ if (document.hidden) {
545
+ poller.stop();
546
+ updateStatus();
547
+ } else if (options.immediate !== false) {
548
+ poller.start();
549
+ updateStatus();
550
+ }
551
+ };
552
+
553
+ document.addEventListener("visibilitychange", handler);
554
+ }
555
+
556
+ // Set initial polling state
557
+ updateStatus();
558
+
559
+ return {
560
+ data: { subscribe: data.subscribe },
561
+ error: { subscribe: error.subscribe },
562
+ isPolling: { subscribe: isPolling.subscribe },
563
+ pollCount: { subscribe: pollCount.subscribe },
564
+ start: () => {
565
+ poller.start();
566
+ updateStatus();
567
+ },
568
+ stop: () => {
569
+ poller.stop();
570
+ updateStatus();
571
+ },
572
+ };
573
+ }
574
+
575
+ // ============================================================================
576
+ // Store-like Interface (Svelte 5 compatible)
577
+ // ============================================================================
578
+
579
+ /**
580
+ * Create a reactive value that can be subscribed to (Svelte store contract).
581
+ * Compatible with Svelte 5's `$` prefix.
582
+ *
583
+ * @example
584
+ * ```svelte
585
+ * <script>
586
+ * const count = createStore(0);
587
+ *
588
+ * function increment() {
589
+ * $count++;
590
+ * }
591
+ * </script>
592
+ *
593
+ * <button onclick={increment}>
594
+ * Count: {$count}
595
+ * </button>
596
+ * ```
597
+ */
598
+ export function createStore<T>(initialValue: T) {
599
+ const { subscribe, set, update } = writable(initialValue);
600
+
601
+ return {
602
+ subscribe,
603
+ set,
604
+ update,
605
+ };
606
+ }
607
+
608
+ // Re-export types
609
+ export type { Result, Scope, Channel, BroadcastChannel } from "go-go-scope";