@limitry/sdk 1.0.0-rc.8 → 1.0.0-rc.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  What changed in the Limitry TypeScript SDK (`@limitry/sdk`). Generated from the source on each release; newest first.
4
4
 
5
+ ## 1.0.0-rc.9 (2026-10-22)
6
+
7
+ ### Added
8
+
9
+ - `POST /v1/checks` can now answer `503` with the code `busy` when
10
+ Limitry is deciding too many checks for those subjects at once. Nothing
11
+ was charged. `guard` and `reserve` (TypeScript and Python) send the
12
+ check again with the same key, within the same 30 seconds they give
13
+ `moving`, before the limit's fail mode decides. Each retry of `busy` or
14
+ `moving` now waits the answer's `Retry-After` plus a random backoff that
15
+ grows with each try, so clients don't all come back at the same moment.
16
+ Older SDK versions treat `busy` as Limitry being unreachable after their
17
+ usual two quick retries, so the fail mode decides; upgrade to retry it.
18
+
19
+ ### Changed
20
+
21
+ - A very busy workspace's counts are now spread over more of Limitry's
22
+ servers as its traffic grows. While some of them move, which takes a few
23
+ seconds, checks, commits and releases on those subjects answer `503`
24
+ with the new error code `moving` and `Retry-After: 1`; nothing is
25
+ counted. `guard` and `reserve` (TypeScript and Python) and the CLI send
26
+ the same request again with the same Idempotency-Key for up to 30
27
+ seconds, each try with its own time budget, before a limit's fail mode
28
+ applies, so work is neither run uncounted nor left uncommitted.
29
+ - When `guard` gets no answer to its check in time and the limit fails
30
+ open (so your work runs), it now sends the same check once more in the
31
+ background, with the same Idempotency-Key and the header
32
+ `limitry-finish-only: 1`: Limitry finishes a check it was part way
33
+ through, and never decides one afresh, so a check that never arrived
34
+ charges nothing. A limit that fails closed sends nothing more.
35
+
36
+ `POST /v1/checks` answers `204` (no body) to such a resend when there was
37
+ nothing to finish, so `checks.create` is typed to allow no decision; a
38
+ check sent without `limitry-finish-only` always gets one.
39
+
40
+ ### Fixed
41
+
42
+ - A reservation (`mode: "reserve"`) now starts a subject's period, as a
43
+ plain check does: on a `billing_date` limit with no billing date set
44
+ yet, the first reservation's time becomes the subject's billing date,
45
+ and on a `first_use` limit it starts the period. Before, reservations
46
+ made before the subject's first commit could each count in a period of
47
+ their own, so a monthly limit could let more through than its amount.
48
+ A reservation you release still starts the period, with nothing used in
49
+ it.
50
+ - A `503 moving` answer with `Retry-After: 0` is now retried after at least
51
+ a second, not at once, and still for at most 30 seconds. A client made
52
+ with `maxRetries: 0` no longer sends the SDK's internal
53
+ `x-limitry-moving` header to the API.
54
+
5
55
  ## 1.0.0-rc.8 (2026-10-21)
6
56
 
7
57
  ### Added
package/dist/index.d.ts CHANGED
@@ -87,7 +87,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
87
87
  id: string;
88
88
  };
89
89
  retryAfterSeconds: number | null;
90
- }>;
90
+ } | undefined>;
91
91
  };
