rexfect 0.0.7 → 0.2.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.
Files changed (51) hide show
  1. package/dist/async/abortable.d.ts +44 -0
  2. package/dist/async/abortable.d.ts.map +1 -1
  3. package/dist/async/abortable.js +44 -7
  4. package/dist/async/abortable.js.map +1 -1
  5. package/dist/async/index.d.ts +1 -1
  6. package/dist/async/index.d.ts.map +1 -1
  7. package/dist/async/index.js +1 -1
  8. package/dist/async/index.js.map +1 -1
  9. package/dist/async/wrappers.d.ts +311 -0
  10. package/dist/async/wrappers.d.ts.map +1 -0
  11. package/dist/async/wrappers.js +542 -0
  12. package/dist/async/wrappers.js.map +1 -0
  13. package/dist/async/wrappers.test.d.ts +2 -0
  14. package/dist/async/wrappers.test.d.ts.map +1 -0
  15. package/dist/async/wrappers.test.js +649 -0
  16. package/dist/async/wrappers.test.js.map +1 -0
  17. package/dist/atom.d.ts +5 -0
  18. package/dist/atom.d.ts.map +1 -1
  19. package/dist/atom.js +11 -3
  20. package/dist/atom.js.map +1 -1
  21. package/dist/batch.js +1 -1
  22. package/dist/batch.js.map +1 -1
  23. package/dist/effect.d.ts.map +1 -1
  24. package/dist/effect.js +3 -2
  25. package/dist/effect.js.map +1 -1
  26. package/dist/emitter.d.ts +2 -2
  27. package/dist/emitter.d.ts.map +1 -1
  28. package/dist/emitter.js +20 -23
  29. package/dist/emitter.js.map +1 -1
  30. package/dist/emitter.test.js +11 -11
  31. package/dist/emitter.test.js.map +1 -1
  32. package/dist/pick.js +3 -2
  33. package/dist/pick.js.map +1 -1
  34. package/dist/react/useRx.d.ts.map +1 -1
  35. package/dist/react/useRx.js +75 -43
  36. package/dist/react/useRx.js.map +1 -1
  37. package/dist/types.d.ts +1 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/event.d.ts +0 -18
  41. package/dist/event.d.ts.map +0 -1
  42. package/dist/event.js +0 -166
  43. package/dist/event.js.map +0 -1
  44. package/dist/event.test.d.ts +0 -2
  45. package/dist/event.test.d.ts.map +0 -1
  46. package/dist/event.test.js +0 -167
  47. package/dist/event.test.js.map +0 -1
  48. package/dist/utils.d.ts +0 -7
  49. package/dist/utils.d.ts.map +0 -1
  50. package/dist/utils.js +0 -7
  51. package/dist/utils.js.map +0 -1
