@zapier/zapier-sdk 0.13.7 → 0.13.9

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 (59) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/api/client.d.ts.map +1 -1
  3. package/dist/api/client.js +5 -5
  4. package/dist/api/client.test.d.ts +2 -0
  5. package/dist/api/client.test.d.ts.map +1 -0
  6. package/dist/api/client.test.js +80 -0
  7. package/dist/api/index.d.ts +1 -0
  8. package/dist/api/index.d.ts.map +1 -1
  9. package/dist/api/index.js +3 -1
  10. package/dist/api/polling.d.ts.map +1 -1
  11. package/dist/api/polling.js +1 -11
  12. package/dist/api/schemas.d.ts +20 -20
  13. package/dist/api/types.d.ts +2 -0
  14. package/dist/api/types.d.ts.map +1 -1
  15. package/dist/auth.d.ts +3 -0
  16. package/dist/auth.d.ts.map +1 -1
  17. package/dist/auth.test.d.ts +2 -0
  18. package/dist/auth.test.d.ts.map +1 -0
  19. package/dist/auth.test.js +102 -0
  20. package/dist/constants.d.ts +4 -4
  21. package/dist/constants.d.ts.map +1 -1
  22. package/dist/constants.js +4 -4
  23. package/dist/index.cjs +194 -33
  24. package/dist/index.d.mts +93 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.mjs +192 -34
  29. package/dist/plugins/api/index.d.ts.map +1 -1
  30. package/dist/plugins/api/index.js +4 -1
  31. package/dist/plugins/eventEmission/index.d.ts +2 -0
  32. package/dist/plugins/eventEmission/index.d.ts.map +1 -1
  33. package/dist/plugins/eventEmission/index.js +35 -9
  34. package/dist/plugins/eventEmission/index.test.js +100 -0
  35. package/dist/schemas/Action.d.ts +2 -2
  36. package/dist/schemas/Auth.d.ts +4 -4
  37. package/dist/schemas/Field.d.ts +10 -10
  38. package/dist/sdk.test.js +121 -1
  39. package/dist/types/sdk.d.ts +3 -0
  40. package/dist/types/sdk.d.ts.map +1 -1
  41. package/dist/utils/batch-utils.d.ts +72 -0
  42. package/dist/utils/batch-utils.d.ts.map +1 -0
  43. package/dist/utils/batch-utils.js +162 -0
  44. package/dist/utils/batch-utils.test.d.ts +2 -0
  45. package/dist/utils/batch-utils.test.d.ts.map +1 -0
  46. package/dist/utils/batch-utils.test.js +476 -0
  47. package/dist/utils/retry-utils.d.ts +45 -0
  48. package/dist/utils/retry-utils.d.ts.map +1 -0
  49. package/dist/utils/retry-utils.js +51 -0
  50. package/dist/utils/retry-utils.test.d.ts +2 -0
  51. package/dist/utils/retry-utils.test.d.ts.map +1 -0
  52. package/dist/utils/retry-utils.test.js +90 -0
  53. package/dist/utils/url-utils.d.ts +19 -0
  54. package/dist/utils/url-utils.d.ts.map +1 -0
  55. package/dist/utils/url-utils.js +62 -0
  56. package/dist/utils/url-utils.test.d.ts +2 -0
  57. package/dist/utils/url-utils.test.d.ts.map +1 -0
  58. package/dist/utils/url-utils.test.js +103 -0
  59. package/package.json +2 -2