92
92
  limits: {
93
93
  archive: (limitId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<undefined>;
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import createOpenApiClient from "openapi-fetch";
3
3
  import { apiErrorFrom } from "./errors.js";
4
4
  import * as product from "./product/index.js";
5
5
  import { resources } from "./resources.gen.js";
6
- import { withRetries } from "./retry.js";
6
+ import { withoutMovingHeader, withRetries } from "./retry.js";
7
7
  import { verifyWebhook } from "./webhooks.js";
8
8
  export { ApiError, apiErrorFrom } from "./errors.js";
9
9
  export { verifyWebhook, WebhookVerificationError, } from "./webhooks.js";
@@ -35,7 +35,7 @@ function createBaseClient(options) {
35
35
  const client = createOpenApiClient({
36
36
  baseUrl: `${origin}/v1`,
37
37
  fetch: options.maxRetries === 0
38
- ? send
38
+ ? withoutMovingHeader(send)
39
39
  : withRetries(send, { maxRetries: options.maxRetries }),
40
40
  });
41
41
  client.use({
@@ -33,6 +33,17 @@ import type { BaseClient, Schemas } from "../index.ts";
33
33
  * `Unavailable` while `open` — the default, and the answer before any
34
34
  * decision was seen — lets the work run unmetered.
35
35
  *
36
+ * A subject being handed over between Limitry's servers (a split, a few
37
+ * seconds) answers `503 moving`: Limitry was reached and charged nothing,
38
+ * so it is not "unreachable" — the check (or commit, or release) is sent
39
+ * again with the same key for up to 30 s, each attempt with its own
40
+ * `timeoutMs`, before the fail mode applies. A fail-open product never
41
+ * runs uncounted through a handover. A limiter with too many checks
42
+ * inside at once answers `503 busy` the same way (charged nothing; retry
43
+ * with the same key): both share those 30 s, and each wait is the
44
+ * answer's Retry-After plus full jitter on a growing backoff, so clients
45
+ * turned away together don't all come back together.
46
+ *
36
47
  * Retries are safe (docs/product idempotency): each guard or reserve
37
48
  * sends one Idempotency-Key, which the client's own retries reuse, so a
38
49
  * check counts once however often it is sent. A reservation whose check
@@ -103,7 +114,13 @@ export declare class LimitReached extends Error {
103
114
  export declare class Unavailable extends Error {
104
115
  constructor();
105
116
  }
106
- export declare function createLimits(client: BaseClient, options?: LimitsOptions): {
117
+ export type LimitsInternals = {
118
+ /** Waits (tests pass a fake). */
119
+ sleep?: (ms: number) => Promise<void>;
120
+ /** 0 ≤ x < 1 for the jitter (tests pass a fixed one). */
121
+ random?: () => number;
122
+ };
123
+ export declare function createLimits(client: BaseClient, options?: LimitsOptions, internals?: LimitsInternals): {
107
124
  /**
108
125
  * Where a subject stands on every limit on it (and on `shared`
109
126
  * subjects): used, remaining, resetsAt — for showing an end user. Call
@@ -1,4 +1,5 @@
1
1
  import { apiErrorFrom } from "../errors.js";
2
+ import { MOVING_HEADER, MOVING_MS, retryWaitMs } from "../retry.js";
2
3
  /** The largest cost a commit takes. */
3
4
  const MAX_COST = 1_000_000_000;
4
5
  function checkedCost(units) {
@@ -40,8 +41,17 @@ const UNREACHABLE = Symbol("unreachable");
40
41
  const TIMED_OUT = Symbol("timed out");
41
42
  /** How long a settle (or a replay to find a hold) may take, retries included. */
42
43
  const SETTLE_TIMEOUT_MS = 5000;
43
- export function createLimits(client, options = {}) {
44
+ const wait = (ms) => new Promise((done) => setTimeout(done, ms));
45
+ /** Seconds of a `Retry-After`, as ms (none: undefined; `retryWaitMs` floors it at a second). */
46
+ function retryAfterMs(response) {
47
+ const value = response.headers.get("retry-after");
48
+ const seconds = value === null ? NaN : Number(value);
49
+ return Number.isFinite(seconds) ? seconds * 1000 : undefined;
50
+ }
51
+ export function createLimits(client, options = {}, internals = {}) {
44
52
  const timeoutMs = options.timeoutMs ?? 1500;
53
+ const sleep = internals.sleep ?? wait;
54
+ const random = internals.random ?? Math.random;
45
55
  /** The last fail mode seen per subject + action (closed if any limit is). */
46
56
  const remembered = new Map();
47
57
  const key = (s, action) => `${s.kind}\u0000${s.id}\u0000${action}`;
@@ -57,9 +67,24 @@ export function createLimits(client, options = {}) {
57
67
  };
58
68
  /**
59
69
  * The decision — or UNREACHABLE (network error, 5xx, past the plan, the
60
- * flood guard's 429), or TIMED_OUT (no answer within `wait`).
70
+ * flood guard's 429), or TIMED_OUT (no answer within `wait`). A
71
+ * `moving` or `busy` answer is asked again, with the same key, for up
72
+ * to 30 s in all: each attempt has its own `wait`.
61
73
  */
62
- async function check(body, idempotencyKey, wait = timeoutMs) {
74
+ async function check(body, idempotencyKey, wait = timeoutMs, finishOnly = false) {
75
+ let waited = 0;
76
+ for (let tries = 0;; tries++) {
77
+ const answer = await checkOnce(body, idempotencyKey, wait, finishOnly);
78
+ if (answer.kind !== "later")
79
+ return answer.value;
80
+ const pause = retryWaitMs(answer.retryAfterMs, tries, random);
81
+ if (waited + pause > MOVING_MS)
82
+ return UNREACHABLE;
83
+ waited += pause;
84
+ await sleep(pause);
85
+ }
86
+ }
87
+ async function checkOnce(body, idempotencyKey, wait, finishOnly) {
63
88
  let timer;
64
89
  // Raced as well as aborted: a custom fetch (a service binding, a
65
90
  // test) may ignore the signal.
@@ -70,11 +95,19 @@ export function createLimits(client, options = {}) {
70
95
  try {
71
96
  const { data, error, response } = await client.POST("/checks", {
72
97
  body,
73
- headers: { "idempotency-key": idempotencyKey },
98
+ headers: {
99
+ "idempotency-key": idempotencyKey,
100
+ // `moving` and `busy` come back here at once: this loop
101
+ // retries them.
102
+ [MOVING_HEADER]: "return",
103
+ ...(finishOnly ? { "limitry-finish-only": "1" } : {}),
104
+ },
74
105
  signal: AbortSignal.timeout(wait),
75
106
  });
76
107
  if (data)
77
108
  return data;
109
+ if (isRetryLater(response, error))
110
+ return { later: retryAfterMs(response) };
78
111
  // Past the plan's allowance (402 plan_limit_reached), or stopped by
79
112
  // the edge's flood guard (429): no decision — each limit's fail
80
113
  // mode applies, as if Limitry were unreachable.
@@ -97,7 +130,9 @@ export function createLimits(client, options = {}) {
97
130
  const answer = await Promise.race([asked, late]);
98
131
  if (answer instanceof Error)
99
132
  throw answer;
100
- return answer;
133
+ if (typeof answer === "object" && "later" in answer)
134
+ return { kind: "later", retryAfterMs: answer.later };
135
+ return { kind: "answer", value: answer };
101
136
  }
102
137
  finally {
103
138
  clearTimeout(timer);
@@ -109,23 +144,40 @@ export function createLimits(client, options = {}) {
109
144
  * same expires by itself.
110
145
  */
111
146
  async function settle(id, action, cost) {
112
- try {
113
- if (action === "commit") {
114
- await client.POST("/reservations/{reservationId}/commit", {
115
- params: { path: { reservationId: id } },
116
- body: { cost: cost ?? 0 },
117
- signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS),
118
- });
147
+ // A commit lost would leave work uncounted: `moving` or `busy` is
148
+ // asked again, with the same key, for up to 30 s, each attempt with
149
+ // its own budget.
150
+ const idempotencyKey = crypto.randomUUID();
151
+ let waited = 0;
152
+ for (let tries = 0;; tries++) {
153
+ let retryMs = null;
154
+ try {
155
+ const headers = {
156
+ "idempotency-key": idempotencyKey,
157
+ [MOVING_HEADER]: "return",
158
+ };
159
+ const { error, response } = action === "commit"
160
+ ? await client.POST("/reservations/{reservationId}/commit", {
161
+ params: { path: { reservationId: id } },
162
+ body: { cost: cost ?? 0 },
163
+ headers,
164
+ signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS),
165
+ })
166
+ : await client.POST("/reservations/{reservationId}/release", {
167
+ params: { path: { reservationId: id } },
168
+ headers,
169
+ signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS),
170
+ });
171
+ if (isRetryLater(response, error))
172
+ retryMs = retryWaitMs(retryAfterMs(response), tries, random);
119
173
  }
120
- else {
121
- await client.POST("/reservations/{reservationId}/release", {
122
- params: { path: { reservationId: id } },
123
- signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS),
124
- });
174
+ catch {
175
+ // The reservation expires on its own.
125
176
  }
126
- }
127
- catch {
128
- // The reservation expires on its own.
177
+ if (retryMs === null || waited + retryMs > MOVING_MS)
178
+ return;
179
+ waited += retryMs;
180
+ await sleep(retryMs);
129
181
  }
130
182
  }
131
183
  /**
@@ -167,16 +219,26 @@ export function createLimits(client, options = {}) {
167
219
  },
168
220
  /** Checks `cost`, runs `fn` only if allowed, and returns its result. */
169
221
  async guard(request, fn) {
170
- const decision = await check({
222
+ const body = {
171
223
  subject: request.subject,
172
224
  shared: request.shared,
173
225
  action: request.action,
174
226
  cost: request.cost ?? 1,
175
- }, crypto.randomUUID());
227
+ };
228
+ const idempotencyKey = crypto.randomUUID();
229
+ const decision = await check(body, idempotencyKey);
176
230
  if (decision === UNREACHABLE || decision === TIMED_OUT) {
177
231
  if (failMode(request.subject, request.action) === "closed") {
178
232
  throw new Unavailable();
179
233
  }
234
+ if (decision === TIMED_OUT) {
235
+ // No answer in time, and the work runs (fail open): the check may
236
+ // be part way (held on some of Limitry's servers, across
237
+ // subjects). Sent once more with the same key, in the background,
238
+ // ONLY TO FINISH it — Limitry never decides it afresh then, so a
239
+ // check that never arrived charges nothing.
240
+ void check(body, idempotencyKey, SETTLE_TIMEOUT_MS, true).catch(() => { });
241
+ }
180
242
  return fn();
181
243
  }
182
244
  remember(request.subject, request.action, decision);
@@ -257,3 +319,9 @@ export function createLimits(client, options = {}) {
257
319
  },
258
320
  };
259
321
  }
322
+ /** `503 moving` or `503 busy`: nothing was charged — retry with the same key. */
323
+ function isRetryLater(response, error) {
324
+ const code = error?.error
325
+ ?.code;
326
+ return response.status === 503 && (code === "moving" || code === "busy");
327
+ }
@@ -16,7 +16,7 @@ export declare function resources(client: RawClient): {
16
16
  id: string;
17
17
  };
18
18
  retryAfterSeconds: number | null;
19
- }>;
19
+ } | undefined>;
20
20
  };
21
21
  limits: {
22
22
  archive: (limitId: string | number, options?: RequestOptions) => Promise<undefined>;
package/dist/retry.d.ts CHANGED
@@ -13,9 +13,29 @@
13
13
  * handed back to the caller rather than slept through. A request whose
14
14
  * own signal aborted (a caller's timeout) is never retried: the caller
15
15
  * decided.
16
+ *
17
+ * A `503 moving` (a subject handed over between Limitry's servers, a few
18
+ * seconds) or `503 busy` (too many checks inside one of them at once) is
19
+ * retried for up to MOVING_MS whatever `maxRetries` says: nothing was
20
+ * charged, and the same key gets the answer once it lands. Each wait is
21
+ * the Retry-After (at least a second) plus full jitter on a capped
22
+ * exponential backoff (`retryWaitMs`), so clients turned away together
23
+ * don't come back together. A caller that keeps its own time budget (the
24
+ * guard and reserve helpers) sends `MOVING_HEADER: return` and gets
25
+ * either answer back at once.
16
26
  */
17
27
  export declare const DEFAULT_MAX_RETRIES = 2;
18
28
  export declare const MAX_WAIT_MS = 20000;
29
+ /**
30
+ * How long a `moving` or `busy` answer is retried, all told: a handover's
31
+ * freeze lasts at most 30 s.
32
+ */
33
+ export declare const MOVING_MS = 30000;
34
+ /** Sent by a caller that retries `moving` and `busy` itself (never reaches the API). */
35
+ export declare const MOVING_HEADER = "x-limitry-moving";
36
+ /** The backoff added to a `moving` or `busy` wait: full jitter under base × 2^n, capped. */
37
+ export declare const JITTER_BASE_MS = 250;
38
+ export declare const JITTER_CAP_MS = 4000;
19
39
  /** The longest key a caller may pass where a client derives keys from it. */
20
40
  export declare const MAX_BASE_KEY = 200;
21
41
  type Fetch = (request: Request) => Promise<Response>;
@@ -26,7 +46,25 @@ export type RetryOptions = {
26
46
  key?: (method: string, path: string) => string | Promise<string>;
27
47
  /** Waits (tests pass a fake). */
28
48
  sleep?: (ms: number) => Promise<void>;
49
+ /** 0 ≤ x < 1 for the jitter (tests pass a fixed one). */
50
+ random?: () => number;
29
51
  };
52
+ /**
53
+ * The wait before retrying a `moving` or `busy` answer, the `attempt`th
54
+ * in a row (0 first): the server's Retry-After, at least a second (a 0,
55
+ * or a date past, must not spin), plus full jitter — anywhere from 0 to
56
+ * min(JITTER_CAP_MS, JITTER_BASE_MS × 2^attempt).
57
+ */
58
+ export declare function retryWaitMs(retryAfterMs: number | undefined, attempt: number, random?: () => number): number;
59
+ /** A `503` that says retry later with the same key: `moving` or `busy` (else null). */
60
+ export declare function retryLater(response: Response): Promise<"moving" | "busy" | null>;
61
+ /** A `503` whose error code is `moving`. */
62
+ export declare function isMoving(response: Response): Promise<boolean>;
63
+ /**
64
+ * `fetch` that only drops MOVING_HEADER (a client with retries off): the
65
+ * header is between the SDK's own parts, never sent to the API.
66
+ */
67
+ export declare function withoutMovingHeader(fetch: Fetch): Fetch;
30
68
  /** `fetch` with the rule above. Each request is one logical call. */
31
69
  export declare function withRetries(fetch: Fetch, options?: RetryOptions): Fetch;
32
70
  /**
package/dist/retry.js CHANGED
@@ -13,11 +13,31 @@
13
13
  * handed back to the caller rather than slept through. A request whose
14
14
  * own signal aborted (a caller's timeout) is never retried: the caller
15
15
  * decided.
16
+ *
17
+ * A `503 moving` (a subject handed over between Limitry's servers, a few
18
+ * seconds) or `503 busy` (too many checks inside one of them at once) is
19
+ * retried for up to MOVING_MS whatever `maxRetries` says: nothing was
20
+ * charged, and the same key gets the answer once it lands. Each wait is
21
+ * the Retry-After (at least a second) plus full jitter on a capped
22
+ * exponential backoff (`retryWaitMs`), so clients turned away together
23
+ * don't come back together. A caller that keeps its own time budget (the
24
+ * guard and reserve helpers) sends `MOVING_HEADER: return` and gets
25
+ * either answer back at once.
16
26
  */
17
27
  const WRITES = new Set(["POST", "PUT", "PATCH", "DELETE"]);
18
28
  const RETRY_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
19
29
  export const DEFAULT_MAX_RETRIES = 2;
20
30
  export const MAX_WAIT_MS = 20_000;
31
+ /**
32
+ * How long a `moving` or `busy` answer is retried, all told: a handover's
33
+ * freeze lasts at most 30 s.
34
+ */
35
+ export const MOVING_MS = 30_000;
36
+ /** Sent by a caller that retries `moving` and `busy` itself (never reaches the API). */
37
+ export const MOVING_HEADER = "x-limitry-moving";
38
+ /** The backoff added to a `moving` or `busy` wait: full jitter under base × 2^n, capped. */
39
+ export const JITTER_BASE_MS = 250;
40
+ export const JITTER_CAP_MS = 4_000;
21
41
  /** The longest key a caller may pass where a client derives keys from it. */
22
42
  export const MAX_BASE_KEY = 200;
23
43
  const wait = (ms) => new Promise((done) => setTimeout(done, ms));
@@ -35,6 +55,34 @@ function retryAfter(response) {
35
55
  const date = Date.parse(value);
36
56
  return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
37
57
  }
58
+ /**
59
+ * The wait before retrying a `moving` or `busy` answer, the `attempt`th
60
+ * in a row (0 first): the server's Retry-After, at least a second (a 0,
61
+ * or a date past, must not spin), plus full jitter — anywhere from 0 to
62
+ * min(JITTER_CAP_MS, JITTER_BASE_MS × 2^attempt).
63
+ */
64
+ export function retryWaitMs(retryAfterMs, attempt, random = Math.random) {
65
+ const floor = Math.max(1_000, retryAfterMs ?? 1_000);
66
+ const ceiling = Math.min(JITTER_CAP_MS, JITTER_BASE_MS * 2 ** attempt);
67
+ return floor + random() * ceiling;
68
+ }
69
+ /** A `503` that says retry later with the same key: `moving` or `busy` (else null). */
70
+ export async function retryLater(response) {
71
+ if (response.status !== 503)
72
+ return null;
73
+ try {
74
+ const body = (await response.clone().json());
75
+ const code = body.error?.code;
76
+ return code === "moving" || code === "busy" ? code : null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /** A `503` whose error code is `moving`. */
83
+ export async function isMoving(response) {
84
+ return (await retryLater(response)) === "moving";
85
+ }
38
86
  async function retryable(response) {
39
87
  if (RETRY_STATUSES.has(response.status))
40
88
  return true;
@@ -48,11 +96,31 @@ async function retryable(response) {
48
96
  return false;
49
97
  }
50
98
  }
99
+ /**
100
+ * `fetch` that only drops MOVING_HEADER (a client with retries off): the
101
+ * header is between the SDK's own parts, never sent to the API.
102
+ */
103
+ export function withoutMovingHeader(fetch) {
104
+ return (request) => {
105
+ if (!request.headers.has(MOVING_HEADER))
106
+ return fetch(request);
107
+ const headers = new Headers(request.headers);
108
+ headers.delete(MOVING_HEADER);
109
+ return fetch(new Request(request, { headers }));
110
+ };
111
+ }
51
112
  /** `fetch` with the rule above. Each request is one logical call. */
52
113
  export function withRetries(fetch, options = {}) {
53
114
  const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
54
115
  const sleep = options.sleep ?? wait;
116
+ const random = options.random ?? Math.random;
55
117
  return async (request) => {
118
+ const returnMoving = request.headers.get(MOVING_HEADER) === "return";
119
+ if (request.headers.has(MOVING_HEADER)) {
120
+ const headers = new Headers(request.headers);
121
+ headers.delete(MOVING_HEADER);
122
+ request = new Request(request, { headers });
123
+ }
56
124
  if (WRITES.has(request.method) && !request.headers.has("idempotency-key")) {
57
125
  const url = new URL(request.url);
58
126
  const key = options.key
@@ -62,10 +130,12 @@ export function withRetries(fetch, options = {}) {
62
130
  headers.set("idempotency-key", key);
63
131
  request = new Request(request, { headers });
64
132
  }
133
+ let movingWaited = 0;
134
+ let movingTries = 0;
65
135
  for (let attempt = 0;; attempt++) {
66
136
  const last = attempt >= maxRetries;
67
137
  // Send a copy while a retry may follow: a body is read once.
68
- const sending = last ? request : request.clone();
138
+ const sending = request.clone();
69
139
  let response;
70
140
  try {
71
141
  response = await fetch(sending);
@@ -76,6 +146,20 @@ export function withRetries(fetch, options = {}) {
76
146
  await sleep(backoff(attempt));
77
147
  continue;
78
148
  }
149
+ const later = await retryLater(response);
150
+ if (later && returnMoving)
151
+ return response;
152
+ if (later) {
153
+ // Retried apart from maxRetries, for up to MOVING_MS.
154
+ const delay = retryWaitMs(retryAfter(response), movingTries++, random);
155
+ if (movingWaited + delay > MOVING_MS)
156
+ return response;
157
+ movingWaited += delay;
158
+ await response.body?.cancel();
159
+ await sleep(delay);
160
+ attempt--;
161
+ continue;
162
+ }
79
163
  if (last || !(await retryable(response)))
80
164
  return response;
81
165
  const delay = retryAfter(response) ?? backoff(attempt);
package/dist/v1.gen.d.ts CHANGED
@@ -1470,6 +1470,13 @@ export interface operations {
1470
1470
  "application/json": components["schemas"]["CheckDecision"];
1471
1471
  };
1472
1472
  };
1473
+ /** @description Sent with `limitry-finish-only: 1` (a client finishing an earlier try with the same Idempotency-Key, after a timeout), and there was no earlier try to finish: nothing was decided or charged. */
1474
+ 204: {
1475
+ headers: {
1476
+ [name: string]: unknown;
1477
+ };
1478
+ content?: never;
1479
+ };
1473
1480
  /** @description Invalid request (code: invalid_request). */
1474
1481
  400: {
1475
1482
  headers: {
@@ -1524,7 +1531,7 @@ export interface operations {
1524
1531
  "application/json": components["schemas"]["ErrorEnvelope"];
1525
1532
  };
1526
1533
  };
1527
- /** @description Checks are unavailable (code: internal_error). */
1534
+ /** @description The subject is moving between Limitry's servers, a few seconds (code: moving, with Retry-After): retry with the same Idempotency-Key — nothing was charged (anything held is given back, or finished by the retry). Or too many checks for these subjects are being decided at once (code: busy, with Retry-After): nothing was charged — retry with the same Idempotency-Key after Retry-After, with backoff. Or checks are unavailable, or a check across Limitry's servers couldn't be confirmed in time and may have been charged (code: internal_error): retry with the same Idempotency-Key. */
1528
1535
  503: {
1529
1536
  headers: {
1530
1537
  [name: string]: unknown;
@@ -2251,7 +2258,7 @@ export interface operations {
2251
2258
  "application/json": components["schemas"]["ErrorEnvelope"];
2252
2259
  };
2253
2260
  };
2254
- /** @description Checks are unavailable (code: internal_error). */
2261
+ /** @description The reservation's subject is moving between Limitry's servers, a few seconds (code: moving, with Retry-After): retry — the same settle answers the same. Or checks are unavailable (code: internal_error). */
2255
2262
  503: {
2256
2263
  headers: {
2257
2264
  [name: string]: unknown;
@@ -2336,7 +2343,7 @@ export interface operations {
2336
2343
  "application/json": components["schemas"]["ErrorEnvelope"];
2337
2344
  };
2338
2345
  };
2339
- /** @description Checks are unavailable (code: internal_error). */
2346
+ /** @description The reservation's subject is moving between Limitry's servers, a few seconds (code: moving, with Retry-After): retry — the same settle answers the same. Or checks are unavailable (code: internal_error). */
2340
2347
  503: {
2341
2348
  headers: {
2342
2349
  [name: string]: unknown;
@@ -2417,7 +2424,7 @@ export interface operations {
2417
2424
  "application/json": components["schemas"]["ErrorEnvelope"];
2418
2425
  };
2419
2426
  };
2420
- /** @description Checks are unavailable (code: internal_error). */
2427
+ /** @description The reservation's subject is moving between Limitry's servers, a few seconds (code: moving, with Retry-After): retry — the same settle answers the same. Or checks are unavailable (code: internal_error). */
2421
2428
  503: {
2422
2429
  headers: {
2423
2430
  [name: string]: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limitry/sdk",
3
- "version": "1.0.0-rc.8",
3
+ "version": "1.0.0-rc.9",
4
4
  "description": "Limitry API client",
5
5
  "type": "module",
6
6
  "exports": {