@limitry/sdk 1.0.0-rc.7 → 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,128 @@
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
+
55
+ ## 1.0.0-rc.8 (2026-10-21)
56
+
57
+ ### Added
58
+
59
+ - Limits have `counting`: `fast` answers the instant it decides, and
60
+ `guaranteed` stores every count safely before answering, so the limit
61
+ never goes over, even if a server fails. A `total` balance defaults to
62
+ `guaranteed`, every other window to `fast`. Set it when creating or
63
+ updating a limit (`counting`, `--counting`), or in `limitry.json`.
64
+
65
+ ### Changed
66
+
67
+ - - Creating a limit needs only who it is for, the amount and the window:
68
+ `limits.create({ subject: { kind: "user" }, amount: 100, window: "month" })`
69
+ (`limitry limits create --subject '{"kind":"user"}' --amount 100 --window month`).
70
+ `action` defaults to every action (`*`), `unit` to `units`, and `name`
71
+ to the sentence the limit reads as, such as "100 units per user per
72
+ month". If a limit already has that name, the API answers `409`: give
73
+ this one a `name`. `limitry.json` entries still need a `name`.
74
+ - A limit's input names who it is for as `subject: { kind, id }` (was
75
+ `subjectKind` and `subjectId`), the same shape the limit itself has.
76
+ - What a limit counts is fixed once it is created: its subject, action,
77
+ unit, window, `every`, `start` and `rolling`. Changing one answers
78
+ `400 invalid_request`: create a new limit and archive the old one (its
79
+ history stays). Name, amount, fail mode and enabled still change any
80
+ time. `limitry sync` refuses such a change until you rename the entry.
81
+ - A limit says when a check last reached it (`lastCheckedAt`), so you can
82
+ see a limit no check names.
83
+ - One name for each idea, everywhere: the API, both SDKs, the CLI, the MCP
84
+ tools, `limitry.json` and webhooks. Update your code when you upgrade:
85
+
86
+ - `hold()` is `reserve()`: it takes `cost` (was `estimate`), and the block
87
+ gets a `Reservation` (was `Held`; Python `Reservation` and
88
+ `AsyncReservation`), with the same `cost()` and `extend()`.
89
+ - A refusal throws `LimitReached` (was `LimitExceeded`), with `limitId`
90
+ (Python `limit_id`) and `reason` `limit_reached` or
91
+ `too_many_reservations`. When Limitry can't be reached and a limit fails
92
+ closed, you now get a separate `Unavailable` error. "Can't be reached"
93
+ also covers a `402 plan_limit_reached` and a `429` from Limitry's flood
94
+ guard, so each limit's fail mode decides instead of your code getting an
95
+ error.
96
+ - A check's answer lists its limits in `limits` (was `remaining`): read
97
+ `decision.limits[0].remaining`. Every answer that names a limit says
98
+ `limitId`: `reason.limitId`, `limits[].limitId`, `usage.standing`'s
99
+ `limits[].limitId`, and webhook payloads.
100
+ - The refusal code is `limit_reached` (was `limit_exceeded`), and the
101
+ webhook event is `limit.reached` (was `limit.exceeded`): subscribe your
102
+ endpoints to the new name.
103
+ - Usage says `refused` (was `denied`); `usage.subjects` no longer has
104
+ `over` (it is `refused > 0`).
105
+ - Credits: `limits.credits.create()` and `limits.credits.list()` at
106
+ `/v1/limits/{limitId}/credits` (were `limits.grants.*` at `…/grants`),
107
+ ids start `crd_` (were `grt_`), and the approval an agent asks for is
108
+ `limit.credit` (was `limit.grant`).
109
+ - A period that starts on the subject's billing date is
110
+ `start: "billing_date"` (was `"anchor"`), and `subjects.update()` takes
111
+ `billingDate` (was `anchor`); the subject says `billingDate` too.
112
+ - `usage.standing()` and `usage.query()` take one `subject`, written
113
+ `kind:id` (`user:usr_123`), instead of separate kind and id fields
114
+ (`limitry usage standing --subject user:usr_123`).
115
+ - A limit's id starts `lmt_` (was `lim_`); keys still start `lim_test_`
116
+ and `lim_live_`.
117
+ - `limitry sync --preview` (was `--dry-run`), and the API's `preview`
118
+ (was `dryRun`), in what you send and what comes back.
119
+
120
+ ### Removed
121
+
122
+ - Limits no longer have `counting` (Fast or Guaranteed), and the CLI no
123
+ longer has `--counting`: every count is now saved before Limitry answers,
124
+ for every limit. Remove `counting` from your code and from
125
+ `limitry.json`; Limitry ignores it.
126
+
5
127
  ## 1.0.0-rc.7 (2026-10-19)
