@limitry/sdk 0.0.0-stage → 1.0.0-rc.1
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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +25 -2
- package/dist/brand.d.ts +6 -0
- package/dist/brand.js +7 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +18 -0
- package/dist/index.d.ts +483 -0
- package/dist/index.js +73 -0
- package/dist/product/index.d.ts +24 -0
- package/dist/product/index.js +7 -0
- package/dist/product/limits.d.ts +110 -0
- package/dist/product/limits.js +234 -0
- package/dist/resources.d.ts +71 -0
- package/dist/resources.gen.d.ts +400 -0
- package/dist/resources.gen.js +83 -0
- package/dist/resources.js +40 -0
- package/dist/retry.d.ts +42 -0
- package/dist/retry.js +116 -0
- package/dist/v1.gen.d.ts +4207 -0
- package/dist/v1.gen.js +1 -0
- package/dist/webhooks.d.ts +39 -0
- package/dist/webhooks.js +88 -0
- package/package.json +36 -4
package/dist/retry.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe retries, one rule for every client (the SDK, the CLI, the MCP
|
|
3
|
+
* server; docs/architecture.md "Idempotency"): a WRITE gets one
|
|
4
|
+
* Idempotency-Key per logical call, and every automatic retry of that
|
|
5
|
+
* call sends the same key, so the API runs it once however many times
|
|
6
|
+
* it is sent. Without a key a timed-out create that actually ran would
|
|
7
|
+
* run again on retry; with a fresh key per attempt it would too.
|
|
8
|
+
*
|
|
9
|
+
* Retried: a network failure, 408, 429, 500, 502, 503, 504, and 409
|
|
10
|
+
* `idempotency_in_progress` (the first attempt is still running). Waits
|
|
11
|
+
* the response's Retry-After when it gives one, else a jittered backoff
|
|
12
|
+
* (0.5s, 1s, … capped at 8s); a Retry-After longer than MAX_WAIT_MS is
|
|
13
|
+
* handed back to the caller rather than slept through. A request whose
|
|
14
|
+
* own signal aborted (a caller's timeout) is never retried: the caller
|
|
15
|
+
* decided.
|
|
16
|
+
*/
|
|
17
|
+
const WRITES = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
18
|
+
const RETRY_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
19
|
+
export const DEFAULT_MAX_RETRIES = 2;
|
|
20
|
+
export const MAX_WAIT_MS = 20_000;
|
|
21
|
+
/** The longest key a caller may pass where a client derives keys from it. */
|
|
22
|
+
export const MAX_BASE_KEY = 200;
|
|
23
|
+
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
24
|
+
function backoff(attempt) {
|
|
25
|
+
const base = Math.min(500 * 2 ** attempt, 8_000);
|
|
26
|
+
return base * (0.75 + Math.random() * 0.25);
|
|
27
|
+
}
|
|
28
|
+
function retryAfter(response) {
|
|
29
|
+
const value = response.headers.get("retry-after");
|
|
30
|
+
if (!value)
|
|
31
|
+
return undefined;
|
|
32
|
+
const seconds = Number(value);
|
|
33
|
+
if (Number.isFinite(seconds))
|
|
34
|
+
return Math.max(0, seconds * 1000);
|
|
35
|
+
const date = Date.parse(value);
|
|
36
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
37
|
+
}
|
|
38
|
+
async function retryable(response) {
|
|
39
|
+
if (RETRY_STATUSES.has(response.status))
|
|
40
|
+
return true;
|
|
41
|
+
if (response.status !== 409)
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
const body = (await response.clone().json());
|
|
45
|
+
return body.error?.code === "idempotency_in_progress";
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** `fetch` with the rule above. Each request is one logical call. */
|
|
52
|
+
export function withRetries(fetch, options = {}) {
|
|
53
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
54
|
+
const sleep = options.sleep ?? wait;
|
|
55
|
+
return async (request) => {
|
|
56
|
+
if (WRITES.has(request.method) && !request.headers.has("idempotency-key")) {
|
|
57
|
+
const url = new URL(request.url);
|
|
58
|
+
const key = options.key
|
|
59
|
+
? await options.key(request.method, url.pathname)
|
|
60
|
+
: crypto.randomUUID();
|
|
61
|
+
const headers = new Headers(request.headers);
|
|
62
|
+
headers.set("idempotency-key", key);
|
|
63
|
+
request = new Request(request, { headers });
|
|
64
|
+
}
|
|
65
|
+
for (let attempt = 0;; attempt++) {
|
|
66
|
+
const last = attempt >= maxRetries;
|
|
67
|
+
// Send a copy while a retry may follow: a body is read once.
|
|
68
|
+
const sending = last ? request : request.clone();
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await fetch(sending);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (last || request.signal.aborted)
|
|
75
|
+
throw error;
|
|
76
|
+
await sleep(backoff(attempt));
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (last || !(await retryable(response)))
|
|
80
|
+
return response;
|
|
81
|
+
const delay = retryAfter(response) ?? backoff(attempt);
|
|
82
|
+
if (delay > MAX_WAIT_MS)
|
|
83
|
+
return response;
|
|
84
|
+
await response.body?.cancel();
|
|
85
|
+
await sleep(delay);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
90
|
+
/**
|
|
91
|
+
* Keys for a command or tool call that may make several writes, from
|
|
92
|
+
* one key its caller chose (`--idempotency-key`, an MCP tool's
|
|
93
|
+
* `idempotencyKey`): each write gets `<key>:<16 hex>`, a digest of its
|
|
94
|
+
* method, path and how many times the call wrote there before — so the
|
|
95
|
+
* same call run again sends the same keys, write for write, and a write
|
|
96
|
+
* it skips the second time shifts no other write's key. Without a key,
|
|
97
|
+
* a fresh UUID per write.
|
|
98
|
+
*/
|
|
99
|
+
export function keysFrom(base) {
|
|
100
|
+
if (base !== undefined && (base.length === 0 || base.length > MAX_BASE_KEY)) {
|
|
101
|
+
throw new Error(`the idempotency key must be 1–${MAX_BASE_KEY} characters`);
|
|
102
|
+
}
|
|
103
|
+
if (base !== undefined && !/^[\x21-\x7e]+$/.test(base)) {
|
|
104
|
+
throw new Error("the idempotency key must be printable ASCII with no spaces");
|
|
105
|
+
}
|
|
106
|
+
const seen = new Map();
|
|
107
|
+
return async (method, path) => {
|
|
108
|
+
if (base === undefined)
|
|
109
|
+
return crypto.randomUUID();
|
|
110
|
+
const target = `${method} ${path}`;
|
|
111
|
+
const n = (seen.get(target) ?? 0) + 1;
|
|
112
|
+
seen.set(target, n);
|
|
113
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${target}#${n}`));
|
|
114
|
+
return `${base}:${hex(digest).slice(0, 16)}`;
|
|
115
|
+
};
|
|
116
|
+
}
|