@to11ai/sdk 0.1.0 → 0.1.2
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/transport.d.ts +6 -1
- package/package.json +1 -1
- package/src/transport.ts +40 -5
package/dist/transport.d.ts
CHANGED
|
@@ -10,7 +10,12 @@ export interface TransportConfig {
|
|
|
10
10
|
readonly auth: AuthConfig;
|
|
11
11
|
/** Request timeout in milliseconds. Default: 30_000 */
|
|
12
12
|
readonly timeoutMs?: number;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Max number of retries for network errors / 5xx on idempotent methods
|
|
15
|
+
* (GET/HEAD/OPTIONS/PUT/DELETE/TRACE per RFC 9110 §9.2.2). POST/PATCH are
|
|
16
|
+
* never auto-retried regardless of this value. Not counting the initial
|
|
17
|
+
* attempt. Default: 3
|
|
18
|
+
*/
|
|
14
19
|
readonly maxRetries?: number;
|
|
15
20
|
}
|
|
16
21
|
/** Standard API envelope returned by the to11ai server. */
|
package/package.json
CHANGED
package/src/transport.ts
CHANGED
|
@@ -37,7 +37,12 @@ export interface TransportConfig {
|
|
|
37
37
|
readonly auth: AuthConfig;
|
|
38
38
|
/** Request timeout in milliseconds. Default: 30_000 */
|
|
39
39
|
readonly timeoutMs?: number;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Max number of retries for network errors / 5xx on idempotent methods
|
|
42
|
+
* (GET/HEAD/OPTIONS/PUT/DELETE/TRACE per RFC 9110 §9.2.2). POST/PATCH are
|
|
43
|
+
* never auto-retried regardless of this value. Not counting the initial
|
|
44
|
+
* attempt. Default: 3
|
|
45
|
+
*/
|
|
41
46
|
readonly maxRetries?: number;
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -58,6 +63,22 @@ export interface ApiEnvelope<T = unknown> {
|
|
|
58
63
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
59
64
|
const DEFAULT_MAX_RETRIES = 3;
|
|
60
65
|
|
|
66
|
+
/**
|
|
67
|
+
* HTTP methods classified as idempotent by RFC 9110 §9.2.2. The SDK only
|
|
68
|
+
* auto-retries 5xx, network failures, and timeouts on these methods because
|
|
69
|
+
* a write that timed out or returned 5xx may have already committed on the
|
|
70
|
+
* server — replaying a non-idempotent method (POST/PATCH) can create
|
|
71
|
+
* duplicates or double-apply state.
|
|
72
|
+
*/
|
|
73
|
+
const IDEMPOTENT_METHODS = new Set([
|
|
74
|
+
"GET",
|
|
75
|
+
"HEAD",
|
|
76
|
+
"OPTIONS",
|
|
77
|
+
"PUT",
|
|
78
|
+
"DELETE",
|
|
79
|
+
"TRACE",
|
|
80
|
+
]);
|
|
81
|
+
|
|
61
82
|
/** Exponential backoff with jitter: 100ms * 2^attempt + random(0,100ms) */
|
|
62
83
|
const backoffMs = (attempt: number): number =>
|
|
63
84
|
100 * 2 ** attempt + Math.random() * 100;
|
|
@@ -271,6 +292,7 @@ export class Transport {
|
|
|
271
292
|
},
|
|
272
293
|
): Promise<ApiEnvelope<T>> {
|
|
273
294
|
const url = new URL(this.baseUrl + path);
|
|
295
|
+
const isIdempotent = IDEMPOTENT_METHODS.has(method.toUpperCase());
|
|
274
296
|
|
|
275
297
|
if (options?.query) {
|
|
276
298
|
for (const [key, value] of Object.entries(options.query)) {
|
|
@@ -300,8 +322,14 @@ export class Transport {
|
|
|
300
322
|
|
|
301
323
|
clearTimeout(timeoutId);
|
|
302
324
|
|
|
303
|
-
// 5xx — retry if
|
|
304
|
-
|
|
325
|
+
// 5xx — retry only if the method is idempotent (RFC 9110 §9.2.2).
|
|
326
|
+
// POST/PATCH may have already committed server-side, so replaying
|
|
327
|
+
// them can create duplicates.
|
|
328
|
+
if (
|
|
329
|
+
response.status >= 500 &&
|
|
330
|
+
isIdempotent &&
|
|
331
|
+
attempt < this.maxRetries
|
|
332
|
+
) {
|
|
305
333
|
// Drain the body to free the underlying connection before sleeping
|
|
306
334
|
await response.body?.cancel().catch(() => undefined);
|
|
307
335
|
attempt++;
|
|
@@ -342,14 +370,21 @@ export class Transport {
|
|
|
342
370
|
throw err;
|
|
343
371
|
}
|
|
344
372
|
|
|
345
|
-
// Timeout / network errors — retry if
|
|
373
|
+
// Timeout / network errors — retry only if the method is idempotent
|
|
374
|
+
// (RFC 9110 §9.2.2). A POST/PATCH whose request body was fully sent
|
|
375
|
+
// before a connection drop or timeout may have committed server-side,
|
|
376
|
+
// so a replay can duplicate the write.
|
|
346
377
|
const isTimeout =
|
|
347
378
|
err instanceof DOMException && err.name === "AbortError";
|
|
348
379
|
// Only retry on genuine network failures (fetch throws TypeError) or timeouts.
|
|
349
380
|
// Unexpected errors (e.g. getAccessToken rejections) propagate immediately.
|
|
350
381
|
const isNetworkError = err instanceof TypeError;
|
|
351
382
|
|
|
352
|
-
if (
|
|
383
|
+
if (
|
|
384
|
+
(isTimeout || isNetworkError) &&
|
|
385
|
+
isIdempotent &&
|
|
386
|
+
attempt < this.maxRetries
|
|
387
|
+
) {
|
|
353
388
|
attempt++;
|
|
354
389
|
await sleep(backoffMs(attempt));
|
|
355
390
|
continue;
|