@revenexx/integrations-node-sdk 0.14.0 → 0.15.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/dist/index.cjs +176 -2
- package/dist/index.d.cts +117 -1
- package/dist/index.d.ts +117 -1
- package/dist/index.js +163 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,29 +23,41 @@ __export(index_exports, {
|
|
|
23
23
|
ApiKeyCredential: () => ApiKeyCredential,
|
|
24
24
|
BaseCredential: () => BaseCredential,
|
|
25
25
|
BasicAuthCredential: () => BasicAuthCredential,
|
|
26
|
+
DEFAULT_MAX_RESPONSE_BYTES: () => DEFAULT_MAX_RESPONSE_BYTES,
|
|
26
27
|
DEFAULT_RETRY_ATTEMPTS: () => DEFAULT_RETRY_ATTEMPTS,
|
|
27
28
|
DEFAULT_RETRY_DELAY_MS: () => DEFAULT_RETRY_DELAY_MS,
|
|
29
|
+
DEFAULT_RETRY_POLICY: () => DEFAULT_RETRY_POLICY,
|
|
28
30
|
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
29
31
|
MANIFEST_VERSION: () => MANIFEST_VERSION,
|
|
32
|
+
MAX_RESPONSE_BYTES: () => MAX_RESPONSE_BYTES,
|
|
30
33
|
MAX_RETRY_ATTEMPTS: () => MAX_RETRY_ATTEMPTS,
|
|
31
34
|
MAX_TIMEOUT_MS: () => MAX_TIMEOUT_MS,
|
|
32
35
|
NodeError: () => NodeError,
|
|
33
36
|
OAuth2AuthCodeCredential: () => OAuth2AuthCodeCredential,
|
|
34
37
|
OAuth2ClientCredentialsCredential: () => OAuth2ClientCredentialsCredential,
|
|
38
|
+
RetryableError: () => RetryableError,
|
|
35
39
|
SimpleValueCredential: () => SimpleValueCredential,
|
|
40
|
+
backoffDelay: () => backoffDelay,
|
|
36
41
|
buildManifest: () => buildManifest,
|
|
42
|
+
clampResponseBytes: () => clampResponseBytes,
|
|
37
43
|
extractCredentialManifest: () => extractCredentialManifest,
|
|
38
44
|
extractCredentialManifests: () => extractCredentialManifests,
|
|
39
45
|
extractManifest: () => extractManifest,
|
|
40
46
|
extractManifests: () => extractManifests,
|
|
41
47
|
isNodeWithIteration: () => isNodeWithIteration,
|
|
42
48
|
isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
|
|
49
|
+
maxBytesConfigField: () => maxBytesConfigField,
|
|
43
50
|
normalizeCredentialType: () => normalizeCredentialType,
|
|
44
51
|
normalizeLocalized: () => normalizeLocalized,
|
|
45
52
|
parsePackageMeta: () => parsePackageMeta,
|
|
53
|
+
readArrayBuffer: () => readArrayBuffer,
|
|
54
|
+
readJsonOrText: () => readJsonOrText,
|
|
55
|
+
readText: () => readText,
|
|
46
56
|
retryConfigFields: () => retryConfigFields,
|
|
47
57
|
safeFetch: () => safeFetch,
|
|
48
|
-
|
|
58
|
+
sleepWithSignal: () => sleepWithSignal,
|
|
59
|
+
timeoutConfigField: () => timeoutConfigField,
|
|
60
|
+
withRetry: () => withRetry
|
|
49
61
|
});
|
|
50
62
|
module.exports = __toCommonJS(index_exports);
|
|
51
63
|
|
|
@@ -123,6 +135,12 @@ var MAX_TIMEOUT_MS = 12e4;
|
|
|
123
135
|
var DEFAULT_RETRY_ATTEMPTS = 0;
|
|
124
136
|
var MAX_RETRY_ATTEMPTS = 5;
|
|
125
137
|
var DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
138
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024;
|
|
139
|
+
var MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
|
|
140
|
+
function clampResponseBytes(maxBytes) {
|
|
141
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
|
|
142
|
+
return Math.min(maxBytes, MAX_RESPONSE_BYTES);
|
|
143
|
+
}
|
|
126
144
|
async function safeFetch(url, options = {}) {
|
|
127
145
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
|
|
128
146
|
const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
@@ -160,6 +178,82 @@ async function safeFetch(url, options = {}) {
|
|
|
160
178
|
}
|
|
161
179
|
throw lastError;
|
|
162
180
|
}
|
|
181
|
+
function tooLargeError(status, bytes, maxBytes) {
|
|
182
|
+
return new NodeError(
|
|
183
|
+
"RESPONSE_TOO_LARGE",
|
|
184
|
+
`Response body of ${bytes} bytes exceeds the ${maxBytes}-byte limit`,
|
|
185
|
+
{ status }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
async function readArrayBuffer(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
189
|
+
const cap = clampResponseBytes(maxBytes);
|
|
190
|
+
const declared = Number(res.headers.get("content-length"));
|
|
191
|
+
if (Number.isFinite(declared) && declared > cap) {
|
|
192
|
+
throw tooLargeError(res.status, declared, cap);
|
|
193
|
+
}
|
|
194
|
+
const body = res.body;
|
|
195
|
+
if (!body) {
|
|
196
|
+
const buf = await res.arrayBuffer();
|
|
197
|
+
if (buf.byteLength > cap) throw tooLargeError(res.status, buf.byteLength, cap);
|
|
198
|
+
return buf;
|
|
199
|
+
}
|
|
200
|
+
const reader = body.getReader();
|
|
201
|
+
const chunks = [];
|
|
202
|
+
let total = 0;
|
|
203
|
+
try {
|
|
204
|
+
for (; ; ) {
|
|
205
|
+
const { done, value } = await reader.read();
|
|
206
|
+
if (done) break;
|
|
207
|
+
total += value.byteLength;
|
|
208
|
+
if (total > cap) {
|
|
209
|
+
await reader.cancel().catch(() => {
|
|
210
|
+
});
|
|
211
|
+
throw tooLargeError(res.status, total, cap);
|
|
212
|
+
}
|
|
213
|
+
chunks.push(value);
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
reader.releaseLock();
|
|
217
|
+
}
|
|
218
|
+
const out = new Uint8Array(total);
|
|
219
|
+
let offset = 0;
|
|
220
|
+
for (const chunk of chunks) {
|
|
221
|
+
out.set(chunk, offset);
|
|
222
|
+
offset += chunk.byteLength;
|
|
223
|
+
}
|
|
224
|
+
return out.buffer;
|
|
225
|
+
}
|
|
226
|
+
async function readText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
227
|
+
const buf = await readArrayBuffer(res, maxBytes);
|
|
228
|
+
return new TextDecoder().decode(buf);
|
|
229
|
+
}
|
|
230
|
+
function isJsonContentType(contentType) {
|
|
231
|
+
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
232
|
+
return mediaType === "application/json" || mediaType.endsWith("+json");
|
|
233
|
+
}
|
|
234
|
+
async function readJsonOrText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
235
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
236
|
+
const text = await readText(res, maxBytes);
|
|
237
|
+
if (!isJsonContentType(contentType)) return text;
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(text);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
throw new NodeError(
|
|
242
|
+
"RESPONSE_PARSE_ERROR",
|
|
243
|
+
`Invalid JSON response: ${e.message}`,
|
|
244
|
+
{ status: res.status }
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function maxBytesConfigField(opts) {
|
|
249
|
+
return {
|
|
250
|
+
key: "maxBytes",
|
|
251
|
+
label: "Max response size (bytes)",
|
|
252
|
+
type: "number",
|
|
253
|
+
default: opts?.default ?? DEFAULT_MAX_RESPONSE_BYTES,
|
|
254
|
+
validation: { min: 1, max: Math.min(opts?.max ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
163
257
|
function timeoutConfigField(opts) {
|
|
164
258
|
return {
|
|
165
259
|
key: "timeoutMs",
|
|
@@ -188,6 +282,74 @@ function retryConfigFields(opts) {
|
|
|
188
282
|
];
|
|
189
283
|
}
|
|
190
284
|
|
|
285
|
+
// src/retry.ts
|
|
286
|
+
var RetryableError = class extends Error {
|
|
287
|
+
/** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
|
|
288
|
+
retryAfterMs;
|
|
289
|
+
constructor(message, opts) {
|
|
290
|
+
super(message, { cause: opts?.cause });
|
|
291
|
+
this.name = "RetryableError";
|
|
292
|
+
this.retryAfterMs = opts?.retryAfterMs;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
var DEFAULT_RETRY_POLICY = {
|
|
296
|
+
maxAttempts: 3,
|
|
297
|
+
baseDelayMs: 500,
|
|
298
|
+
maxDelayMs: 3e4,
|
|
299
|
+
factor: 2,
|
|
300
|
+
jitter: true
|
|
301
|
+
};
|
|
302
|
+
function abortReason(signal) {
|
|
303
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
304
|
+
}
|
|
305
|
+
function backoffDelay(attempt, policy) {
|
|
306
|
+
const exponent = Math.max(0, attempt - 1);
|
|
307
|
+
const raw = policy.baseDelayMs * Math.pow(policy.factor, exponent);
|
|
308
|
+
const cap = Math.min(policy.maxDelayMs, raw);
|
|
309
|
+
return policy.jitter ? Math.random() * cap : cap;
|
|
310
|
+
}
|
|
311
|
+
function sleepWithSignal(ms, signal) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
if (signal.aborted) {
|
|
314
|
+
reject(abortReason(signal));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const onAbort = () => {
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
reject(abortReason(signal));
|
|
320
|
+
};
|
|
321
|
+
const timer = setTimeout(() => {
|
|
322
|
+
signal.removeEventListener("abort", onAbort);
|
|
323
|
+
resolve();
|
|
324
|
+
}, ms);
|
|
325
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function withRetry(fn, policy, hooks) {
|
|
329
|
+
const effective = { ...DEFAULT_RETRY_POLICY, ...policy };
|
|
330
|
+
const maxAttempts = Math.max(1, effective.maxAttempts);
|
|
331
|
+
const { signal, logger, onRetry } = hooks;
|
|
332
|
+
for (let attempt = 1; ; attempt++) {
|
|
333
|
+
if (signal.aborted) throw abortReason(signal);
|
|
334
|
+
try {
|
|
335
|
+
return await fn(attempt);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (!(err instanceof RetryableError) || attempt >= maxAttempts) throw err;
|
|
338
|
+
const retryAfter = err.retryAfterMs;
|
|
339
|
+
const rawDelay = typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : backoffDelay(attempt, effective);
|
|
340
|
+
const delayMs = Number.isFinite(rawDelay) && rawDelay >= 0 ? rawDelay : 0;
|
|
341
|
+
onRetry?.({ attempt, delayMs, error: err });
|
|
342
|
+
logger?.warn("Retrying after retryable error", {
|
|
343
|
+
attempt,
|
|
344
|
+
maxAttempts,
|
|
345
|
+
delayMs,
|
|
346
|
+
error: err.message
|
|
347
|
+
});
|
|
348
|
+
await sleepWithSignal(delayMs, signal);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
191
353
|
// src/manifest.ts
|
|
192
354
|
var MANIFEST_VERSION = "v0-draft";
|
|
193
355
|
function parsePackageMeta(raw) {
|
|
@@ -424,27 +586,39 @@ function base64Url(buf) {
|
|
|
424
586
|
ApiKeyCredential,
|
|
425
587
|
BaseCredential,
|
|
426
588
|
BasicAuthCredential,
|
|
589
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
427
590
|
DEFAULT_RETRY_ATTEMPTS,
|
|
428
591
|
DEFAULT_RETRY_DELAY_MS,
|
|
592
|
+
DEFAULT_RETRY_POLICY,
|
|
429
593
|
DEFAULT_TIMEOUT_MS,
|
|
430
594
|
MANIFEST_VERSION,
|
|
595
|
+
MAX_RESPONSE_BYTES,
|
|
431
596
|
MAX_RETRY_ATTEMPTS,
|
|
432
597
|
MAX_TIMEOUT_MS,
|
|
433
598
|
NodeError,
|
|
434
599
|
OAuth2AuthCodeCredential,
|
|
435
600
|
OAuth2ClientCredentialsCredential,
|
|
601
|
+
RetryableError,
|
|
436
602
|
SimpleValueCredential,
|
|
603
|
+
backoffDelay,
|
|
437
604
|
buildManifest,
|
|
605
|
+
clampResponseBytes,
|
|
438
606
|
extractCredentialManifest,
|
|
439
607
|
extractCredentialManifests,
|
|
440
608
|
extractManifest,
|
|
441
609
|
extractManifests,
|
|
442
610
|
isNodeWithIteration,
|
|
443
611
|
isOAuthAuthorizeCredential,
|
|
612
|
+
maxBytesConfigField,
|
|
444
613
|
normalizeCredentialType,
|
|
445
614
|
normalizeLocalized,
|
|
446
615
|
parsePackageMeta,
|
|
616
|
+
readArrayBuffer,
|
|
617
|
+
readJsonOrText,
|
|
618
|
+
readText,
|
|
447
619
|
retryConfigFields,
|
|
448
620
|
safeFetch,
|
|
449
|
-
|
|
621
|
+
sleepWithSignal,
|
|
622
|
+
timeoutConfigField,
|
|
623
|
+
withRetry
|
|
450
624
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -434,6 +434,21 @@ declare const MAX_TIMEOUT_MS = 120000;
|
|
|
434
434
|
declare const DEFAULT_RETRY_ATTEMPTS = 0;
|
|
435
435
|
declare const MAX_RETRY_ATTEMPTS = 5;
|
|
436
436
|
declare const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
437
|
+
/**
|
|
438
|
+
* Default cap for response bodies read via {@link readArrayBuffer} /
|
|
439
|
+
* {@link readText} / {@link readJsonOrText}. Guards the (shared) worker process
|
|
440
|
+
* against a single oversized response exhausting its memory.
|
|
441
|
+
*/
|
|
442
|
+
declare const DEFAULT_MAX_RESPONSE_BYTES: number;
|
|
443
|
+
/**
|
|
444
|
+
* Hard upper ceiling for any per-node `maxBytes`. Even a permissive node author
|
|
445
|
+
* (who calls {@link maxBytesConfigField} without an explicit `max`, or passes a
|
|
446
|
+
* large `maxBytes` to the `read*` helpers) can never lift the cap above this, so
|
|
447
|
+
* the shared worker's memory stays bounded. Analogous to {@link MAX_TIMEOUT_MS}.
|
|
448
|
+
*/
|
|
449
|
+
declare const MAX_RESPONSE_BYTES: number;
|
|
450
|
+
/** Clamp a requested `maxBytes` into `[1, MAX_RESPONSE_BYTES]`. */
|
|
451
|
+
declare function clampResponseBytes(maxBytes: number): number;
|
|
437
452
|
interface SafeFetchRetry {
|
|
438
453
|
attempts: number;
|
|
439
454
|
delayMs?: number;
|
|
@@ -445,6 +460,31 @@ interface SafeFetchOptions extends RequestInit {
|
|
|
445
460
|
retry?: SafeFetchRetry;
|
|
446
461
|
}
|
|
447
462
|
declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
|
|
463
|
+
/**
|
|
464
|
+
* Read a response body into an ArrayBuffer while enforcing a hard byte cap.
|
|
465
|
+
*
|
|
466
|
+
* The `Content-Length` header is used as a fast-reject (bail before downloading
|
|
467
|
+
* anything), but the limit is *also* enforced while streaming, since the header
|
|
468
|
+
* can be absent or lie. On overrun the stream is cancelled and a
|
|
469
|
+
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown.
|
|
470
|
+
*/
|
|
471
|
+
declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
|
|
472
|
+
/** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
|
|
473
|
+
declare function readText(res: Response, maxBytes?: number): Promise<string>;
|
|
474
|
+
/**
|
|
475
|
+
* Read a response body as JSON when the `Content-Type` denotes JSON (see
|
|
476
|
+
* {@link isJsonContentType}), otherwise as text — the content-type sniff
|
|
477
|
+
* previously duplicated across the HTTP/Upload/DeepL node sinks. Capped at
|
|
478
|
+
* `maxBytes` (see {@link readArrayBuffer}).
|
|
479
|
+
*
|
|
480
|
+
* A malformed JSON body surfaces as `NodeError('RESPONSE_PARSE_ERROR', …, { status })`
|
|
481
|
+
* rather than a raw `SyntaxError`, keeping to the SDK error contract.
|
|
482
|
+
*/
|
|
483
|
+
declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
|
|
484
|
+
declare function maxBytesConfigField(opts?: {
|
|
485
|
+
default?: number;
|
|
486
|
+
max?: number;
|
|
487
|
+
}): IConfigField;
|
|
448
488
|
declare function timeoutConfigField(opts?: {
|
|
449
489
|
default?: number;
|
|
450
490
|
max?: number;
|
|
@@ -454,6 +494,82 @@ declare function retryConfigFields(opts?: {
|
|
|
454
494
|
defaultDelayMs?: number;
|
|
455
495
|
}): IConfigField[];
|
|
456
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Transport-agnostic retry/backoff primitive (PO-139).
|
|
499
|
+
*
|
|
500
|
+
* Wraps *any* async operation — raw `fetch`, an SDK client (axios), a future
|
|
501
|
+
* transport — with exponential backoff + full jitter, honouring a
|
|
502
|
+
* server-dictated delay (HTTP `Retry-After`) when the caller supplies one.
|
|
503
|
+
*
|
|
504
|
+
* The retry decision lives with the connector: it throws a {@link RetryableError}
|
|
505
|
+
* only when an attempt failed *and* is worth retrying. Everything else is
|
|
506
|
+
* rethrown as-is, and terminal API errors that connectors model as *values*
|
|
507
|
+
* (e.g. a `{ kind: 'http-error' }` union) simply flow back as the return value.
|
|
508
|
+
*
|
|
509
|
+
* This is deliberately NOT a circuit breaker, rate limiter, or HTTP client, and
|
|
510
|
+
* it does not replace Temporal activity-level retries — it is the fine-grained,
|
|
511
|
+
* idempotency-aware, `Retry-After`-honouring layer inside a single activity.
|
|
512
|
+
*/
|
|
513
|
+
/** Thrown by the wrapped fn to signal "this attempt failed but is retryable". */
|
|
514
|
+
declare class RetryableError extends Error {
|
|
515
|
+
/** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
|
|
516
|
+
readonly retryAfterMs?: number;
|
|
517
|
+
constructor(message: string, opts?: {
|
|
518
|
+
retryAfterMs?: number;
|
|
519
|
+
cause?: unknown;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
interface RetryPolicy {
|
|
523
|
+
/** Total tries including the first. */
|
|
524
|
+
maxAttempts: number;
|
|
525
|
+
/** Base delay for the exponential backoff. */
|
|
526
|
+
baseDelayMs: number;
|
|
527
|
+
/** Upper bound for any single computed delay. */
|
|
528
|
+
maxDelayMs: number;
|
|
529
|
+
/** Exponential growth factor. */
|
|
530
|
+
factor: number;
|
|
531
|
+
/** Apply full jitter to the computed backoff. */
|
|
532
|
+
jitter: boolean;
|
|
533
|
+
}
|
|
534
|
+
declare const DEFAULT_RETRY_POLICY: RetryPolicy;
|
|
535
|
+
interface RetryHooks {
|
|
536
|
+
/** Pass `ctx.signal` so cancelling a workflow cancels the wait (and stops retrying) immediately. */
|
|
537
|
+
signal: AbortSignal;
|
|
538
|
+
logger?: {
|
|
539
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
540
|
+
};
|
|
541
|
+
onRetry?(info: {
|
|
542
|
+
attempt: number;
|
|
543
|
+
delayMs: number;
|
|
544
|
+
error: unknown;
|
|
545
|
+
}): void;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Pure backoff math, exported for testing/reuse. Returns the delay before the
|
|
549
|
+
* retry that follows the given (1-based) failed `attempt`: an exponential term
|
|
550
|
+
* `baseDelayMs * factor^(attempt-1)` capped at `maxDelayMs`, with full jitter
|
|
551
|
+
* (`random() * cap`) when `policy.jitter` is set.
|
|
552
|
+
*/
|
|
553
|
+
declare function backoffDelay(attempt: number, policy: RetryPolicy): number;
|
|
554
|
+
/**
|
|
555
|
+
* Abort-aware sleep. Resolves after `ms`, or rejects with the signal's reason
|
|
556
|
+
* (a DOMException `AbortError` by default) as soon as `signal` aborts — so a
|
|
557
|
+
* caller's retry loop stops cleanly instead of finishing the wait.
|
|
558
|
+
*/
|
|
559
|
+
declare function sleepWithSignal(ms: number, signal: AbortSignal): Promise<void>;
|
|
560
|
+
/**
|
|
561
|
+
* Runs `fn`; if it throws a {@link RetryableError} and attempts remain, waits
|
|
562
|
+
* (`RetryableError.retryAfterMs` when present, else the computed
|
|
563
|
+
* {@link backoffDelay}) respecting `signal`, then retries. Any other throw is
|
|
564
|
+
* rethrown immediately; on the final attempt the RetryableError itself is
|
|
565
|
+
* thrown (preserving its `retryAfterMs`/`cause`), not its `.cause`.
|
|
566
|
+
*
|
|
567
|
+
* Distinct from `safeFetch`'s built-in fixed-delay `retry` option, which is
|
|
568
|
+
* unchanged: that is a simple in-fetch retry; this is the richer, transport-
|
|
569
|
+
* agnostic, `Retry-After`-aware primitive connectors wrap around any operation.
|
|
570
|
+
*/
|
|
571
|
+
declare function withRetry<T>(fn: (attempt: number) => Promise<T>, policy: Partial<RetryPolicy>, hooks: RetryHooks): Promise<T>;
|
|
572
|
+
|
|
457
573
|
/**
|
|
458
574
|
* Envelope version expected by the integrations server-side
|
|
459
575
|
* `TarballInspector`. Earlier (pre-registry) builds emitted a bare array
|
|
@@ -624,4 +740,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
|
|
|
624
740
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
625
741
|
}
|
|
626
742
|
|
|
627
|
-
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, parsePackageMeta, retryConfigFields, safeFetch, timeoutConfigField };
|
|
743
|
+
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, backoffDelay, buildManifest, clampResponseBytes, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, sleepWithSignal, timeoutConfigField, withRetry };
|
package/dist/index.d.ts
CHANGED
|
@@ -434,6 +434,21 @@ declare const MAX_TIMEOUT_MS = 120000;
|
|
|
434
434
|
declare const DEFAULT_RETRY_ATTEMPTS = 0;
|
|
435
435
|
declare const MAX_RETRY_ATTEMPTS = 5;
|
|
436
436
|
declare const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
437
|
+
/**
|
|
438
|
+
* Default cap for response bodies read via {@link readArrayBuffer} /
|
|
439
|
+
* {@link readText} / {@link readJsonOrText}. Guards the (shared) worker process
|
|
440
|
+
* against a single oversized response exhausting its memory.
|
|
441
|
+
*/
|
|
442
|
+
declare const DEFAULT_MAX_RESPONSE_BYTES: number;
|
|
443
|
+
/**
|
|
444
|
+
* Hard upper ceiling for any per-node `maxBytes`. Even a permissive node author
|
|
445
|
+
* (who calls {@link maxBytesConfigField} without an explicit `max`, or passes a
|
|
446
|
+
* large `maxBytes` to the `read*` helpers) can never lift the cap above this, so
|
|
447
|
+
* the shared worker's memory stays bounded. Analogous to {@link MAX_TIMEOUT_MS}.
|
|
448
|
+
*/
|
|
449
|
+
declare const MAX_RESPONSE_BYTES: number;
|
|
450
|
+
/** Clamp a requested `maxBytes` into `[1, MAX_RESPONSE_BYTES]`. */
|
|
451
|
+
declare function clampResponseBytes(maxBytes: number): number;
|
|
437
452
|
interface SafeFetchRetry {
|
|
438
453
|
attempts: number;
|
|
439
454
|
delayMs?: number;
|
|
@@ -445,6 +460,31 @@ interface SafeFetchOptions extends RequestInit {
|
|
|
445
460
|
retry?: SafeFetchRetry;
|
|
446
461
|
}
|
|
447
462
|
declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
|
|
463
|
+
/**
|
|
464
|
+
* Read a response body into an ArrayBuffer while enforcing a hard byte cap.
|
|
465
|
+
*
|
|
466
|
+
* The `Content-Length` header is used as a fast-reject (bail before downloading
|
|
467
|
+
* anything), but the limit is *also* enforced while streaming, since the header
|
|
468
|
+
* can be absent or lie. On overrun the stream is cancelled and a
|
|
469
|
+
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown.
|
|
470
|
+
*/
|
|
471
|
+
declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
|
|
472
|
+
/** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
|
|
473
|
+
declare function readText(res: Response, maxBytes?: number): Promise<string>;
|
|
474
|
+
/**
|
|
475
|
+
* Read a response body as JSON when the `Content-Type` denotes JSON (see
|
|
476
|
+
* {@link isJsonContentType}), otherwise as text — the content-type sniff
|
|
477
|
+
* previously duplicated across the HTTP/Upload/DeepL node sinks. Capped at
|
|
478
|
+
* `maxBytes` (see {@link readArrayBuffer}).
|
|
479
|
+
*
|
|
480
|
+
* A malformed JSON body surfaces as `NodeError('RESPONSE_PARSE_ERROR', …, { status })`
|
|
481
|
+
* rather than a raw `SyntaxError`, keeping to the SDK error contract.
|
|
482
|
+
*/
|
|
483
|
+
declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
|
|
484
|
+
declare function maxBytesConfigField(opts?: {
|
|
485
|
+
default?: number;
|
|
486
|
+
max?: number;
|
|
487
|
+
}): IConfigField;
|
|
448
488
|
declare function timeoutConfigField(opts?: {
|
|
449
489
|
default?: number;
|
|
450
490
|
max?: number;
|
|
@@ -454,6 +494,82 @@ declare function retryConfigFields(opts?: {
|
|
|
454
494
|
defaultDelayMs?: number;
|
|
455
495
|
}): IConfigField[];
|
|
456
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Transport-agnostic retry/backoff primitive (PO-139).
|
|
499
|
+
*
|
|
500
|
+
* Wraps *any* async operation — raw `fetch`, an SDK client (axios), a future
|
|
501
|
+
* transport — with exponential backoff + full jitter, honouring a
|
|
502
|
+
* server-dictated delay (HTTP `Retry-After`) when the caller supplies one.
|
|
503
|
+
*
|
|
504
|
+
* The retry decision lives with the connector: it throws a {@link RetryableError}
|
|
505
|
+
* only when an attempt failed *and* is worth retrying. Everything else is
|
|
506
|
+
* rethrown as-is, and terminal API errors that connectors model as *values*
|
|
507
|
+
* (e.g. a `{ kind: 'http-error' }` union) simply flow back as the return value.
|
|
508
|
+
*
|
|
509
|
+
* This is deliberately NOT a circuit breaker, rate limiter, or HTTP client, and
|
|
510
|
+
* it does not replace Temporal activity-level retries — it is the fine-grained,
|
|
511
|
+
* idempotency-aware, `Retry-After`-honouring layer inside a single activity.
|
|
512
|
+
*/
|
|
513
|
+
/** Thrown by the wrapped fn to signal "this attempt failed but is retryable". */
|
|
514
|
+
declare class RetryableError extends Error {
|
|
515
|
+
/** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
|
|
516
|
+
readonly retryAfterMs?: number;
|
|
517
|
+
constructor(message: string, opts?: {
|
|
518
|
+
retryAfterMs?: number;
|
|
519
|
+
cause?: unknown;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
interface RetryPolicy {
|
|
523
|
+
/** Total tries including the first. */
|
|
524
|
+
maxAttempts: number;
|
|
525
|
+
/** Base delay for the exponential backoff. */
|
|
526
|
+
baseDelayMs: number;
|
|
527
|
+
/** Upper bound for any single computed delay. */
|
|
528
|
+
maxDelayMs: number;
|
|
529
|
+
/** Exponential growth factor. */
|
|
530
|
+
factor: number;
|
|
531
|
+
/** Apply full jitter to the computed backoff. */
|
|
532
|
+
jitter: boolean;
|
|
533
|
+
}
|
|
534
|
+
declare const DEFAULT_RETRY_POLICY: RetryPolicy;
|
|
535
|
+
interface RetryHooks {
|
|
536
|
+
/** Pass `ctx.signal` so cancelling a workflow cancels the wait (and stops retrying) immediately. */
|
|
537
|
+
signal: AbortSignal;
|
|
538
|
+
logger?: {
|
|
539
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
540
|
+
};
|
|
541
|
+
onRetry?(info: {
|
|
542
|
+
attempt: number;
|
|
543
|
+
delayMs: number;
|
|
544
|
+
error: unknown;
|
|
545
|
+
}): void;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Pure backoff math, exported for testing/reuse. Returns the delay before the
|
|
549
|
+
* retry that follows the given (1-based) failed `attempt`: an exponential term
|
|
550
|
+
* `baseDelayMs * factor^(attempt-1)` capped at `maxDelayMs`, with full jitter
|
|
551
|
+
* (`random() * cap`) when `policy.jitter` is set.
|
|
552
|
+
*/
|
|
553
|
+
declare function backoffDelay(attempt: number, policy: RetryPolicy): number;
|
|
554
|
+
/**
|
|
555
|
+
* Abort-aware sleep. Resolves after `ms`, or rejects with the signal's reason
|
|
556
|
+
* (a DOMException `AbortError` by default) as soon as `signal` aborts — so a
|
|
557
|
+
* caller's retry loop stops cleanly instead of finishing the wait.
|
|
558
|
+
*/
|
|
559
|
+
declare function sleepWithSignal(ms: number, signal: AbortSignal): Promise<void>;
|
|
560
|
+
/**
|
|
561
|
+
* Runs `fn`; if it throws a {@link RetryableError} and attempts remain, waits
|
|
562
|
+
* (`RetryableError.retryAfterMs` when present, else the computed
|
|
563
|
+
* {@link backoffDelay}) respecting `signal`, then retries. Any other throw is
|
|
564
|
+
* rethrown immediately; on the final attempt the RetryableError itself is
|
|
565
|
+
* thrown (preserving its `retryAfterMs`/`cause`), not its `.cause`.
|
|
566
|
+
*
|
|
567
|
+
* Distinct from `safeFetch`'s built-in fixed-delay `retry` option, which is
|
|
568
|
+
* unchanged: that is a simple in-fetch retry; this is the richer, transport-
|
|
569
|
+
* agnostic, `Retry-After`-aware primitive connectors wrap around any operation.
|
|
570
|
+
*/
|
|
571
|
+
declare function withRetry<T>(fn: (attempt: number) => Promise<T>, policy: Partial<RetryPolicy>, hooks: RetryHooks): Promise<T>;
|
|
572
|
+
|
|
457
573
|
/**
|
|
458
574
|
* Envelope version expected by the integrations server-side
|
|
459
575
|
* `TarballInspector`. Earlier (pre-registry) builds emitted a bare array
|
|
@@ -624,4 +740,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
|
|
|
624
740
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
625
741
|
}
|
|
626
742
|
|
|
627
|
-
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, parsePackageMeta, retryConfigFields, safeFetch, timeoutConfigField };
|
|
743
|
+
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, backoffDelay, buildManifest, clampResponseBytes, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, sleepWithSignal, timeoutConfigField, withRetry };
|
package/dist/index.js
CHANGED
|
@@ -68,6 +68,12 @@ var MAX_TIMEOUT_MS = 12e4;
|
|
|
68
68
|
var DEFAULT_RETRY_ATTEMPTS = 0;
|
|
69
69
|
var MAX_RETRY_ATTEMPTS = 5;
|
|
70
70
|
var DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
71
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024;
|
|
72
|
+
var MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
|
|
73
|
+
function clampResponseBytes(maxBytes) {
|
|
74
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
|
|
75
|
+
return Math.min(maxBytes, MAX_RESPONSE_BYTES);
|
|
76
|
+
}
|
|
71
77
|
async function safeFetch(url, options = {}) {
|
|
72
78
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
|
|
73
79
|
const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
@@ -105,6 +111,82 @@ async function safeFetch(url, options = {}) {
|
|
|
105
111
|
}
|
|
106
112
|
throw lastError;
|
|
107
113
|
}
|
|
114
|
+
function tooLargeError(status, bytes, maxBytes) {
|
|
115
|
+
return new NodeError(
|
|
116
|
+
"RESPONSE_TOO_LARGE",
|
|
117
|
+
`Response body of ${bytes} bytes exceeds the ${maxBytes}-byte limit`,
|
|
118
|
+
{ status }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
async function readArrayBuffer(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
122
|
+
const cap = clampResponseBytes(maxBytes);
|
|
123
|
+
const declared = Number(res.headers.get("content-length"));
|
|
124
|
+
if (Number.isFinite(declared) && declared > cap) {
|
|
125
|
+
throw tooLargeError(res.status, declared, cap);
|
|
126
|
+
}
|
|
127
|
+
const body = res.body;
|
|
128
|
+
if (!body) {
|
|
129
|
+
const buf = await res.arrayBuffer();
|
|
130
|
+
if (buf.byteLength > cap) throw tooLargeError(res.status, buf.byteLength, cap);
|
|
131
|
+
return buf;
|
|
132
|
+
}
|
|
133
|
+
const reader = body.getReader();
|
|
134
|
+
const chunks = [];
|
|
135
|
+
let total = 0;
|
|
136
|
+
try {
|
|
137
|
+
for (; ; ) {
|
|
138
|
+
const { done, value } = await reader.read();
|
|
139
|
+
if (done) break;
|
|
140
|
+
total += value.byteLength;
|
|
141
|
+
if (total > cap) {
|
|
142
|
+
await reader.cancel().catch(() => {
|
|
143
|
+
});
|
|
144
|
+
throw tooLargeError(res.status, total, cap);
|
|
145
|
+
}
|
|
146
|
+
chunks.push(value);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
reader.releaseLock();
|
|
150
|
+
}
|
|
151
|
+
const out = new Uint8Array(total);
|
|
152
|
+
let offset = 0;
|
|
153
|
+
for (const chunk of chunks) {
|
|
154
|
+
out.set(chunk, offset);
|
|
155
|
+
offset += chunk.byteLength;
|
|
156
|
+
}
|
|
157
|
+
return out.buffer;
|
|
158
|
+
}
|
|
159
|
+
async function readText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
160
|
+
const buf = await readArrayBuffer(res, maxBytes);
|
|
161
|
+
return new TextDecoder().decode(buf);
|
|
162
|
+
}
|
|
163
|
+
function isJsonContentType(contentType) {
|
|
164
|
+
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
165
|
+
return mediaType === "application/json" || mediaType.endsWith("+json");
|
|
166
|
+
}
|
|
167
|
+
async function readJsonOrText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
168
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
169
|
+
const text = await readText(res, maxBytes);
|
|
170
|
+
if (!isJsonContentType(contentType)) return text;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(text);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
throw new NodeError(
|
|
175
|
+
"RESPONSE_PARSE_ERROR",
|
|
176
|
+
`Invalid JSON response: ${e.message}`,
|
|
177
|
+
{ status: res.status }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function maxBytesConfigField(opts) {
|
|
182
|
+
return {
|
|
183
|
+
key: "maxBytes",
|
|
184
|
+
label: "Max response size (bytes)",
|
|
185
|
+
type: "number",
|
|
186
|
+
default: opts?.default ?? DEFAULT_MAX_RESPONSE_BYTES,
|
|
187
|
+
validation: { min: 1, max: Math.min(opts?.max ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) }
|
|
188
|
+
};
|
|
189
|
+
}
|
|
108
190
|
function timeoutConfigField(opts) {
|
|
109
191
|
return {
|
|
110
192
|
key: "timeoutMs",
|
|
@@ -133,6 +215,74 @@ function retryConfigFields(opts) {
|
|
|
133
215
|
];
|
|
134
216
|
}
|
|
135
217
|
|
|
218
|
+
// src/retry.ts
|
|
219
|
+
var RetryableError = class extends Error {
|
|
220
|
+
/** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
|
|
221
|
+
retryAfterMs;
|
|
222
|
+
constructor(message, opts) {
|
|
223
|
+
super(message, { cause: opts?.cause });
|
|
224
|
+
this.name = "RetryableError";
|
|
225
|
+
this.retryAfterMs = opts?.retryAfterMs;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var DEFAULT_RETRY_POLICY = {
|
|
229
|
+
maxAttempts: 3,
|
|
230
|
+
baseDelayMs: 500,
|
|
231
|
+
maxDelayMs: 3e4,
|
|
232
|
+
factor: 2,
|
|
233
|
+
jitter: true
|
|
234
|
+
};
|
|
235
|
+
function abortReason(signal) {
|
|
236
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
237
|
+
}
|
|
238
|
+
function backoffDelay(attempt, policy) {
|
|
239
|
+
const exponent = Math.max(0, attempt - 1);
|
|
240
|
+
const raw = policy.baseDelayMs * Math.pow(policy.factor, exponent);
|
|
241
|
+
const cap = Math.min(policy.maxDelayMs, raw);
|
|
242
|
+
return policy.jitter ? Math.random() * cap : cap;
|
|
243
|
+
}
|
|
244
|
+
function sleepWithSignal(ms, signal) {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
if (signal.aborted) {
|
|
247
|
+
reject(abortReason(signal));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const onAbort = () => {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
reject(abortReason(signal));
|
|
253
|
+
};
|
|
254
|
+
const timer = setTimeout(() => {
|
|
255
|
+
signal.removeEventListener("abort", onAbort);
|
|
256
|
+
resolve();
|
|
257
|
+
}, ms);
|
|
258
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async function withRetry(fn, policy, hooks) {
|
|
262
|
+
const effective = { ...DEFAULT_RETRY_POLICY, ...policy };
|
|
263
|
+
const maxAttempts = Math.max(1, effective.maxAttempts);
|
|
264
|
+
const { signal, logger, onRetry } = hooks;
|
|
265
|
+
for (let attempt = 1; ; attempt++) {
|
|
266
|
+
if (signal.aborted) throw abortReason(signal);
|
|
267
|
+
try {
|
|
268
|
+
return await fn(attempt);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (!(err instanceof RetryableError) || attempt >= maxAttempts) throw err;
|
|
271
|
+
const retryAfter = err.retryAfterMs;
|
|
272
|
+
const rawDelay = typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : backoffDelay(attempt, effective);
|
|
273
|
+
const delayMs = Number.isFinite(rawDelay) && rawDelay >= 0 ? rawDelay : 0;
|
|
274
|
+
onRetry?.({ attempt, delayMs, error: err });
|
|
275
|
+
logger?.warn("Retrying after retryable error", {
|
|
276
|
+
attempt,
|
|
277
|
+
maxAttempts,
|
|
278
|
+
delayMs,
|
|
279
|
+
error: err.message
|
|
280
|
+
});
|
|
281
|
+
await sleepWithSignal(delayMs, signal);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
136
286
|
// src/credentials.ts
|
|
137
287
|
import { createHash, randomBytes } from "crypto";
|
|
138
288
|
function readString(source, key) {
|
|
@@ -341,27 +491,39 @@ export {
|
|
|
341
491
|
ApiKeyCredential,
|
|
342
492
|
BaseCredential,
|
|
343
493
|
BasicAuthCredential,
|
|
494
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
344
495
|
DEFAULT_RETRY_ATTEMPTS,
|
|
345
496
|
DEFAULT_RETRY_DELAY_MS,
|
|
497
|
+
DEFAULT_RETRY_POLICY,
|
|
346
498
|
DEFAULT_TIMEOUT_MS,
|
|
347
499
|
MANIFEST_VERSION,
|
|
500
|
+
MAX_RESPONSE_BYTES,
|
|
348
501
|
MAX_RETRY_ATTEMPTS,
|
|
349
502
|
MAX_TIMEOUT_MS,
|
|
350
503
|
NodeError,
|
|
351
504
|
OAuth2AuthCodeCredential,
|
|
352
505
|
OAuth2ClientCredentialsCredential,
|
|
506
|
+
RetryableError,
|
|
353
507
|
SimpleValueCredential,
|
|
508
|
+
backoffDelay,
|
|
354
509
|
buildManifest,
|
|
510
|
+
clampResponseBytes,
|
|
355
511
|
extractCredentialManifest,
|
|
356
512
|
extractCredentialManifests,
|
|
357
513
|
extractManifest,
|
|
358
514
|
extractManifests,
|
|
359
515
|
isNodeWithIteration,
|
|
360
516
|
isOAuthAuthorizeCredential,
|
|
517
|
+
maxBytesConfigField,
|
|
361
518
|
normalizeCredentialType,
|
|
362
519
|
normalizeLocalized,
|
|
363
520
|
parsePackageMeta,
|
|
521
|
+
readArrayBuffer,
|
|
522
|
+
readJsonOrText,
|
|
523
|
+
readText,
|
|
364
524
|
retryConfigFields,
|
|
365
525
|
safeFetch,
|
|
366
|
-
|
|
526
|
+
sleepWithSignal,
|
|
527
|
+
timeoutConfigField,
|
|
528
|
+
withRetry
|
|
367
529
|
};
|