6
128
 
7
129
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @limitry/sdk
2
2
 
3
- The Limitry TypeScript SDK puts a limit around costly work in one call: `guard` checks before your code runs, and `hold` reserves an estimate, then charges what the work actually cost. Every API operation is a typed method too.
3
+ The Limitry TypeScript SDK puts a limit around costly work in one call: `guard` checks before your code runs, and `reserve` reserves what the work will probably cost, then charges what it actually cost. Every API operation is a typed method too.
4
4
 
5
5
  Limitry is the limits, credits and budgets layer for your software: give each of your users, teams, tenants, API keys or agents an allowance, and before any costly action one call tells your code whether it may happen.
6
6
 
@@ -19,7 +19,7 @@ key (`lim_test_…`) while you build.
19
19
  it counts one AI call against a limit such as 100 a day:
20
20
 
21
21
  ```ts
22
- import { createClient, LimitExceeded } from "@limitry/sdk";
22
+ import { createClient, LimitReached } from "@limitry/sdk";
23
23
 
24
24
  const limitry = createClient(); // reads LIMITRY_API_KEY
25
25
 
@@ -29,7 +29,7 @@ try {
29
29
  () => callModel(prompt),
30
30
  );
31
31
  } catch (error) {
32
- if (error instanceof LimitExceeded) {
32
+ if (error instanceof LimitReached) {
33
33
  console.log(`Try again in ${error.retryAfterSeconds} seconds`);
34
34
  }
35
35
  }
@@ -38,26 +38,28 @@ try {
38
38
  ## Charge what it actually cost
39
39
 
40
40
  When you only know the cost afterwards, such as an AI reply priced by
41
- the token, `hold` reserves an estimate (5 cents here), runs your code
42
- and charges what it used. If your code throws, the hold is released:
41
+ the token, `reserve` reserves what it will probably cost (5 cents here),
42
+ runs your code and charges what it used. If your code throws, the
43
+ reservation is released:
43
44
 
44
45
  ```ts
45
46
  import { createClient } from "@limitry/sdk";
46
47
 
47
48
  const limitry = createClient();
48
49
 