@@ -0,0 +1,649 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { abortable } from "./abortable";
3
+ import { retry, catchError, timeout, debounce, throttle, fallback, cache, rateLimit, circuitBreaker, observe, } from "./wrappers";
4
+ describe("wrappers", () => {
5
+ describe("retry()", () => {
6
+ it("should retry on failure with default retries", async () => {
7
+ let attempts = 0;
8
+ const fn = abortable(async () => {
9
+ attempts++;
10
+ if (attempts < 3) {
11
+ throw new Error("fail");
12
+ }
13
+ return "success";
14
+ }).use(retry(3));
15
+ const result = await fn();
16
+ expect(result).toBe("success");
17
+ expect(attempts).toBe(3);
18
+ });
19
+ it("should fail after all retries exhausted", async () => {
20
+ let attempts = 0;
21
+ const fn = abortable(async () => {
22
+ attempts++;
23
+ throw new Error("always fail");
24
+ }).use(retry(2));
25
+ await expect(fn()).rejects.toThrow("always fail");
26
+ expect(attempts).toBe(2);
27
+ });
28
+ it("should use custom delay", async () => {
29
+ let attempts = 0;
30
+ const timestamps = [];
31
+ const fn = abortable(async () => {
32
+ timestamps.push(Date.now());
33
+ attempts++;
34
+ if (attempts < 2) {
35
+ throw new Error("fail");
36
+ }
37
+ return "success";
38
+ }).use(retry({ retries: 2, delay: () => 50 }));
39
+ await fn();
40
+ expect(attempts).toBe(2);
41
+ expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(45);
42
+ });
43
+ it("should use strategy name for delay", async () => {
44
+ let attempts = 0;
45
+ const fn = abortable(async () => {
46
+ attempts++;
47
+ if (attempts < 2) {
48
+ throw new Error("fail");
49
+ }
50
+ return "done";
51
+ }).use(retry({ retries: 2, delay: "immediate" }));
52
+ const result = await fn();
53
+ expect(result).toBe("done");
54
+ expect(attempts).toBe(2);
55
+ });
56
+ it("should not retry if cancelled", async () => {
57
+ let attempts = 0;
58
+ const controller = new AbortController();
59
+ const fn = abortable(async ({ signal }) => {
60
+ attempts++;
61
+ if (signal.aborted)
62
+ throw new Error("Aborted");
63
+ throw new Error("fail");
64
+ }).use(retry(3));
65
+ // When parent signal is already aborted, abortable rejects immediately
66
+ // without executing the function (correct behavior)
67
+ controller.abort();
68
+ await expect(fn.withSignal(controller.signal)).rejects.toThrow();
69
+ expect(attempts).toBe(0); // Never executed because already aborted
70
+ });
71
+ it("should cancel delay when aborted", async () => {
72
+ let attempts = 0;
73
+ const controller = new AbortController();
74
+ const fn = abortable(async () => {
75
+ attempts++;
76
+ throw new Error("fail");
77
+ }).use(retry({ retries: 3, delay: 5000 })); // Long delay
78
+ const promise = fn.withSignal(controller.signal);
79
+ // Abort after first attempt
80
+ setTimeout(() => controller.abort(), 50);
81
+ const start = Date.now();
82
+ await expect(promise).rejects.toThrow();
83
+ const elapsed = Date.now() - start;
84
+ // Should abort quickly, not wait for 5000ms delay
85
+ expect(elapsed).toBeLessThan(500);
86
+ expect(attempts).toBe(1);
87
+ });
88
+ it("should accept strategy name as first argument", async () => {
89
+ let attempts = 0;
90
+ const fn = abortable(async () => {
91
+ attempts++;
92
+ if (attempts < 2)
93
+ throw new Error("fail");
94
+ return "done";
95
+ }).use(retry("immediate"));
96
+ const result = await fn();
97
+ expect(result).toBe("done");
98
+ expect(attempts).toBe(2);
99
+ });
100
+ });
101
+ describe("catchError()", () => {
102
+ it("should call callback on error", async () => {
103
+ const errorCallback = vi.fn();
104
+ const fn = abortable(async () => {
105
+ throw new Error("test error");
106
+ }).use(catchError(errorCallback));
107
+ await expect(fn()).rejects.toThrow("test error");
108
+ expect(errorCallback).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({ signal: expect.any(AbortSignal) }));
109
+ });
110
+ it("should pass args to callback", async () => {
111
+ const errorCallback = vi.fn();
112
+ const fn = abortable(async ({}, _id, _count) => {
113
+ throw new Error("fail");
114
+ }).use(catchError(errorCallback));
115
+ await expect(fn("user-1", 42)).rejects.toThrow("fail");
116
+ expect(errorCallback).toHaveBeenCalledWith(expect.any(Error), expect.anything(), "user-1", 42);
117
+ });
118
+ it("should not call callback on success", async () => {
119
+ const errorCallback = vi.fn();
120
+ const fn = abortable(async () => "success").use(catchError(errorCallback));
121
+ await fn();
122
+ expect(errorCallback).not.toHaveBeenCalled();
123
+ });
124
+ });
125
+ describe("timeout()", () => {
126
+ it("should throw if operation takes too long", async () => {
127
+ const fn = abortable(async () => {
128
+ await new Promise((r) => setTimeout(r, 100));
129
+ return "done";
130
+ }).use(timeout(30));
131
+ await expect(fn()).rejects.toThrow("Operation timed out");
132
+ });
133
+ it("should succeed if operation completes in time", async () => {
134
+ const fn = abortable(async () => {
135
+ await new Promise((r) => setTimeout(r, 10));
136
+ return "success";
137
+ }).use(timeout(100));
138
+ const result = await fn();
139
+ expect(result).toBe("success");
140
+ });
141
+ it("should use custom error message", async () => {
142
+ const fn = abortable(async () => {
143
+ await new Promise((r) => setTimeout(r, 100));
144
+ return "done";
145
+ }).use(timeout(30, "Request took too long"));
146
+ await expect(fn()).rejects.toThrow("Request took too long");
147
+ });
148
+ });
149
+ describe("debounce()", () => {
150
+ it("should only execute after delay with no new calls", async () => {
151
+ let callCount = 0;
152
+ const fn = abortable(async () => {
153
+ callCount++;
154
+ return callCount;
155
+ }).use(debounce(50));
156
+ // Start multiple calls rapidly
157
+ fn();
158
+ fn();
159
+ const p3 = fn();
160
+ // Only the last one should execute after debounce
161
+ await p3;
162
+ expect(callCount).toBe(1);
163
+ });
164
+ });
165
+ describe("throttle()", () => {
166
+ it("should only execute once per time window", async () => {
167
+ let callCount = 0;
168
+ const fn = abortable(async () => {
169
+ callCount++;
170
+ return callCount;
171
+ }).use(throttle(100));
172
+ // First call executes
173
+ const r1 = await fn();
174
+ expect(r1).toBe(1);
175
+ // Second call within window returns same result
176
+ const r2 = await fn();
177
+ expect(r2).toBe(1);
178
+ expect(callCount).toBe(1);
179
+ // Wait for throttle window to pass
180
+ await new Promise((r) => setTimeout(r, 110));
181
+ // Third call after window executes
182
+ const r3 = await fn();
183
+ expect(r3).toBe(2);
184
+ expect(callCount).toBe(2);
185
+ });
186
+ });
187
+ describe("fallback()", () => {
188
+ it("should return fallback value on error", async () => {
189
+ const fn = abortable(async () => {
190
+ throw new Error("fail");
191
+ }).use(fallback("default"));
192
+ const result = await fn();
193
+ expect(result).toBe("default");
194
+ });
195
+ it("should return null fallback on error", async () => {
196
+ const fn = abortable(async () => {
197
+ throw new Error("fail");
198
+ }).use(fallback(null));
199
+ const result = await fn();
200
+ expect(result).toBe(null);
201
+ });
202
+ it("should return empty array fallback on error", async () => {
203
+ const fn = abortable(async () => {
204
+ throw new Error("fail");
205
+ }).use(fallback([]));
206
+ const result = await fn();
207
+ expect(result).toEqual([]);
208
+ });
209
+ it("should not use fallback on success", async () => {
210
+ const fn = abortable(async () => "success").use(fallback("default"));
211
+ const result = await fn();
212
+ expect(result).toBe("success");
213
+ });
214
+ it("should support dynamic fallback function", async () => {
215
+ const fn = abortable(async ({}, _id) => {
216
+ throw new Error("not found");
217
+ }).use(fallback((error, _ctx, id) => ({ error: error.message, id })));
218
+ const result = await fn("user-123");
219
+ expect(result).toEqual({ error: "not found", id: "user-123" });
220
+ });
221
+ it("should propagate abort errors (not fallback)", async () => {
222
+ const controller = new AbortController();
223
+ const fn = abortable(async ({ signal }) => {
224
+ if (signal.aborted)
225
+ throw new Error("Aborted");
226
+ return "success";
227
+ }).use(fallback("default"));
228
+ controller.abort();
229
+ await expect(fn.withSignal(controller.signal)).rejects.toThrow("aborted");
230
+ });
231
+ });
232
+ describe("cache()", () => {
233
+ it("should cache results for TTL duration", async () => {
234
+ let callCount = 0;
235
+ const fn = abortable(async ({}, id) => {
236
+ callCount++;
237
+ return `result-${id}-${callCount}`;
238
+ }).use(cache(1000));
239
+ const r1 = await fn("user-1");
240
+ const r2 = await fn("user-1");
241
+ expect(r1).toBe("result-user-1-1");
242
+ expect(r2).toBe("result-user-1-1"); // Cached
243
+ expect(callCount).toBe(1);
244
+ });
245
+ it("should cache different keys separately", async () => {
246
+ let callCount = 0;
247
+ const fn = abortable(async ({}, id) => {
248
+ callCount++;
249
+ return `result-${id}`;
250
+ }).use(cache(1000));
251
+ await fn("user-1");
252
+ await fn("user-2");
253
+ await fn("user-1"); // Should be cached
254
+ expect(callCount).toBe(2);
255
+ });
256
+ it("should expire cache after TTL", async () => {
257
+ let callCount = 0;
258
+ const fn = abortable(async () => {
259
+ callCount++;
260
+ return callCount;
261
+ }).use(cache(50));
262
+ await fn();
263
+ expect(callCount).toBe(1);
264
+ await new Promise((r) => setTimeout(r, 60));
265
+ await fn();
266
+ expect(callCount).toBe(2);
267
+ });
268
+ it("should use custom key function", async () => {
269
+ let callCount = 0;
270
+ const fn = abortable(async ({}, user) => {
271
+ callCount++;
272
+ return user.name;
273
+ }).use(cache({ ttl: 1000, key: (user) => user.id }));
274
+ await fn({ id: "1", name: "John" });
275
+ await fn({ id: "1", name: "Jane" }); // Same ID, should be cached
276
+ expect(callCount).toBe(1);
277
+ });
278
+ });
279
+ describe("rateLimit()", () => {
280
+ it("should allow calls within limit", async () => {
281
+ let callCount = 0;
282
+ const fn = abortable(async () => {
283
+ callCount++;
284
+ return callCount;
285
+ }).use(rateLimit({ limit: 3, window: 1000 }));
286
+ const results = await Promise.all([fn(), fn(), fn()]);
287
+ expect(results).toEqual([1, 2, 3]);
288
+ expect(callCount).toBe(3);
289
+ });
290
+ it("should queue calls beyond limit", async () => {
291
+ let callCount = 0;
292
+ const fn = abortable(async () => {
293
+ callCount++;
294
+ return callCount;
295
+ }).use(rateLimit({ limit: 2, window: 50 }));
296
+ // Start 3 calls - first 2 execute immediately, 3rd queued
297
+ const p1 = fn();
298
+ const p2 = fn();
299
+ const p3 = fn();
300
+ const r1 = await p1;
301
+ const r2 = await p2;
302
+ expect(r1).toBe(1);
303
+ expect(r2).toBe(2);
304
+ // Third should execute after window
305
+ const r3 = await p3;
306
+ expect(r3).toBe(3);
307
+ });
308
+ it("should handle abort while queued", async () => {
309
+ const controller = new AbortController();
310
+ const fn = abortable(async () => {
311
+ await new Promise((r) => setTimeout(r, 10));
312
+ return "done";
313
+ }).use(rateLimit({ limit: 1, window: 1000 }));
314
+ // First call takes the slot
315
+ const p1 = fn();
316
+ // Second call gets queued
317
+ const p2 = fn.withSignal(controller.signal);
318
+ // Abort the queued call
319
+ controller.abort();
320
+ await expect(p2).rejects.toThrow();
321
+ expect(await p1).toBe("done");
322
+ });
323
+ });
324
+ describe("circuitBreaker()", () => {
325
+ it("should allow calls when circuit is closed", async () => {
326
+ const fn = abortable(async () => "success").use(circuitBreaker());
327
+ const result = await fn();
328
+ expect(result).toBe("success");
329
+ });
330
+ it("should open circuit after threshold failures", async () => {
331
+ let callCount = 0;
332
+ const fn = abortable(async () => {
333
+ callCount++;
334
+ throw new Error("fail");
335
+ }).use(circuitBreaker({ threshold: 3 }));
336
+ // 3 failures to trip the circuit
337
+ await expect(fn()).rejects.toThrow("fail");
338
+ await expect(fn()).rejects.toThrow("fail");
339
+ await expect(fn()).rejects.toThrow("fail");
340
+ expect(callCount).toBe(3);
341
+ // Circuit is now open - should fail fast
342
+ await expect(fn()).rejects.toThrow("Circuit breaker is open");
343
+ expect(callCount).toBe(3); // No additional call
344
+ });
345
+ it("should transition to half-open after reset timeout", async () => {
346
+ let callCount = 0;
347
+ let shouldFail = true;
348
+ const fn = abortable(async () => {
349
+ callCount++;
350
+ if (shouldFail)
351
+ throw new Error("fail");
352
+ return "recovered";
353
+ }).use(circuitBreaker({ threshold: 2, resetTimeout: 50 }));
354
+ // Trip the circuit
355
+ await expect(fn()).rejects.toThrow("fail");
356
+ await expect(fn()).rejects.toThrow("fail");
357
+ // Circuit open
358
+ await expect(fn()).rejects.toThrow("Circuit breaker is open");
359
+ // Wait for reset timeout
360
+ await new Promise((r) => setTimeout(r, 60));
361
+ // Now in half-open, allow one request
362
+ shouldFail = false;
363
+ const result = await fn();
364
+ expect(result).toBe("recovered");
365
+ });
366
+ it("should re-open circuit if half-open test fails", async () => {
367
+ let callCount = 0;
368
+ const fn = abortable(async () => {
369
+ callCount++;
370
+ throw new Error("still broken");
371
+ }).use(circuitBreaker({ threshold: 2, resetTimeout: 50 }));
372
+ // Trip the circuit
373
+ await expect(fn()).rejects.toThrow("still broken");
374
+ await expect(fn()).rejects.toThrow("still broken");
375
+ // Wait for reset timeout
376
+ await new Promise((r) => setTimeout(r, 60));
377
+ // Half-open test fails
378
+ await expect(fn()).rejects.toThrow("still broken");
379
+ // Should be open again
380
+ await expect(fn()).rejects.toThrow("Circuit breaker is open");
381
+ });
382
+ it("should not count aborts as failures", async () => {
383
+ let callCount = 0;
384
+ const controller = new AbortController();
385
+ const fn = abortable(async ({ signal }) => {
386
+ callCount++;
387
+ if (signal.aborted)
388
+ throw new Error("Aborted");
389
+ return "success";
390
+ }).use(circuitBreaker({ threshold: 2 }));
391
+ controller.abort();
392
+ // Aborts shouldn't count toward threshold
393
+ await expect(fn.withSignal(controller.signal)).rejects.toThrow("aborted");
394
+ await expect(fn.withSignal(controller.signal)).rejects.toThrow("aborted");
395
+ await expect(fn.withSignal(controller.signal)).rejects.toThrow("aborted");
396
+ // Circuit should still be closed
397
+ const freshController = new AbortController();
398
+ const result = await fn.withSignal(freshController.signal);
399
+ expect(result).toBe("success");
400
+ });
401
+ it("should decrement failure count on success", async () => {
402
+ let shouldFail = true;
403
+ const fn = abortable(async () => {
404
+ if (shouldFail)
405
+ throw new Error("fail");
406
+ return "success";
407
+ }).use(circuitBreaker({ threshold: 3 }));
408
+ // 2 failures (failures = 2)
409
+ await expect(fn()).rejects.toThrow("fail");
410
+ await expect(fn()).rejects.toThrow("fail");
411
+ // Success - decrements (failures = 1)
412
+ shouldFail = false;
413
+ await fn();
414
+ // 1 more failure (failures = 2, still under threshold)
415
+ shouldFail = true;
416
+ await expect(fn()).rejects.toThrow("fail");
417
+ // Another success - decrements (failures = 1)
418
+ shouldFail = false;
419
+ await fn();
420
+ // 1 more failure - should still be under threshold (failures = 2)
421
+ shouldFail = true;
422
+ await expect(fn()).rejects.toThrow("fail");
423
+ // Circuit should still be closed
424
+ shouldFail = false;
425
+ const result = await fn();
426
+ expect(result).toBe("success");
427
+ });
428
+ });
429
+ describe("chaining wrappers", () => {
430
+ it("should chain multiple wrappers", async () => {
431
+ let attempts = 0;
432
+ const errors = [];
433
+ const fn = abortable(async () => {
434
+ attempts++;
435
+ if (attempts < 2) {
436
+ throw new Error(`fail-${attempts}`);
437
+ }
438
+ return "success";
439
+ })
440
+ .use(catchError((e) => errors.push(e)))
441
+ .use(retry(3));
442
+ const result = await fn();
443
+ expect(result).toBe("success");
444
+ expect(attempts).toBe(2);
445
+ expect(errors).toHaveLength(1);
446
+ expect(errors[0].message).toBe("fail-1");
447
+ });
448
+ it("should apply wrappers in correct order", async () => {
449
+ const order = [];
450
+ const fn = abortable(async () => {
451
+ order.push("base");
452
+ return "done";
453
+ })
454
+ .use((next) => async (ctx) => {
455
+ order.push("wrapper1-before");
456
+ const result = await next(ctx);
457
+ order.push("wrapper1-after");
458
+ return result;
459
+ })
460
+ .use((next) => async (ctx) => {
461
+ order.push("wrapper2-before");
462
+ const result = await next(ctx);
463
+ order.push("wrapper2-after");
464
+ return result;
465
+ });
466
+ await fn();
467
+ expect(order).toEqual([
468
+ "wrapper2-before",
469
+ "wrapper1-before",
470
+ "base",
471
+ "wrapper1-after",
472
+ "wrapper2-after",
473
+ ]);
474
+ });
475
+ it("should compose fallback with retry", async () => {
476
+ let attempts = 0;
477
+ const fn = abortable(async () => {
478
+ attempts++;
479
+ throw new Error("fail");
480
+ })
481
+ .use(retry(2))
482
+ .use(fallback("default"));
483
+ const result = await fn();
484
+ expect(result).toBe("default");
485
+ expect(attempts).toBe(2); // Retried, then fallback
486
+ });
487
+ it("should compose cache with circuit breaker", async () => {
488
+ let callCount = 0;
489
+ const fn = abortable(async ({}, id) => {
490
+ callCount++;
491
+ return `result-${id}`;
492
+ })
493
+ .use(cache(1000))
494
+ .use(circuitBreaker());
495
+ await fn("1");
496
+ await fn("1"); // Cached
497
+ await fn("2");
498
+ await fn("1"); // Still cached
499
+ expect(callCount).toBe(2); // Only 2 actual calls
500
+ });
501
+ });
502
+ describe("TYield preservation", () => {
503
+ it("should preserve TYield type through wrapper chain (compile-time check)", async () => {
504
+ const workflow = abortable(async ({ take }) => {
505
+ const step1 = await take("step");
506
+ return `completed: ${step1}`;
507
+ });
508
+ // Apply wrappers - TYield type should be preserved
509
+ const wrappedWorkflow = workflow.use(retry(2));
510
+ // Type check: result should have send() with correct signature
511
+ const result = wrappedWorkflow();
512
+ // This should compile - send() should accept "step" with number
513
+ // @ts-expect-error - "invalid" is not a valid event key
514
+ result.send("invalid", 10);
515
+ // Abort and catch to clean up
516
+ result.abort();
517
+ await result.catch(() => { });
518
+ });
519
+ it("should preserve TYield through multiple wrappers", async () => {
520
+ const fn = abortable(async ({ take }) => {
521
+ const data = await take("data");
522
+ const done = await take("done");
523
+ return done ? data.length : 0;
524
+ });
525
+ // Chain multiple wrappers
526
+ const wrapped = fn.use(retry(3)).use(catchError(() => { }));
527
+ const result = wrapped("test-id");
528
+ // Type check: send signature should match MyEvents
529
+ // @ts-expect-error - wrong value type for "data" event
530
+ result.send("data", 123);
531
+ // Abort and catch to clean up
532
+ result.abort();
533
+ await result.catch(() => { });
534
+ });
535
+ });
536
+ describe("observe()", () => {
537
+ it("should call onStart when function is invoked", async () => {
538
+ const onStart = vi.fn();
539
+ const fn = abortable(async (_ctx, id) => `user-${id}`).use(observe(onStart));
540
+ await fn("123");
541
+ expect(onStart).toHaveBeenCalledTimes(1);
542
+ expect(onStart).toHaveBeenCalledWith(expect.objectContaining({ signal: expect.any(AbortSignal) }), "123");
543
+ });
544
+ it("should call onAbort callback when aborted", async () => {
545
+ const onAbort = vi.fn();
546
+ const fn = abortable(async (ctx) => new Promise((resolve) => {
547
+ ctx.signal.addEventListener("abort", () => resolve("aborted"));
548
+ })).use(observe(() => ({ onAbort })));
549
+ const result = fn();
550
+ result.abort();
551
+ await expect(result).resolves.toBe("aborted");
552
+ expect(onAbort).toHaveBeenCalledTimes(1);
553
+ });
554
+ it("should call onSuccess callback on success", async () => {
555
+ const onSuccess = vi.fn();
556
+ const fn = abortable(async () => "result").use(observe(() => ({ onSuccess })));
557
+ await fn();
558
+ expect(onSuccess).toHaveBeenCalledTimes(1);
559
+ expect(onSuccess).toHaveBeenCalledWith("result");
560
+ });
561
+ it("should call onError callback on error and re-throw", async () => {
562
+ const onError = vi.fn();
563
+ const error = new Error("test error");
564
+ const fn = abortable(async () => {
565
+ throw error;
566
+ }).use(observe(() => ({ onError })));
567
+ await expect(fn()).rejects.toThrow("test error");
568
+ expect(onError).toHaveBeenCalledTimes(1);
569
+ expect(onError).toHaveBeenCalledWith(error);
570
+ });
571
+ it("should call onDone callback after success", async () => {
572
+ const onDone = vi.fn();
573
+ const fn = abortable(async () => "result").use(observe(() => ({ onDone })));
574
+ await fn();
575
+ expect(onDone).toHaveBeenCalledTimes(1);
576
+ });
577
+ it("should call onDone callback after error", async () => {
578
+ const onDone = vi.fn();
579
+ const fn = abortable(async () => {
580
+ throw new Error("test");
581
+ }).use(observe(() => ({ onDone })));
582
+ await fn().catch(() => { });
583
+ expect(onDone).toHaveBeenCalledTimes(1);
584
+ });
585
+ it("should call all callbacks in correct order on success", async () => {
586
+ const calls = [];
587
+ const fn = abortable(async () => "result").use(observe(() => ({
588
+ onSuccess: () => calls.push("onSuccess"),
589
+ onDone: () => calls.push("onDone"),
590
+ })));
591
+ await fn();
592
+ expect(calls).toEqual(["onSuccess", "onDone"]);
593
+ });
594
+ it("should call all callbacks in correct order on error", async () => {
595
+ const calls = [];
596
+ const fn = abortable(async () => {
597
+ throw new Error("test");
598
+ }).use(observe(() => ({
599
+ onError: () => calls.push("onError"),
600
+ onDone: () => calls.push("onDone"),
601
+ })));
602
+ await fn().catch(() => { });
603
+ expect(calls).toEqual(["onError", "onDone"]);
604
+ });
605
+ it("should not call onAbort if not aborted", async () => {
606
+ const onAbort = vi.fn();
607
+ const fn = abortable(async () => "done").use(observe(() => ({ onAbort })));
608
+ await fn();
609
+ expect(onAbort).not.toHaveBeenCalled();
610
+ });
611
+ it("should work with multiple observers", async () => {
612
+ const onStart1 = vi.fn();
613
+ const onStart2 = vi.fn();
614
+ const fn = abortable(async (_ctx, x) => x * 2)
615
+ .use(observe(onStart1))
616
+ .use(observe(onStart2));
617
+ const result = await fn(5);
618
+ expect(result).toBe(10);
619
+ expect(onStart1).toHaveBeenCalledTimes(1);
620
+ expect(onStart2).toHaveBeenCalledTimes(1);
621
+ });
622
+ it("should pass correct args to onStart", async () => {
623
+ const onStart = vi.fn();
624
+ const fn = abortable(async (_ctx, name, age) => `${name}-${age}`).use(observe(onStart));
625
+ await fn("John", 30);
626
+ expect(onStart).toHaveBeenCalledWith(expect.any(Object), "John", 30);
627
+ });
628
+ it("should work when onStart returns void", async () => {
629
+ const fn = abortable(async () => "result").use(observe(() => {
630
+ // No return - just logging
631
+ }));
632
+ const result = await fn();
633
+ expect(result).toBe("result");
634
+ });
635
+ it("should handle loading indicator pattern", async () => {
636
+ let isLoading = false;
637
+ const fn = abortable(async () => {
638
+ expect(isLoading).toBe(true); // Loading during execution
639
+ return "data";
640
+ }).use(observe(() => {
641
+ isLoading = true;
642
+ return { onDone: () => (isLoading = false) };
643
+ }));
644
+ await fn();
645
+ expect(isLoading).toBe(false); // Loading cleared after
646
+ });
647
+ });
648
+ });
649
+ //# sourceMappingURL=wrappers.test.js.map