@zapier/zapier-sdk 0.8.2 → 0.8.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @zapier/zapier-sdk
2
2
 
3
+ ## 0.8.3
4
+
5
+ ### Patch Changes
6
+
7
+ - ed235b6: Adds improved backoff logic to the API polling
8
+
3
9
  ## 0.8.2
4
10
 
5
11
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAGjB,MAAM,SAAS,CAAC;AA4bjB,eAAO,MAAM,eAAe,GAAI,SAAS,gBAAgB,KAAG,SAqB3D,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAGjB,MAAM,SAAS,CAAC;AA6bjB,eAAO,MAAM,eAAe,GAAI,SAAS,gBAAgB,KAAG,SAqB3D,CAAC"}
@@ -40,9 +40,8 @@ class ZapierApiClient {
40
40
  searchParams: options.searchParams,
41
41
  authRequired: options.authRequired,
42
42
  }),
43
- maxAttempts: options.maxAttempts,
44
43
  initialDelay: options.initialDelay,
45
- maxDelay: options.maxDelay,
44
+ timeoutMs: options.timeoutMs,
46
45
  successStatus: options.successStatus,
47
46
  pendingStatus: options.pendingStatus,
48
47
  resultExtractor: options.resultExtractor,
@@ -4,13 +4,43 @@
4
4
  * This module provides utilities for polling HTTP endpoints until completion,
5
5
  * with configurable retry logic and exponential backoff.
6
6
  */
7
- export declare function pollUntilComplete(options: {
7
+ /**
8
+ * Options for the polling function
9
+ */
10
+ export interface PollOptions<TResult = unknown> {
11
+ /** Function that performs the HTTP request */
8
12
  fetchPoll: () => Promise<Response>;
9
- maxAttempts?: number;
10
- initialDelay?: number;
11
- maxDelay?: number;
13
+ /** Maximum time to wait for completion (in milliseconds) */
14
+ timeoutMs?: number;
15
+ /** HTTP status code indicating successful completion */
12
16
  successStatus?: number;
17
+ /** HTTP status code indicating the operation is still pending */
13
18
  pendingStatus?: number;
14
- resultExtractor?: (response: any) => any;
15
- }): Promise<any>;
19
+ /** Function to extract the result from the response */
20
+ resultExtractor?: (response: unknown) => TResult;
21
+ /** Initial delay before the first poll attempt (in milliseconds) */
22
+ initialDelay?: number;
23
+ }
24
+ declare const enum PollStatus {
25
+ Success = "success",
26
+ Continue = "continue"
27
+ }
28
+ /**
29
+ * Result of a poll operation
30
+ */
31
+ export type PollResult<TResult = unknown> = {
32
+ result?: TResult;
33
+ status: PollStatus;
34
+ errorCount: number;
35
+ };
36
+ /**
37
+ * Polls an endpoint until completion, timeout, or error
38
+ * @param options Configuration options for polling
39
+ * @returns The extracted result from the successful response
40
+ * @throws {ZapierValidationError} When the input parameters are invalid
41
+ * @throws {ZapierTimeoutError} When the operation times out
42
+ * @throws {ZapierApiError} When the API returns consecutive errors
43
+ */
44
+ export declare function pollUntilComplete<TResult = unknown>(options: PollOptions<TResult>): Promise<TResult>;
45
+ export {};
16
46
  //# sourceMappingURL=polling.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"polling.d.ts","sourceRoot":"","sources":["../../src/api/polling.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,SAAS,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,GAAG,CAAC;CAC1C,GAAG,OAAO,CAAC,GAAG,CAAC,CAuDf"}