49
- const answer = await limitry.hold(
50
- { subject: { kind: "user", id: "usr_123" }, action: "ai-call", estimate: 5 },
51
- async (held) => {
50
+ const answer = await limitry.reserve(
51
+ { subject: { kind: "user", id: "usr_123" }, action: "ai-call", cost: 5 },
52
+ async (reservation) => {
52
53
  const response = await callModel(prompt);
53
- held.cost(priceCents(response.usage)); // your prices, in cents
54
+ reservation.cost(priceCents(response.usage)); // your prices, in cents
54
55
  return response;
55
56
  },
56
57
  );
57
58
  ```
58
59
 
59
- If Limitry can't be reached, each limit's fail mode decides: `open` lets
60
- the work run, `closed` refuses it.
60
+ A refusal throws `LimitReached`. If Limitry can't be reached, each
61
+ limit's fail mode decides: `open` lets the work run, `closed` throws
62
+ `Unavailable`.
61
63
 
62
64
  ## Everything else
63
65
 
@@ -69,9 +71,9 @@ import { createClient } from "@limitry/sdk";
69
71
 
70
72
  const limitry = createClient();
71
73
 
74
+ // "100 units per user per day for ai-call"
72
75
  await limitry.limits.create({
73
- name: "AI replies per day",
74
- subjectKind: "user",
76
+ subject: { kind: "user" },
75
77
  action: "ai-call",
76
78
  amount: 100,
77
79
  window: "day",
package/dist/index.d.ts CHANGED
@@ -76,18 +76,18 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
76
76
  create: (input: import("./resources.ts").Input<"/checks", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
77
77
  allowed: boolean;
78
78
  id: string;
79
+ limits: components["schemas"]["CheckLimit"][];
79
80
  reason: {
80
- code: "limit_exceeded" | "too_many_reservations";
81
- limit: string | null;
81
+ code: "limit_reached" | "too_many_reservations";
82
+ limitId: string | null;
82
83
  message: string;
83
84
  } | null;
84
- remaining: components["schemas"]["CheckRemaining"][];
85
85
  reservation?: {
86
86
  expiresAt: string;
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>;
@@ -100,15 +100,33 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
100
100
  every: number;
101
101
  failMode: "open" | "closed";
102
102
  id: string;
103
+ lastCheckedAt: string | null;
103
104
  name: string;
104
105
  rolling: boolean;
105
- start: "calendar" | "anchor" | "first_use";
106
+ start: "calendar" | "billing_date" | "first_use";
106
107
  subject: components["schemas"]["LimitSubject"];
107
108
  unit: string;
108
109
  updatedAt: string;
109
110
  version: number;
110
111
  window: "minute" | "hour" | "day" | "week" | "month" | "year" | "total" | "concurrent";
111
112
  }>;
113
+ credits: {
114
+ create: (limitId: string | number, input: import("./resources.ts").Input<"/limits/{limitId}/credits", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
115
+ amount: number;
116
+ createdAt: string;
117
+ createdByUserId: string | null;
118
+ id: string;
119
+ limitId: string;
120
+ note: string;
121
+ subject: {
122
+ id: string;
123
+ kind: string;
124
+ };
125
+ }>;
126
+ list: (limitId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<{
127
+ items: components["schemas"]["Credit"][];
128
+ }>;
129
+ };
112
130
  get: (limitId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<{
113
131
  action: string;
114
132
  amount: number;
@@ -118,32 +136,16 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
118
136
  every: number;
119
137
  failMode: "open" | "closed";
120
138
  id: string;
139
+ lastCheckedAt: string | null;
121
140
  name: string;
122
141
  rolling: boolean;
123
- start: "calendar" | "anchor" | "first_use";
142
+ start: "calendar" | "billing_date" | "first_use";
124
143
  subject: components["schemas"]["LimitSubject"];
125
144
  unit: string;
126
145
  updatedAt: string;
127
146
  version: number;
128
147
  window: "minute" | "hour" | "day" | "week" | "month" | "year" | "total" | "concurrent";
129
148
  }>;
130
- grants: {
131
- create: (limitId: string | number, input: import("./resources.ts").Input<"/limits/{limitId}/grants", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
132
- amount: number;
133
- createdAt: string;
134
- createdByUserId: string | null;
135
- id: string;
136
- limitId: string;
137
- note: string;
138
- subject: {
139
- id: string;
140
- kind: string;
141
- };
142
- }>;
143
- list: (limitId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<{
144
- items: components["schemas"]["LimitGrant"][];
145
- }>;
146
- };
147
149
  list: (input?: import("./resources.ts").Input<"/limits", "get">, options?: import("./resources.ts").RequestOptions) => Promise<{
148
150
  items: components["schemas"]["Limit"][];
149
151
  nextCursor: string | null;
@@ -157,9 +159,10 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
157
159
  every: number;
158
160
  failMode: "open" | "closed";
159
161
  id: string;
162
+ lastCheckedAt: string | null;
160
163
  name: string;
161
164
  rolling: boolean;
162
- start: "calendar" | "anchor" | "first_use";
165
+ start: "calendar" | "billing_date" | "first_use";
163
166
  subject: components["schemas"]["LimitSubject"];
164
167
  unit: string;
165
168
  updatedAt: string;
@@ -169,7 +172,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
169
172
  sync: (input: import("./resources.ts").Input<"/limits/sync", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
170
173
  archived: string[];
171
174
  created: string[];
172
- dryRun: boolean;
175
+ preview: boolean;
173
176
  unchanged: string[];
174
177
  updated: {
175
178
  changes: {
@@ -190,9 +193,10 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
190
193
  every: number;
191
194
  failMode: "open" | "closed";
192
195
  id: string;
196
+ lastCheckedAt: string | null;
193
197
  name: string;
194
198
  rolling: boolean;
195
- start: "calendar" | "anchor" | "first_use";
199
+ start: "calendar" | "billing_date" | "first_use";
196
200
  subject: components["schemas"]["LimitSubject"];
197
201
  unit: string;
198
202
  updatedAt: string;
@@ -225,7 +229,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
225
229
  };
226
230
  subjects: {
227
231
  get: (subjectKind: string | number, subjectId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<{
228
- anchor: string | null;
232
+ billingDate: string | null;
229
233
  subject: components["schemas"]["SubjectRef"];
230
234
  }>;
231
235
  reset: (subjectKind: string | number, subjectId: string | number, input?: import("./resources.ts").Input<"/subjects/{subjectKind}/{subjectId}/reset", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
@@ -233,7 +237,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
233
237
  subject: components["schemas"]["SubjectRef"];
234
238
  }>;
235
239
  update: (subjectKind: string | number, subjectId: string | number, input: import("./resources.ts").Input<"/subjects/{subjectKind}/{subjectId}", "patch">, options?: import("./resources.ts").RequestOptions) => Promise<{
236
- anchor: string | null;
240
+ billingDate: string | null;
237
241
  subject: components["schemas"]["SubjectRef"];
238
242
  }>;
239
243
  };
@@ -243,7 +247,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
243
247
  limitId: string;
244
248
  points: {
245
249
  allowed: number;
246
- denied: number;
250
+ refused: number;
247
251
  start: string;
248
252
  used: number;
249
253
  }[];
@@ -253,7 +257,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
253
257
  limits: {
254
258
  action: string;
255
259
  amount: number;
256
- limit: string;
260
+ limitId: string;
257
261
  name: string;
258
262
  remaining: number;
259
263
  resetsAt: string | null;
@@ -274,8 +278,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
274
278
  }>;
275
279
  subjects: (input: import("./resources.ts").Input<"/usage/subjects", "get">, options?: import("./resources.ts").RequestOptions) => Promise<{
276
280
  items: {
277
- denied: number;
278
- over: boolean;
281
+ refused: number;
279
282
  remaining: number;
280
283
  subject: {
281
284
  id: string;
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({
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * PRODUCT-OWNED: Limitry's SDK helpers (fork-owned seam — docs/conventions.md
3
3
  * "Fork-owned seams"): re-exported by the SDK entry and published with
4
- * it. `guard` / `hold` (docs/product/roadmap.md slice 6) sit on the client
4
+ * it. `guard` / `reserve` (docs/product/roadmap.md slice 6) sit on the client
5
5
  * (`limitry.guard(…)`, the SDK's `productMethods` seam); `createLimits(client,
6
6
  * options)` gives them with your own timeout or fail mode.
7
7
  */
8
8
  import type { BaseClient } from "../index.ts";
9
- export { createLimits, LimitExceeded, type GuardRequest, type Held, type HoldRequest, type Limits, type LimitsOptions, } from "./limits.ts";
10
- /** Limitry's methods on the client: `limitry.guard(…)`, `limitry.hold(…)`, `limitry.standing(…)`. */
9
+ export { createLimits, LimitReached, Unavailable, type GuardRequest, type Limits, type LimitsOptions, type Reservation, type ReserveRequest, } from "./limits.ts";
10
+ /** Limitry's methods on the client: `limitry.guard(…)`, `limitry.reserve(…)`, `limitry.standing(…)`. */
11
11
  export declare const productMethods: (client: BaseClient) => {
12
12
  guard: <T>(request: import("./limits.ts").GuardRequest, fn: () => Promise<T>) => Promise<T>;
13
- hold: <T>(request: import("./limits.ts").HoldRequest, fn: (held: import("./limits.ts").Held) => Promise<T>) => Promise<T>;
13
+ reserve: <T>(request: import("./limits.ts").ReserveRequest, fn: (reservation: import("./limits.ts").Reservation) => Promise<T>) => Promise<T>;
14
14
  standing: (request: {
15
15
  subject: {
16
16
  kind: string;
@@ -1,7 +1,11 @@
1
1
  import { createLimits } from "./limits.js";
2
- export { createLimits, LimitExceeded, } from "./limits.js";
3
- /** Limitry's methods on the client: `limitry.guard(…)`, `limitry.hold(…)`, `limitry.standing(…)`. */
2
+ export { createLimits, LimitReached, Unavailable, } from "./limits.js";
3
+ /** Limitry's methods on the client: `limitry.guard(…)`, `limitry.reserve(…)`, `limitry.standing(…)`. */
4
4
  export const productMethods = (client) => {
5
5
  const limits = createLimits(client);
6
- return { guard: limits.guard, hold: limits.hold, standing: limits.standing };
6
+ return {
7
+ guard: limits.guard,
8
+ reserve: limits.reserve,
9
+ standing: limits.standing,
10
+ };
7
11
  };
@@ -13,33 +13,45 @@ import type { BaseClient, Schemas } from "../index.ts";
13
13
  * );
14
14
  *
15
15
  * // Cost known afterwards (an AI reply's cents), or "at most N at
16
- * // once": hold an estimate, do the work, commit what it used —
17
- * // released on failure.
18
- * const answer = await limitry.hold(
19
- * { subject: { kind: "user", id: user.id }, action: "ai-call", estimate: 5 },
20
- * async (held) => {
16
+ * // once": reserve what it will probably cost, do the work, commit
17
+ * // what it used — released on failure.
18
+ * const answer = await limitry.reserve(
19
+ * { subject: { kind: "user", id: user.id }, action: "ai-call", cost: 5 },
20
+ * async (reservation) => {
21
21
  * const res = await callModel(prompt);
22
- * held.cost(priceCents(res.usage));
22
+ * reservation.cost(priceCents(res.usage));
23
23
  * return res;
24
24
  * },
25
25
  * );
26
26
  *
27
- * A refusal throws `LimitExceeded` (with `retryAfterSeconds`). When
28
- * Limitry cannot be reached (an error, a 5xx, or no answer within
29
- * `timeoutMs`) — or the workspace is past its plan's allowance (402
30
- * `plan_limit_reached`) — each limit's fail mode decides: the last decision's
31
- * `failMode` for this subject and action is remembered in memory, and
32
- * `closed` refuses (`LimitExceeded` with reason `unavailable`) while
33
- * `open` — the default, and the answer before any decision was seen —
34
- * lets the work run unmetered.
27
+ * A refusal throws `LimitReached` (with `limitId` and
28
+ * `retryAfterSeconds`). When Limitry cannot be reached — an error, a 5xx,
29
+ * no answer within `timeoutMs`, the workspace past its plan's allowance
30
+ * (402 `plan_limit_reached`) or the edge's flood guard (429) — each
31
+ * limit's fail mode decides: the last decision's `failMode` for this
32
+ * subject and action is remembered in memory, and `closed` throws
33
+ * `Unavailable` while `open` — the default, and the answer before any
34
+ * decision was seen — lets the work run unmetered.
35
35
  *
36
- * Retries are safe (docs/product idempotency): each guard or hold sends
37
- * one Idempotency-Key, which the client's own retries reuse, so a check
38
- * counts once however often it is sent. A hold whose check timed out may
39
- * have been granted all the same: once the work is done, `hold` sends the
40
- * check again with the same key — a replay, nothing counted twice — and
41
- * releases the hold it finds, rather than leave it taken until it expires.
42
- * Commits and releases retry too (settling twice answers the same).
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
+ *
47
+ * Retries are safe (docs/product idempotency): each guard or reserve
48
+ * sends one Idempotency-Key, which the client's own retries reuse, so a
49
+ * check counts once however often it is sent. A reservation whose check
50
+ * timed out may have been made all the same: once the work is done,
51
+ * `reserve` sends the check again with the same key — a replay, nothing
52
+ * counted twice — and releases the reservation it finds, rather than
53
+ * leave it taken until it expires. Commits and releases retry too
54
+ * (committing or releasing twice answers the same).
43
55
  */
44
56
  type Decision = Schemas["CheckDecision"];
45
57
  type Subject = {
@@ -60,40 +72,55 @@ export type GuardRequest = {
60
72
  /** Pools it also draws from (a team's, an org's). */
61
73
  shared?: Subject[];
62
74
  };
63
- export type HoldRequest = {
75
+ export type ReserveRequest = {
64
76
  subject: Subject;
65
77
  action: string;
66
- /** What it will probably use — held until you commit the actual cost. Default 1. */
67
- estimate?: number;
78
+ /** What it will probably use — reserved until you commit the actual cost. Default 1. */
79
+ cost?: number;
68
80
  shared?: Subject[];
69
- /** How long the hold lasts if never settled (default 300, at most 3600). */
81
+ /** How long the reservation lasts if never committed or released (default 300, at most 3600). */
70
82
  ttlSeconds?: number;
71
83
  };
72
- export type Held = {
84
+ export type Reservation = {
73
85
  /**
74
86
  * The cost actually used — committed when the work returns. Default: the
75
- * estimate. A whole number from 0 to 1,000,000,000, as the API commits:
76
- * anything else throws a RangeError here, in your code, rather than a
77
- * commit the API refuses after the work (whose usage would go uncounted).
87
+ * reserved cost. A whole number from 0 to 1,000,000,000, as the API
88
+ * commits: anything else throws a RangeError here, in your code, rather
89
+ * than a commit the API refuses after the work (whose usage would go
90
+ * uncounted).
78
91
  */
79
92
  cost(units: number): void;
80
93
  /**
81
- * Keep the hold for longer work: it expires `ttlSeconds` from now
82
- * (default 300, 1 to 3600). Throws the API's error, such as for a hold
83
- * that already expired.
94
+ * Keep the reservation for longer work: it expires `ttlSeconds` from now
95
+ * (default 300, 1 to 3600). Throws the API's error, such as for a
96
+ * reservation that already expired.
84
97
  */
85
98
  extend(ttlSeconds?: number): Promise<void>;
86
99
  };
87
- /** Not allowed: a limit is used up — or Limitry is unreachable and a limit fails closed. */
88
- export declare class LimitExceeded extends Error {
89
- readonly reason: "limit_exceeded" | "too_many_reservations" | "unavailable";
90
- /** The limit that refused; null when unavailable or too many holds. */
91
- readonly limit: string | null;
100
+ /** Refused: a limit is reached, or the subject holds too many open reservations. */
101
+ export declare class LimitReached extends Error {
102
+ readonly reason: "limit_reached" | "too_many_reservations";
103
+ /** The limit that refused; null for too many open reservations. */
104
+ readonly limitId: string | null;
92
105
  readonly retryAfterSeconds: number | null;
93
- readonly decision: Decision | null;
94
- constructor(decision: Decision | null);
106
+ readonly decision: Decision;
107
+ constructor(decision: Decision);
108
+ }
109
+ /**
110
+ * Limitry could not be reached (an error, a 5xx, a timeout, the plan's
111
+ * allowance used up, or the flood guard's 429) and a limit on this
112
+ * subject and action fails closed. With `open`, the work runs instead.
113
+ */
114
+ export declare class Unavailable extends Error {
115
+ constructor();
95
116
  }
96
- 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): {
97
124
  /**
98
125
  * Where a subject stands on every limit on it (and on `shared`
99
126
  * subjects): used, remaining, resetsAt — for showing an end user. Call
@@ -108,12 +135,12 @@ export declare function createLimits(client: BaseClient, options?: LimitsOptions
108
135
  /** Checks `cost`, runs `fn` only if allowed, and returns its result. */
109
136
  guard<T>(request: GuardRequest, fn: () => Promise<T>): Promise<T>;
110
137
  /**
111
- * Holds `estimate`, runs `fn`, then commits the cost it reported
112
- * (`held.cost(n)`, default the estimate) — or releases the hold if
113
- * `fn` throws, and rethrows. A concurrent limit's slot is held for
114
- * exactly as long as `fn` runs.
138
+ * Reserves `cost`, runs `fn`, then commits the cost it reported
139
+ * (`reservation.cost(n)`, default the reserved cost) — or releases the
140
+ * reservation if `fn` throws, and rethrows. A concurrent limit's slot
141
+ * is taken for exactly as long as `fn` runs.
115
142
  */
116
- hold<T>(request: HoldRequest, fn: (held: Held) => Promise<T>): Promise<T>;
143
+ reserve<T>(request: ReserveRequest, fn: (reservation: Reservation) => Promise<T>): Promise<T>;
117
144
  };
118
145
  export type Limits = ReturnType<typeof createLimits>;
119
146
  export {};