@@ -0,0 +1,476 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { batch } from "./batch-utils";
3
+ // Mock the timers/promises module
4
+ vi.mock("timers/promises", () => ({
5
+ setTimeout: vi.fn(() => Promise.resolve()),
6
+ }));
7
+ // Mock retry-utils to control backoff behavior in tests
8
+ vi.mock("./retry-utils", () => ({
9
+ calculateWaitTime: vi.fn(() => 0), // No delay in tests
10
+ MAX_CONSECUTIVE_ERRORS: 3,
11
+ }));
12
+ describe("batch", () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+ afterEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+ describe("basic functionality", () => {
20
+ it("should execute all tasks and return settled results", async () => {
21
+ const tasks = [
22
+ () => Promise.resolve("result1"),
23
+ () => Promise.resolve("result2"),
24
+ () => Promise.resolve("result3"),
25
+ ];
26
+ const results = await batch(tasks);
27
+ expect(results).toEqual([
28
+ { status: "fulfilled", value: "result1" },
29
+ { status: "fulfilled", value: "result2" },
30
+ { status: "fulfilled", value: "result3" },
31
+ ]);
32
+ });
33
+ it("should handle empty task array", async () => {
34
+ const results = await batch([]);
35
+ expect(results).toEqual([]);
36
+ });
37
+ it("should handle single task", async () => {
38
+ const tasks = [() => Promise.resolve("single")];
39
+ const results = await batch(tasks);
40
+ expect(results).toEqual([{ status: "fulfilled", value: "single" }]);
41
+ });
42
+ it("should maintain result order matching input order", async () => {
43
+ // Create tasks that resolve in reverse order but should maintain input order
44
+ const tasks = [
45
+ () => new Promise((resolve) => setTimeout(() => resolve("first"), 30)),
46
+ () => new Promise((resolve) => setTimeout(() => resolve("second"), 20)),
47
+ () => new Promise((resolve) => setTimeout(() => resolve("third"), 10)),
48
+ ];
49
+ const results = await batch(tasks);
50
+ expect(results[0]).toEqual({ status: "fulfilled", value: "first" });
51
+ expect(results[1]).toEqual({ status: "fulfilled", value: "second" });
52
+ expect(results[2]).toEqual({ status: "fulfilled", value: "third" });
53
+ });
54
+ });
55
+ describe("concurrency limiting", () => {
56
+ it("should limit concurrent execution", async () => {
57
+ let concurrentCount = 0;
58
+ let maxConcurrent = 0;
59
+ const createTask = () => () => {
60
+ concurrentCount++;
61
+ maxConcurrent = Math.max(maxConcurrent, concurrentCount);
62
+ return new Promise((resolve) => {
63
+ setTimeout(() => {
64
+ concurrentCount--;
65
+ resolve("done");
66
+ }, 10);
67
+ });
68
+ };
69
+ const tasks = Array.from({ length: 20 }, createTask);
70
+ await batch(tasks, { concurrency: 5 });
71
+ // Max concurrent should not exceed limit
72
+ expect(maxConcurrent).toBeLessThanOrEqual(5);
73
+ });
74
+ it("should respect custom concurrency limit", async () => {
75
+ let concurrentCount = 0;
76
+ let maxConcurrent = 0;
77
+ const createTask = () => () => {
78
+ concurrentCount++;
79
+ maxConcurrent = Math.max(maxConcurrent, concurrentCount);
80
+ return new Promise((resolve) => {
81
+ setTimeout(() => {
82
+ concurrentCount--;
83
+ resolve("done");
84
+ }, 10);
85
+ });
86
+ };
87
+ const tasks = Array.from({ length: 20 }, createTask);
88
+ await batch(tasks, { concurrency: 3 });
89
+ expect(maxConcurrent).toBeLessThanOrEqual(3);
90
+ });
91
+ it("should handle concurrency limit greater than task count", async () => {
92
+ const tasks = [() => Promise.resolve(1), () => Promise.resolve(2)];
93
+ const results = await batch(tasks, { concurrency: 10 });
94
+ expect(results).toHaveLength(2);
95
+ });
96
+ });
97
+ describe("error handling", () => {
98
+ it("should handle failing tasks without stopping batch", async () => {
99
+ const tasks = [
100
+ () => Promise.resolve("success1"),
101
+ () => Promise.reject(new Error("failed")),
102
+ () => Promise.resolve("success2"),
103
+ ];
104
+ const results = await batch(tasks, { retry: false });
105
+ expect(results[0]).toEqual({ status: "fulfilled", value: "success1" });
106
+ expect(results[1]).toEqual({
107
+ status: "rejected",
108
+ reason: expect.any(Error),
109
+ });
110
+ expect(results[2]).toEqual({ status: "fulfilled", value: "success2" });
111
+ });
112
+ it("should retry failed tasks when retry is enabled", async () => {
113
+ let attemptCount = 0;
114
+ const failTwiceThenSucceed = () => {
115
+ attemptCount++;
116
+ if (attemptCount <= 2) {
117
+ return Promise.reject(new Error("temporary failure"));
118
+ }
119
+ return Promise.resolve("success");
120
+ };
121
+ const tasks = [failTwiceThenSucceed];
122
+ const results = await batch(tasks, { retry: true });
123
+ expect(results[0]).toEqual({ status: "fulfilled", value: "success" });
124
+ expect(attemptCount).toBe(3); // Initial + 2 retries
125
+ });
126
+ it("should give up after MAX_CONSECUTIVE_ERRORS retries", async () => {
127
+ let attemptCount = 0;
128
+ const alwaysFail = () => {
129
+ attemptCount++;
130
+ return Promise.reject(new Error("always fails"));
131
+ };
132
+ const tasks = [alwaysFail];
133
+ const results = await batch(tasks, { retry: true });
134
+ expect(results[0]).toEqual({
135
+ status: "rejected",
136
+ reason: expect.any(Error),
137
+ });
138
+ expect(attemptCount).toBe(3); // MAX_CONSECUTIVE_ERRORS
139
+ });
140
+ it("should not retry when retry is disabled", async () => {
141
+ let attemptCount = 0;
142
+ const failTask = () => {
143
+ attemptCount++;
144
+ return Promise.reject(new Error("failed"));
145
+ };
146
+ const tasks = [failTask];
147
+ const results = await batch(tasks, { retry: false });
148
+ expect(results[0]).toEqual({
149
+ status: "rejected",
150
+ reason: expect.any(Error),
151
+ });
152
+ expect(attemptCount).toBe(1); // No retries
153
+ });
154
+ });
155
+ describe("options validation", () => {
156
+ it("should throw error for invalid concurrency", async () => {
157
+ const tasks = [() => Promise.resolve("test")];
158
+ await expect(batch(tasks, { concurrency: 0 })).rejects.toThrow("Concurrency must be greater than 0");
159
+ await expect(batch(tasks, { concurrency: -1 })).rejects.toThrow("Concurrency must be greater than 0");
160
+ });
161
+ it("should use default options when not provided", async () => {
162
+ const tasks = [() => Promise.resolve("test")];
163
+ const results = await batch(tasks);
164
+ expect(results).toEqual([{ status: "fulfilled", value: "test" }]);
165
+ });
166
+ });
167
+ describe("mixed success and failure scenarios", () => {
168
+ it("should handle alternating success and failure", async () => {
169
+ const tasks = [
170
+ () => Promise.resolve("success1"),
171
+ () => Promise.reject(new Error("error1")),
172
+ () => Promise.resolve("success2"),
173
+ () => Promise.reject(new Error("error2")),
174
+ () => Promise.resolve("success3"),
175
+ ];
176
+ const results = await batch(tasks, { retry: false });
177
+ expect(results[0].status).toBe("fulfilled");
178
+ expect(results[1].status).toBe("rejected");
179
+ expect(results[2].status).toBe("fulfilled");
180
+ expect(results[3].status).toBe("rejected");
181
+ expect(results[4].status).toBe("fulfilled");
182
+ });
183
+ it("should handle some tasks succeeding after retry", async () => {
184
+ let task1Attempts = 0;
185
+ let task2Attempts = 0;
186
+ const task1 = () => {
187
+ task1Attempts++;
188
+ if (task1Attempts === 1) {
189
+ return Promise.reject(new Error("task1 first attempt"));
190
+ }
191
+ return Promise.resolve("task1 success");
192
+ };
193
+ const task2 = () => {
194
+ task2Attempts++;
195
+ return Promise.resolve("task2 success");
196
+ };
197
+ const tasks = [task1, task2];
198
+ const results = await batch(tasks, { retry: true });
199
+ expect(results[0]).toEqual({
200
+ status: "fulfilled",
201
+ value: "task1 success",
202
+ });
203
+ expect(results[1]).toEqual({
204
+ status: "fulfilled",
205
+ value: "task2 success",
206
+ });
207
+ expect(task1Attempts).toBe(2); // Initial + 1 retry
208
+ expect(task2Attempts).toBe(1); // No retry needed
209
+ });
210
+ });
211
+ describe("performance characteristics", () => {
212
+ it("should complete faster with higher concurrency", async () => {
213
+ const delay = 50; // ms
214
+ const taskCount = 10;
215
+ const tasks = Array.from({ length: taskCount }, () => () => new Promise((resolve) => setTimeout(() => resolve("done"), delay)));
216
+ // Low concurrency (sequential-ish)
217
+ const start1 = Date.now();
218
+ await batch([...tasks], { concurrency: 1 });
219
+ const duration1 = Date.now() - start1;
220
+ // High concurrency (parallel)
221
+ const start2 = Date.now();
222
+ await batch([...tasks], { concurrency: 10 });
223
+ const duration2 = Date.now() - start2;
224
+ // High concurrency should be significantly faster
225
+ // Allow some margin for timing variability
226
+ expect(duration2).toBeLessThan(duration1 * 0.8);
227
+ });
228
+ });
229
+ describe("result ordering guarantees", () => {
230
+ it("should maintain exact order with out-of-order completion and mixed results", async () => {
231
+ // This is the ultimate ordering test - combining:
232
+ // 1. Tasks that complete in reverse order (via delays)
233
+ // 2. Mixed success/failure
234
+ // 3. High concurrency (multiple workers racing)
235
+ const executionLog = [];
236
+ const tasks = [
237
+ // Task 0: Slow success
238
+ () => new Promise((resolve) => {
239
+ setTimeout(() => {
240
+ executionLog.push("task0-complete");
241
+ resolve("result0");
242
+ }, 100);
243
+ }),
244
+ // Task 1: Fast failure
245
+ () => new Promise((_, reject) => {
246
+ setTimeout(() => {
247
+ executionLog.push("task1-complete");
248
+ reject(new Error("error1"));
249
+ }, 10);
250
+ }),
251
+ // Task 2: Medium success
252
+ () => new Promise((resolve) => {
253
+ setTimeout(() => {
254
+ executionLog.push("task2-complete");
255
+ resolve("result2");
256
+ }, 50);
257
+ }),
258
+ // Task 3: Instant failure
259
+ () => new Promise((_, reject) => {
260
+ executionLog.push("task3-complete");
261
+ reject(new Error("error3"));
262
+ }),
263
+ // Task 4: Medium-slow success
264
+ () => new Promise((resolve) => {
265
+ setTimeout(() => {
266
+ executionLog.push("task4-complete");
267
+ resolve("result4");
268
+ }, 75);
269
+ }),
270
+ ];
271
+ const results = await batch(tasks, { concurrency: 5, retry: false });
272
+ // Verify all tasks completed
273
+ expect(executionLog).toHaveLength(5);
274
+ expect(executionLog).toContain("task0-complete");
275
+ expect(executionLog).toContain("task1-complete");
276
+ expect(executionLog).toContain("task2-complete");
277
+ expect(executionLog).toContain("task3-complete");
278
+ expect(executionLog).toContain("task4-complete");
279
+ // BUT results MUST maintain input order despite out-of-order completion
280
+ expect(results).toHaveLength(5);
281
+ // Task 0: position 0, slow success
282
+ expect(results[0]).toEqual({
283
+ status: "fulfilled",
284
+ value: "result0",
285
+ });
286
+ // Task 1: position 1, fast failure
287
+ expect(results[1]).toEqual({
288
+ status: "rejected",
289
+ reason: expect.objectContaining({
290
+ message: "error1",
291
+ }),
292
+ });
293
+ // Task 2: position 2, medium success
294
+ expect(results[2]).toEqual({
295
+ status: "fulfilled",
296
+ value: "result2",
297
+ });
298
+ // Task 3: position 3, instant failure
299
+ expect(results[3]).toEqual({
300
+ status: "rejected",
301
+ reason: expect.objectContaining({
302
+ message: "error3",
303
+ }),
304
+ });
305
+ // Task 4: position 4, medium-slow success
306
+ expect(results[4]).toEqual({
307
+ status: "fulfilled",
308
+ value: "result4",
309
+ });
310
+ });
311
+ it("should match Promise.allSettled ordering behavior exactly", async () => {
312
+ // This test ensures our batch function has identical ordering to Promise.allSettled
313
+ const tasks = [
314
+ () => new Promise((resolve) => setTimeout(() => resolve("a"), 30)),
315
+ () => new Promise((_, reject) => setTimeout(() => reject(new Error("b")), 10)),
316
+ () => new Promise((resolve) => setTimeout(() => resolve("c"), 20)),
317
+ ];
318
+ // Run both in parallel
319
+ const [batchResults, settledResults] = await Promise.all([
320
+ batch(tasks.map((t) => t), { concurrency: 3, retry: false }),
321
+ Promise.allSettled(tasks.map((t) => t())),
322
+ ]);
323
+ // Results should have same structure
324
+ expect(batchResults).toHaveLength(settledResults.length);
325
+ // Compare each result
326
+ for (let i = 0; i < batchResults.length; i++) {
327
+ const batchResult = batchResults[i];
328
+ const settledResult = settledResults[i];
329
+ expect(batchResult.status).toBe(settledResult.status);
330
+ if (batchResult.status === "fulfilled" &&
331
+ settledResult.status === "fulfilled") {
332
+ expect(batchResult.value).toBe(settledResult.value);
333
+ }
334
+ if (batchResult.status === "rejected" &&
335
+ settledResult.status === "rejected") {
336
+ expect(batchResult.reason).toBeInstanceOf(Error);
337
+ expect(settledResult.reason).toBeInstanceOf(Error);
338
+ }
339
+ }
340
+ });
341
+ });
342
+ describe("timeout handling", () => {
343
+ beforeEach(async () => {
344
+ // Unmock setTimeout for timeout tests to allow real timing
345
+ vi.unmock("timers/promises");
346
+ // Re-import batch to get the non-mocked version
347
+ vi.resetModules();
348
+ });
349
+ afterEach(() => {
350
+ // Restore mocks after timeout tests
351
+ vi.doMock("timers/promises", () => ({
352
+ setTimeout: vi.fn(() => Promise.resolve()),
353
+ }));
354
+ });
355
+ it("should throw error when overall batch timeout is exceeded", async () => {
356
+ const { batch } = await import("./batch-utils");
357
+ const tasks = Array.from({ length: 10 }, () => () => new Promise((resolve) => setTimeout(() => resolve("done"), 100)));
358
+ await expect(batch(tasks, { concurrency: 2, timeoutMs: 50 })).rejects.toThrow("Batch operation timed out");
359
+ });
360
+ it("should timeout individual tasks when taskTimeoutMs is set", async () => {
361
+ const { batch } = await import("./batch-utils");
362
+ const tasks = [
363
+ () => Promise.resolve("instant"), // Instant task - should not timeout
364
+ () => new Promise((resolve) => setTimeout(() => resolve("slow"), 500)), // Slow task - should timeout
365
+ () => Promise.resolve("instant2"), // Another instant task - should not timeout
366
+ ];
367
+ const results = await batch(tasks, {
368
+ taskTimeoutMs: 100, // 100ms timeout
369
+ retry: false,
370
+ batchDelay: 0, // Disable batch delay for timing reliability
371
+ });
372
+ expect(results[0].status).toBe("fulfilled");
373
+ expect(results[1].status).toBe("rejected");
374
+ if (results[1].status === "rejected") {
375
+ expect(results[1].reason).toBeInstanceOf(Error);
376
+ expect(results[1].reason.message).toContain("timed out");
377
+ }
378
+ expect(results[2].status).toBe("fulfilled");
379
+ });
380
+ it("should not retry tasks that timeout", async () => {
381
+ const { batch } = await import("./batch-utils");
382
+ let attemptCount = 0;
383
+ const slowTask = () => {
384
+ attemptCount++;
385
+ return new Promise((resolve) => setTimeout(() => resolve("done"), 200));
386
+ };
387
+ const results = await batch([slowTask], {
388
+ taskTimeoutMs: 50,
389
+ retry: true,
390
+ });
391
+ expect(results[0].status).toBe("rejected");
392
+ expect(attemptCount).toBe(1); // Should not retry timeout errors
393
+ });
394
+ it("should validate timeout parameters", async () => {
395
+ const { batch } = await import("./batch-utils");
396
+ const tasks = [() => Promise.resolve("test")];
397
+ await expect(batch(tasks, { timeoutMs: 0 })).rejects.toThrow("Timeout must be greater than 0");
398
+ await expect(batch(tasks, { timeoutMs: -100 })).rejects.toThrow("Timeout must be greater than 0");
399
+ await expect(batch(tasks, { taskTimeoutMs: 0 })).rejects.toThrow("Task timeout must be greater than 0");
400
+ await expect(batch(tasks, { taskTimeoutMs: -100 })).rejects.toThrow("Task timeout must be greater than 0");
401
+ });
402
+ it("should complete tasks that finish before overall timeout", async () => {
403
+ const { batch } = await import("./batch-utils");
404
+ const tasks = [
405
+ () => new Promise((resolve) => setTimeout(() => resolve("task1"), 10)),
406
+ () => new Promise((resolve) => setTimeout(() => resolve("task2"), 20)),
407
+ () => new Promise((resolve) => setTimeout(() => resolve("task3"), 30)),
408
+ ];
409
+ const results = await batch(tasks, {
410
+ timeoutMs: 100,
411
+ concurrency: 3,
412
+ });
413
+ expect(results).toHaveLength(3);
414
+ expect(results[0].status).toBe("fulfilled");
415
+ expect(results[1].status).toBe("fulfilled");
416
+ expect(results[2].status).toBe("fulfilled");
417
+ });
418
+ it("should report tasks remaining when batch timeout occurs", async () => {
419
+ const { batch } = await import("./batch-utils");
420
+ const tasks = Array.from({ length: 20 }, () => () => new Promise((resolve) => setTimeout(() => resolve("done"), 100)));
421
+ try {
422
+ await batch(tasks, { concurrency: 2, timeoutMs: 50 });
423
+ expect.fail("Should have thrown timeout error");
424
+ }
425
+ catch (error) {
426
+ expect(error).toBeInstanceOf(Error);
427
+ expect(error.message).toContain("task(s) not completed");
428
+ }
429
+ });
430
+ it("should allow taskTimeoutMs without overall timeout affecting quick tasks", async () => {
431
+ const { batch } = await import("./batch-utils");
432
+ const tasks = [
433
+ () => Promise.resolve("instant1"),
434
+ () => Promise.resolve("instant2"),
435
+ () => Promise.resolve("instant3"),
436
+ ];
437
+ const results = await batch(tasks, {
438
+ taskTimeoutMs: 1000,
439
+ timeoutMs: 5000,
440
+ });
441
+ expect(results).toHaveLength(3);
442
+ expect(results.every((r) => r.status === "fulfilled")).toBe(true);
443
+ });
444
+ it("should handle mix of timed out and successful tasks", async () => {
445
+ const { batch } = await import("./batch-utils");
446
+ const tasks = [
447
+ () => Promise.resolve("fast1"), // Instant - should not timeout
448
+ () => new Promise((resolve) => setTimeout(() => resolve("slow1"), 500)), // Slow - should timeout
449
+ () => Promise.resolve("fast2"), // Instant - should not timeout
450
+ () => new Promise((resolve) => setTimeout(() => resolve("slow2"), 500)), // Slow - should timeout
451
+ ];
452
+ const results = await batch(tasks, {
453
+ taskTimeoutMs: 100,
454
+ retry: false,
455
+ concurrency: 4,
456
+ batchDelay: 0, // Disable batch delay for timing reliability
457
+ });
458
+ expect(results[0].status).toBe("fulfilled");
459
+ expect(results[1].status).toBe("rejected");
460
+ expect(results[2].status).toBe("fulfilled");
461
+ expect(results[3].status).toBe("rejected");
462
+ });
463
+ it("should use default timeout when not specified", async () => {
464
+ const { batch } = await import("./batch-utils");
465
+ const tasks = [
466
+ () => Promise.resolve("test1"),
467
+ () => Promise.resolve("test2"),
468
+ ];
469
+ // Should not throw with default 3-minute timeout
470
+ const results = await batch(tasks);
471
+ expect(results).toHaveLength(2);
472
+ expect(results[0].status).toBe("fulfilled");
473
+ expect(results[1].status).toBe("fulfilled");
474
+ });
475
+ });
476
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Retry and Backoff Utilities
3
+ *
4
+ * Shared utilities for implementing resilient retry logic with exponential backoff
5
+ * and jitter. Used by both polling and batch operations.
6
+ */
7
+ /**
8
+ * Maximum number of consecutive errors before giving up
9
+ * Prevents infinite retry loops when the API is consistently failing
10
+ */
11
+ export declare const MAX_CONSECUTIVE_ERRORS = 3;
12
+ /**
13
+ * Base delay for error backoff in milliseconds
14
+ * Each error adds this amount (with scaling) to the wait time
15
+ */
16
+ export declare const BASE_ERROR_BACKOFF_MS = 1000;
17
+ /**
18
+ * Jitter factor (0.0 to 1.0) for randomizing wait times
19
+ * Prevents thundering herd problem when many clients retry simultaneously
20
+ * A factor of 0.5 means we add 0-50% random variance to the base interval
21
+ */
22
+ export declare const JITTER_FACTOR = 0.5;
23
+ /**
24
+ * Calculate wait time with jitter and error backoff
25
+ *
26
+ * This implements two key reliability patterns:
27
+ * 1. Jitter - Adds randomness to prevent synchronized retries across clients
28
+ * 2. Error backoff - Increases wait time when errors occur, giving the API time to recover
29
+ *
30
+ * @param baseInterval - The base wait time in milliseconds
31
+ * @param errorCount - Number of consecutive errors (0 if no errors)
32
+ * @returns Wait time in milliseconds with jitter and error backoff applied
33
+ *
34
+ * @example
35
+ * // No errors: returns 1000-1500ms (1000 + 0-500ms jitter)
36
+ * calculateWaitTime(1000, 0);
37
+ *
38
+ * // 2 errors: returns 1000-1500ms base + up to 1000ms error backoff = 1000-2500ms
39
+ * calculateWaitTime(1000, 2);
40
+ *
41
+ * // 6 errors: returns 1000-1500ms base + 2000ms capped backoff = 3000-3500ms
42
+ * calculateWaitTime(1000, 6);
43
+ */
44
+ export declare function calculateWaitTime(baseInterval: number, errorCount: number): number;
45
+ //# sourceMappingURL=retry-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-utils.d.ts","sourceRoot":"","sources":["../../src/utils/retry-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,OAAQ,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Retry and Backoff Utilities
3
+ *
4
+ * Shared utilities for implementing resilient retry logic with exponential backoff
5
+ * and jitter. Used by both polling and batch operations.
6
+ */
7
+ /**
8
+ * Maximum number of consecutive errors before giving up
9
+ * Prevents infinite retry loops when the API is consistently failing
10
+ */
11
+ export const MAX_CONSECUTIVE_ERRORS = 3;
12
+ /**
13
+ * Base delay for error backoff in milliseconds
14
+ * Each error adds this amount (with scaling) to the wait time
15
+ */
16
+ export const BASE_ERROR_BACKOFF_MS = 1000;
17
+ /**
18
+ * Jitter factor (0.0 to 1.0) for randomizing wait times
19
+ * Prevents thundering herd problem when many clients retry simultaneously
20
+ * A factor of 0.5 means we add 0-50% random variance to the base interval
21
+ */
22
+ export const JITTER_FACTOR = 0.5;
23
+ /**
24
+ * Calculate wait time with jitter and error backoff
25
+ *
26
+ * This implements two key reliability patterns:
27
+ * 1. Jitter - Adds randomness to prevent synchronized retries across clients
28
+ * 2. Error backoff - Increases wait time when errors occur, giving the API time to recover
29
+ *
30
+ * @param baseInterval - The base wait time in milliseconds
31
+ * @param errorCount - Number of consecutive errors (0 if no errors)
32
+ * @returns Wait time in milliseconds with jitter and error backoff applied
33
+ *
34
+ * @example
35
+ * // No errors: returns 1000-1500ms (1000 + 0-500ms jitter)
36
+ * calculateWaitTime(1000, 0);
37
+ *
38
+ * // 2 errors: returns 1000-1500ms base + up to 1000ms error backoff = 1000-2500ms
39
+ * calculateWaitTime(1000, 2);
40
+ *
41
+ * // 6 errors: returns 1000-1500ms base + 2000ms capped backoff = 3000-3500ms
42
+ * calculateWaitTime(1000, 6);
43
+ */
44
+ export function calculateWaitTime(baseInterval, errorCount) {
45
+ // Jitter to avoid thundering herd (multiple clients retrying at exact same time)
46
+ const jitter = Math.random() * JITTER_FACTOR * baseInterval;
47
+ // Exponential backoff scaled by error count, but capped at 2x base interval
48
+ // This prevents wait times from growing unboundedly
49
+ const errorBackoff = Math.min(BASE_ERROR_BACKOFF_MS * (errorCount / 2), baseInterval * 2);
50
+ return Math.floor(baseInterval + jitter + errorBackoff);
51
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=retry-utils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-utils.test.d.ts","sourceRoot":"","sources":["../../src/utils/retry-utils.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { calculateWaitTime, MAX_CONSECUTIVE_ERRORS, BASE_ERROR_BACKOFF_MS, JITTER_FACTOR, } from "./retry-utils";
3
+ describe("retry-utils", () => {
4
+ describe("calculateWaitTime", () => {
5
+ beforeEach(() => {
6
+ // Seed random for consistent testing
7
+ vi.spyOn(Math, "random");
8
+ });
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ });
12
+ it("should return base interval with jitter when no errors", () => {
13
+ vi.spyOn(Math, "random").mockReturnValue(0.5); // Middle of jitter range
14
+ const baseInterval = 1000;
15
+ const waitTime = calculateWaitTime(baseInterval, 0);
16
+ // With 0.5 random and JITTER_FACTOR 0.5:
17
+ // jitter = 0.5 * 0.5 * 1000 = 250ms
18
+ // errorBackoff = 0 (no errors)
19
+ // total = 1000 + 250 + 0 = 1250ms
20
+ expect(waitTime).toBe(1250);
21
+ });
22
+ it("should add error backoff for consecutive errors", () => {
23
+ vi.spyOn(Math, "random").mockReturnValue(0); // No jitter for simpler math
24
+ const baseInterval = 1000;
25
+ const errorCount = 2;
26
+ const waitTime = calculateWaitTime(baseInterval, errorCount);
27
+ // jitter = 0 (random is 0)
28
+ // errorBackoff = BASE_ERROR_BACKOFF_MS * (errorCount / 2) = 1000 * (2/2) = 1000ms
29
+ // total = 1000 + 0 + 1000 = 2000ms
30
+ expect(waitTime).toBe(2000);
31
+ });
32
+ it("should cap error backoff at 2x base interval", () => {
33
+ vi.spyOn(Math, "random").mockReturnValue(0); // No jitter
34
+ const baseInterval = 1000;
35
+ const errorCount = 10; // Large error count
36
+ const waitTime = calculateWaitTime(baseInterval, errorCount);
37
+ // errorBackoff would be 1000 * (10/2) = 5000ms
38
+ // but it's capped at 2x base = 2000ms
39
+ // total = 1000 + 0 + 2000 = 3000ms
40
+ expect(waitTime).toBe(3000);
41
+ });
42
+ it("should add random jitter to prevent thundering herd", () => {
43
+ const baseInterval = 1000;
44
+ const results = new Set();
45
+ // Run multiple times with real random to ensure jitter varies
46
+ vi.spyOn(Math, "random").mockRestore(); // Use real random
47
+ for (let i = 0; i < 10; i++) {
48
+ const waitTime = calculateWaitTime(baseInterval, 0);
49
+ results.add(waitTime);
50
+ }
51
+ // Should have multiple different values due to jitter
52
+ expect(results.size).toBeGreaterThan(1);
53
+ // All values should be in expected range
54
+ // baseInterval (1000) + jitter (0 to JITTER_FACTOR * baseInterval)
55
+ // = 1000 to 1500ms
56
+ for (const result of results) {
57
+ expect(result).toBeGreaterThanOrEqual(1000);
58
+ expect(result).toBeLessThanOrEqual(1500);
59
+ }
60
+ });
61
+ it("should combine jitter and error backoff", () => {
62
+ vi.spyOn(Math, "random").mockReturnValue(0.5);
63
+ const baseInterval = 1000;
64
+ const errorCount = 2;
65
+ const waitTime = calculateWaitTime(baseInterval, errorCount);
66
+ // jitter = 0.5 * 0.5 * 1000 = 250ms
67
+ // errorBackoff = 1000 * (2/2) = 1000ms
68
+ // total = 1000 + 250 + 1000 = 2250ms
69
+ expect(waitTime).toBe(2250);
70
+ });
71
+ it("should return integer wait time", () => {
72
+ vi.spyOn(Math, "random").mockReturnValue(0.333);
73
+ const baseInterval = 1000;
74
+ const waitTime = calculateWaitTime(baseInterval, 1);
75
+ // Result should be floored integer
76
+ expect(Number.isInteger(waitTime)).toBe(true);
77
+ });
78
+ });
79
+ describe("constants", () => {
80
+ it("should export MAX_CONSECUTIVE_ERRORS", () => {
81
+ expect(MAX_CONSECUTIVE_ERRORS).toBe(3);
82
+ });
83
+ it("should export BASE_ERROR_BACKOFF_MS", () => {
84
+ expect(BASE_ERROR_BACKOFF_MS).toBe(1000);
85
+ });
86
+ it("should export JITTER_FACTOR", () => {
87
+ expect(JITTER_FACTOR).toBe(0.5);
88
+ });
89
+ });
90
+ });