1
+ {"version":3,"file":"polling.d.ts","sourceRoot":"","sources":["../../src/api/polling.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA+BH;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,OAAO,GAAG,OAAO;IAC5C,8CAA8C;IAC9C,SAAS,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;IACjD,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mBAAW,UAAU;IACnB,OAAO,YAAY;IACnB,QAAQ,aAAa;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,CAAC,OAAO,GAAG,OAAO,IAAI;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAuEF;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,GAAG,OAAO,EACvD,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,GAC5B,OAAO,CAAC,OAAO,CAAC,CAyGlB"}
@@ -4,42 +4,146 @@
4
4
  * This module provides utilities for polling HTTP endpoints until completion,
5
5
  * with configurable retry logic and exponential backoff.
6
6
  */
7
- import { ZapierTimeoutError, ZapierApiError } from "../types/errors";
7
+ import { ZapierTimeoutError, ZapierApiError, ZapierValidationError, } from "../types/errors";
8
+ import { setTimeout } from "timers/promises";
9
+ // Constants
10
+ const DEFAULT_TIMEOUT_MS = 180000;
11
+ const DEFAULT_SUCCESS_STATUS = 200;
12
+ const DEFAULT_PENDING_STATUS = 202;
13
+ const DEFAULT_INITIAL_DELAY_MS = 50;
14
+ const DEFAULT_MAX_POLLING_INTERVAL_MS = 10000;
15
+ const MAX_CONSECUTIVE_ERRORS = 3;
16
+ const MAX_TIMEOUT_BUFFER_MS = 10000;
17
+ const BASE_ERROR_BACKOFF_MS = 1000;
18
+ const JITTER_FACTOR = 0.5;
19
+ // Polling stages: [threshold_ms, interval_ms]
20
+ // Note: These are default stages, actual hard timeout is enforced separately below
21
+ const DEFAULT_POLLING_STAGES = [
22
+ [125, 125], // Up to 125ms: poll every 125ms
23
+ [375, 250], // Up to 375ms: poll every 250ms
24
+ [875, 500], // Up to 875ms: poll every 500ms
25
+ [10000, 1000], // Up to 10s: poll every 1s
26
+ [30000, 2500], // Up to 30s: poll every 2.5s
27
+ [60000, 5000], // Up to 60s: poll every 5s
28
+ ];
29
+ // Helper to calculate wait time with jitter and error backoff
30
+ const calculateWaitTime = (baseInterval, errorCount) => {
31
+ // Jitter to avoid thundering herd
32
+ const jitter = Math.random() * JITTER_FACTOR * baseInterval;
33
+ // More backoff added if errors are seen
34
+ const errorBackoff = Math.min(BASE_ERROR_BACKOFF_MS * (errorCount / 2), baseInterval * 2);
35
+ return Math.floor(baseInterval + jitter + errorBackoff);
36
+ };
37
+ const processResponse = async (response, successStatus, pendingStatus, resultExtractor, errorCount) => {
38
+ // Handle other error responses
39
+ if (!response.ok) {
40
+ return {
41
+ status: "continue" /* PollStatus.Continue */,
42
+ // If for some reason the status is pending, we don't want to increment the error count
43
+ errorCount: response.status === pendingStatus ? errorCount : errorCount + 1,
44
+ };
45
+ }
46
+ // Check for successful completion
47
+ if (response.status === successStatus) {
48
+ try {
49
+ const resultJson = await response.json();
50
+ return {
51
+ result: resultExtractor(resultJson),
52
+ status: "success" /* PollStatus.Success */,
53
+ errorCount: 0,
54
+ };
55
+ }
56
+ catch (error) {
57
+ throw new ZapierApiError("Result extractor failed to parse successful response as JSON", {
58
+ statusCode: response.status,
59
+ cause: error,
60
+ });
61
+ }
62
+ }
63
+ // If it's not pending, it's unexpected
64
+ if (response.status !== pendingStatus) {
65
+ throw new ZapierApiError(`Unexpected response status during polling: ${response.status}`, {
66
+ statusCode: response.status,
67
+ });
68
+ }
69
+ // It's still pending, so we continue polling
70
+ return {
71
+ status: "continue" /* PollStatus.Continue */,
72
+ errorCount: 0,
73
+ };
74
+ };
75
+ /**
76
+ * Polls an endpoint until completion, timeout, or error
77
+ * @param options Configuration options for polling
78
+ * @returns The extracted result from the successful response
79
+ * @throws {ZapierValidationError} When the input parameters are invalid
80
+ * @throws {ZapierTimeoutError} When the operation times out
81
+ * @throws {ZapierApiError} When the API returns consecutive errors
82
+ */
8
83
  export async function pollUntilComplete(options) {
9
- const { fetchPoll, maxAttempts = 30, initialDelay = 50, maxDelay = 1000, successStatus = 200, pendingStatus = 202, resultExtractor = (response) => response, } = options;
10
- let delay = initialDelay;
84
+ const { fetchPoll, timeoutMs = DEFAULT_TIMEOUT_MS, initialDelay = DEFAULT_INITIAL_DELAY_MS, successStatus = DEFAULT_SUCCESS_STATUS, pendingStatus = DEFAULT_PENDING_STATUS, resultExtractor = (response) => response, } = options;
85
+ // Validate input parameters
86
+ if (timeoutMs <= 0) {
87
+ throw new ZapierValidationError("Timeout must be greater than 0", {
88
+ details: { timeoutMs },
89
+ });
90
+ }
91
+ if (initialDelay < 0) {
92
+ throw new ZapierValidationError("Initial delay must be non-negative", {
93
+ details: { initialDelay },
94
+ });
95
+ }
96
+ const startTime = Date.now();
97
+ let attempts = 0;
11
98
  let errorCount = 0;
12
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
13
- const response = await fetchPoll();
14
- if (response.status === successStatus) {
15
- // Success - reset error count and return results
16
- errorCount = 0;
17
- const result = await response.json();
18
- return resultExtractor(result);
99
+ // Build polling stages with the actual timeout appended
100
+ const pollingStages = [
101
+ ...DEFAULT_POLLING_STAGES,
102
+ [timeoutMs + MAX_TIMEOUT_BUFFER_MS, DEFAULT_MAX_POLLING_INTERVAL_MS], // Up to timeout + 10s: poll every 10s
103
+ ];
104
+ // Apply initial delay if specified
105
+ if (initialDelay > 0) {
106
+ await setTimeout(initialDelay);
107
+ }
108
+ while (true) {
109
+ attempts++;
110
+ const elapsedTime = Date.now() - startTime;
111
+ // Find the current polling stage
112
+ const pollingInterval = pollingStages.find(([maxTimeForStage, _interval]) => {
113
+ return elapsedTime < maxTimeForStage;
114
+ });
115
+ // If there isn't a current stage, throw timeout error
116
+ if (!pollingInterval) {
117
+ throw new ZapierTimeoutError(`Operation timed out after ${Math.floor(elapsedTime / 1000)}s (${attempts} attempts)`, {
118
+ attempts,
119
+ });
19
120
  }
20
- else if (response.status === pendingStatus) {
21
- // Still processing - reset error count and wait and retry
22
- errorCount = 0;
23
- if (attempt < maxAttempts - 1) {
24
- await new Promise((resolve) => setTimeout(resolve, delay));
25
- delay = Math.min(delay * 2, maxDelay); // Exponential backoff
26
- continue;
27
- }
121
+ // Wait before polling (except on first attempt)
122
+ if (attempts > 1) {
123
+ const waitTime = calculateWaitTime(pollingInterval[1], errorCount);
124
+ await setTimeout(waitTime);
28
125
  }
29
- else {
30
- // Error occurred - increment error count
31
- errorCount++;
32
- if (errorCount >= 3) {
126
+ // Perform the poll request
127
+ try {
128
+ const response = await fetchPoll();
129
+ const { result, errorCount: newErrorCount, status, } = await processResponse(response, successStatus, pendingStatus, resultExtractor, errorCount);
130
+ errorCount = newErrorCount;
131
+ if (status === "success" /* PollStatus.Success */) {
132
+ return result;
133
+ }
134
+ if (errorCount >= MAX_CONSECUTIVE_ERRORS) {
33
135
  // Too many consecutive errors, fail
34
136
  throw new ZapierApiError(`Poll request failed: ${response.status} ${response.statusText}`, { statusCode: response.status });
35
137
  }
36
- // Treat as pending for up to 3 errors - wait and retry
37
- if (attempt < maxAttempts - 1) {
38
- await new Promise((resolve) => setTimeout(resolve, delay));
39
- delay = Math.min(delay * 2, maxDelay); // Exponential backoff
40
- continue;
138
+ }
139
+ catch (error) {
140
+ errorCount++;
141
+ if (errorCount >= MAX_CONSECUTIVE_ERRORS) {
142
+ throw new ZapierApiError(`Failed to poll after ${errorCount} consecutive errors: ${error instanceof Error ? error.message : String(error)}`, {
143
+ cause: error,
144
+ });
41
145
  }
42
146
  }
147
+ // Continue polling if status is pending
43
148
  }
44
- throw new ZapierTimeoutError(`Operation timed out after ${maxAttempts} attempts`, { attempts: maxAttempts, maxAttempts: maxAttempts });
45
149
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=polling.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"polling.test.d.ts","sourceRoot":"","sources":["../../src/api/polling.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,318 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { pollUntilComplete } from "./polling";
3
+ import { ZapierTimeoutError, ZapierApiError, ZapierValidationError, } from "../types/errors";
4
+ // Mock the timers/promises module
5
+ vi.mock("timers/promises", () => ({
6
+ setTimeout: vi.fn(() => Promise.resolve()),
7
+ }));
8
+ describe("pollUntilComplete", () => {
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+ afterEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ describe("successful polling", () => {
16
+ it("should return immediately on first successful response", async () => {
17
+ const mockResponse = {
18
+ ok: true,
19
+ status: 200,
20
+ json: vi.fn().mockResolvedValue({ data: "success" }),
21
+ };
22
+ const fetchPoll = vi.fn().mockResolvedValue(mockResponse);
23
+ const result = await pollUntilComplete({ fetchPoll });
24
+ expect(result).toEqual({ data: "success" });
25
+ expect(fetchPoll).toHaveBeenCalledTimes(1);
26
+ });
27
+ it("should poll until success status is received", async () => {
28
+ const pendingResponse = {
29
+ ok: true,
30
+ status: 202,
31
+ };
32
+ const successResponse = {
33
+ ok: true,
34
+ status: 200,
35
+ json: vi.fn().mockResolvedValue({ data: "complete" }),
36
+ };
37
+ const fetchPoll = vi
38
+ .fn()
39
+ .mockResolvedValueOnce(pendingResponse)
40
+ .mockResolvedValueOnce(pendingResponse)
41
+ .mockResolvedValueOnce(successResponse);
42
+ const result = await pollUntilComplete({ fetchPoll });
43
+ expect(result).toEqual({ data: "complete" });
44
+ expect(fetchPoll).toHaveBeenCalledTimes(3);
45
+ });
46
+ it("should use custom success and pending status codes", async () => {
47
+ const pendingResponse = {
48
+ ok: true,
49
+ status: 301,
50
+ };
51
+ const successResponse = {
52
+ ok: true,
53
+ status: 204,
54
+ json: vi.fn().mockResolvedValue({ done: true }),
55
+ };
56
+ const fetchPoll = vi
57
+ .fn()
58
+ .mockResolvedValueOnce(pendingResponse)
59
+ .mockResolvedValueOnce(successResponse);
60
+ const result = await pollUntilComplete({
61
+ fetchPoll,
62
+ successStatus: 204,
63
+ pendingStatus: 301,
64
+ });
65
+ expect(result).toEqual({ done: true });
66
+ expect(fetchPoll).toHaveBeenCalledTimes(2);
67
+ });
68
+ it("should apply result extractor function", async () => {
69
+ const mockResponse = {
70
+ ok: true,
71
+ status: 200,
72
+ json: vi.fn().mockResolvedValue({
73
+ nested: { data: { value: "extracted" } },
74
+ }),
75
+ };
76
+ const fetchPoll = vi.fn().mockResolvedValue(mockResponse);
77
+ const resultExtractor = (response) => response.nested.data.value;
78
+ const result = await pollUntilComplete({
79
+ fetchPoll,
80
+ resultExtractor,
81
+ });
82
+ expect(result).toBe("extracted");
83
+ });
84
+ });
85
+ describe("initial delay", () => {
86
+ it("should wait for initial delay before first poll", async () => {
87
+ const mockResponse = {
88
+ ok: true,
89
+ status: 200,
90
+ json: vi.fn().mockResolvedValue({ data: "success" }),
91
+ };
92
+ const fetchPoll = vi.fn().mockResolvedValue(mockResponse);
93
+ const { setTimeout: mockSetTimeout } = await import("timers/promises");
94
+ const result = await pollUntilComplete({
95
+ fetchPoll,
96
+ initialDelay: 500,
97
+ });
98
+ expect(result).toEqual({ data: "success" });
99
+ expect(fetchPoll).toHaveBeenCalledTimes(1);
100
+ // Verify setTimeout was called with the initial delay
101
+ expect(mockSetTimeout).toHaveBeenCalledWith(500);
102
+ });
103
+ });
104
+ describe("error handling", () => {
105
+ it("should retry on transient network errors", async () => {
106
+ const successResponse = {
107
+ ok: true,
108
+ status: 200,
109
+ json: vi.fn().mockResolvedValue({ data: "recovered" }),
110
+ };
111
+ const fetchPoll = vi
112
+ .fn()
113
+ .mockRejectedValueOnce(new Error("Network error"))
114
+ .mockRejectedValueOnce(new Error("Network error"))
115
+ .mockResolvedValueOnce(successResponse);
116
+ const result = await pollUntilComplete({ fetchPoll });
117
+ expect(result).toEqual({ data: "recovered" });
118
+ expect(fetchPoll).toHaveBeenCalledTimes(3);
119
+ });
120
+ it("should fail after MAX_CONSECUTIVE_ERRORS network failures", async () => {
121
+ const fetchPoll = vi
122
+ .fn()
123
+ .mockRejectedValue(new Error("Persistent network error"));
124
+ await expect(pollUntilComplete({ fetchPoll })).rejects.toThrow(ZapierApiError);
125
+ expect(fetchPoll).toHaveBeenCalledTimes(3);
126
+ });
127
+ it("should retry on HTTP error responses", async () => {
128
+ const errorResponse = {
129
+ ok: false,
130
+ status: 500,
131
+ statusText: "Internal Server Error",
132
+ };
133
+ const successResponse = {
134
+ ok: true,
135
+ status: 200,
136
+ json: vi.fn().mockResolvedValue({ data: "recovered" }),
137
+ };
138
+ const fetchPoll = vi
139
+ .fn()
140
+ .mockResolvedValueOnce(errorResponse)
141
+ .mockResolvedValueOnce(errorResponse)
142
+ .mockResolvedValueOnce(successResponse);
143
+ const result = await pollUntilComplete({ fetchPoll });
144
+ expect(result).toEqual({ data: "recovered" });
145
+ expect(fetchPoll).toHaveBeenCalledTimes(3);
146
+ });
147
+ it("should fail after MAX_CONSECUTIVE_ERRORS HTTP failures", async () => {
148
+ const errorResponse = {
149
+ ok: false,
150
+ status: 503,
151
+ statusText: "Service Unavailable",
152
+ };
153
+ const fetchPoll = vi.fn().mockResolvedValue(errorResponse);
154
+ await expect(pollUntilComplete({ fetchPoll })).rejects.toThrow(ZapierApiError);
155
+ expect(fetchPoll).toHaveBeenCalledTimes(3);
156
+ });
157
+ it("should throw error for unexpected response status", async () => {
158
+ const unexpectedResponse = {
159
+ ok: true,
160
+ status: 301, // Not success (200) or pending (202)
161
+ };
162
+ const fetchPoll = vi.fn().mockResolvedValue(unexpectedResponse);
163
+ await expect(pollUntilComplete({ fetchPoll })).rejects.toThrow("Unexpected response status");
164
+ // Will retry 3 times before failing
165
+ expect(fetchPoll).toHaveBeenCalledTimes(3);
166
+ });
167
+ it("should throw error if successful response is not valid JSON", async () => {
168
+ const mockResponse = {
169
+ ok: true,
170
+ status: 200,
171
+ json: vi.fn().mockRejectedValue(new Error("Invalid JSON")),
172
+ };
173
+ const fetchPoll = vi.fn().mockResolvedValue(mockResponse);
174
+ await expect(pollUntilComplete({ fetchPoll })).rejects.toThrow("Failed to poll after 3 consecutive errors");
175
+ // Will retry 3 times before failing
176
+ expect(fetchPoll).toHaveBeenCalledTimes(3);
177
+ });
178
+ });
179
+ describe("timeout behavior", () => {
180
+ it("should timeout after specified duration", async () => {
181
+ // Mock Date.now to simulate time passing
182
+ const originalDateNow = Date.now;
183
+ let currentTime = 1000;
184
+ Date.now = vi.fn(() => currentTime);
185
+ const pendingResponse = {
186
+ ok: true,
187
+ status: 202,
188
+ };
189
+ const fetchPoll = vi.fn().mockImplementation(() => {
190
+ // Simulate time passing with each poll
191
+ currentTime += 500;
192
+ return Promise.resolve(pendingResponse);
193
+ });
194
+ try {
195
+ await expect(pollUntilComplete({
196
+ fetchPoll,
197
+ timeoutMs: 1000,
198
+ initialDelay: 0,
199
+ })).rejects.toThrow(ZapierTimeoutError);
200
+ }
201
+ finally {
202
+ Date.now = originalDateNow;
203
+ }
204
+ });
205
+ it("should use default timeout when not specified", async () => {
206
+ // Mock Date.now to simulate time passing beyond default timeout
207
+ const originalDateNow = Date.now;
208
+ let currentTime = 1000;
209
+ Date.now = vi.fn(() => currentTime);
210
+ const pendingResponse = {
211
+ ok: true,
212
+ status: 202,
213
+ };
214
+ const fetchPoll = vi.fn().mockImplementation(() => {
215
+ // Simulate time passing beyond default timeout (180 seconds + buffer)
216
+ currentTime += 50000;
217
+ return Promise.resolve(pendingResponse);
218
+ });
219
+ try {
220
+ await expect(pollUntilComplete({
221
+ fetchPoll,
222
+ initialDelay: 0,
223
+ })).rejects.toThrow(ZapierTimeoutError);
224
+ }
225
+ finally {
226
+ Date.now = originalDateNow;
227
+ }
228
+ });
229
+ });
230
+ describe("input validation", () => {
231
+ it("should throw error for invalid timeout", async () => {
232
+ const fetchPoll = vi.fn();
233
+ await expect(pollUntilComplete({
234
+ fetchPoll,
235
+ timeoutMs: 0,
236
+ })).rejects.toThrow(ZapierValidationError);
237
+ await expect(pollUntilComplete({
238
+ fetchPoll,
239
+ timeoutMs: -1000,
240
+ })).rejects.toThrow(/Timeout must be greater than 0/);
241
+ });
242
+ it("should throw error for negative initial delay", async () => {
243
+ const fetchPoll = vi.fn();
244
+ await expect(pollUntilComplete({
245
+ fetchPoll,
246
+ initialDelay: -100,
247
+ })).rejects.toThrow(ZapierValidationError);
248
+ await expect(pollUntilComplete({
249
+ fetchPoll,
250
+ initialDelay: -1,
251
+ })).rejects.toThrow(/Initial delay must be non-negative/);
252
+ });
253
+ });
254
+ describe("edge cases", () => {
255
+ it("should handle rapid status changes", async () => {
256
+ const responses = [
257
+ { ok: true, status: 202 },
258
+ { ok: false, status: 500 },
259
+ { ok: true, status: 202 },
260
+ {
261
+ ok: true,
262
+ status: 200,
263
+ json: vi.fn().mockResolvedValue({ data: "success" }),
264
+ },
265
+ ];
266
+ let index = 0;
267
+ const fetchPoll = vi.fn().mockImplementation(() => {
268
+ const response = responses[index];
269
+ index++;
270
+ return Promise.resolve(response);
271
+ });
272
+ const result = await pollUntilComplete({ fetchPoll });
273
+ expect(result).toEqual({ data: "success" });
274
+ expect(fetchPoll).toHaveBeenCalledTimes(4);
275
+ });
276
+ it("should handle immediate success with no initial delay", async () => {
277
+ const mockResponse = {
278
+ ok: true,
279
+ status: 200,
280
+ json: vi.fn().mockResolvedValue({ instant: true }),
281
+ };
282
+ const fetchPoll = vi.fn().mockResolvedValue(mockResponse);
283
+ const result = await pollUntilComplete({
284
+ fetchPoll,
285
+ initialDelay: 0,
286
+ });
287
+ expect(result).toEqual({ instant: true });
288
+ expect(fetchPoll).toHaveBeenCalledTimes(1);
289
+ });
290
+ it("should handle very short timeout", async () => {
291
+ // Mock Date.now to simulate time passing
292
+ const originalDateNow = Date.now;
293
+ let currentTime = 1000;
294
+ Date.now = vi.fn(() => currentTime);
295
+ const pendingResponse = {
296
+ ok: true,
297
+ status: 202,
298
+ };
299
+ const fetchPoll = vi.fn().mockImplementation(() => {
300
+ // Simulate time passing beyond the short timeout
301
+ currentTime += 200;
302
+ return Promise.resolve(pendingResponse);
303
+ });
304
+ try {
305
+ await expect(pollUntilComplete({
306
+ fetchPoll,
307
+ timeoutMs: 100,
308
+ initialDelay: 0,
309
+ })).rejects.toThrow(ZapierTimeoutError);
310
+ // Should have made at least 1 attempt before timing out
311
+ expect(fetchPoll.mock.calls.length).toBeGreaterThanOrEqual(1);
312
+ }
313
+ finally {
314
+ Date.now = originalDateNow;
315
+ }
316
+ });
317
+ });
318
+ });
@@ -41,9 +41,8 @@ export interface RequestOptions {
41
41
  }) => Error | undefined;
42
42
  }
43
43
  export interface PollOptions extends RequestOptions {
44
- maxAttempts?: number;
45
44
  initialDelay?: number;
46
- maxDelay?: number;
45
+ timeoutMs?: number;
47
46
  successStatus?: number;
48
47
  pendingStatus?: number;
49
48
  resultExtractor?: (response: unknown) => unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,2BAA2B,EAC3B,uBAAuB,EACvB,iBAAiB,EACjB,oBAAoB,EACpB,6BAA6B,EAC7B,iBAAiB,EACjB,SAAS,EACT,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,6BAA6B,EAC7B,wBAAwB,EACxB,iCAAiC,EACjC,aAAa,EACb,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,6BAA6B,EAC7B,8BAA8B,EAC/B,MAAM,WAAW,CAAC;AAMnB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5E,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACvE,KAAK,EAAE,CACL,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,WAAW,GAAG;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,KACE,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE;QAC/B,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC;KACf,KAAK,KAAK,GAAG,SAAS,CAAC;CACzB;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CAClD;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CACzC;AAOD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAC9C,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAChD,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAChF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAG5D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAC5C,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AACpD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAGtE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAGhE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AAGF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAC/C,OAAO,iCAAiC,CACzC,CAAC;AAGF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC5E,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AACF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAC5C,OAAO,8BAA8B,CACtC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,2BAA2B,EAC3B,uBAAuB,EACvB,iBAAiB,EACjB,oBAAoB,EACpB,6BAA6B,EAC7B,iBAAiB,EACjB,SAAS,EACT,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,6BAA6B,EAC7B,wBAAwB,EACxB,iCAAiC,EACjC,aAAa,EACb,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,6BAA6B,EAC7B,8BAA8B,EAC/B,MAAM,WAAW,CAAC;AAMnB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5E,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACvE,KAAK,EAAE,CACL,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,WAAW,GAAG;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,KACE,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE;QAC/B,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC;KACf,KAAK,KAAK,GAAG,SAAS,CAAC;CACzB;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CAClD;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CACzC;AAOD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAC9C,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAChD,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAChF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAG5D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAC5C,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AACpD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAGtE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAGhE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AAGF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAC/C,OAAO,iCAAiC,CACzC,CAAC;AAGF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC5E,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AACF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAC5C,OAAO,8BAA8B,CACtC,CAAC"}