@limitry/sdk 1.0.0-rc.6 → 1.0.0-rc.8

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.
@@ -1,40 +1,40 @@
1
- /**
2
- * A refusal in the CLI's words, naming the limit rather than its id:
3
- * "Refused by AI replies per day: 0 replies left, resets …. Try again in
4
- * 42 s."
5
- */
6
- function refusal(decision) {
7
- const id = decision?.reason?.limit ?? null;
8
- const limit = decision?.remaining.find((r) => r.limit === id);
9
- const retry = decision?.retryAfterSeconds != null
10
- ? ` Try again in ${decision.retryAfterSeconds} s.`
11
- : "";
12
- if (!limit)
13
- return `Refused: limit ${id ?? "(unknown)"} is used up.${retry}`;
14
- const resets = limit.resetsAt ? `, resets ${limit.resetsAt}` : "";
15
- return `Refused by ${limit.name}: ${limit.remaining} ${limit.unit} left${resets}.${retry}`;
1
+ import { apiErrorFrom } from "../errors.js";
2
+ /** The largest cost a commit takes. */
3
+ const MAX_COST = 1_000_000_000;
4
+ function checkedCost(units) {
5
+ if (!Number.isInteger(units) || units < 0 || units > MAX_COST) {
6
+ throw new RangeError(`reservation.cost(${units}): the cost must be a whole number from 0 to ${MAX_COST}`);
7
+ }
8
+ return units;
16
9
  }
17
- /** Not allowed: a limit is used up — or Limitry is unreachable and a limit fails closed. */
18
- export class LimitExceeded extends Error {
10
+ /** Refused: a limit is reached, or the subject holds too many open reservations. */
11
+ export class LimitReached extends Error {
19
12
  reason;
20
- /** The limit that refused; null when unavailable or too many holds. */
21
- limit;
13
+ /** The limit that refused; null for too many open reservations. */
14
+ limitId;
22
15
  retryAfterSeconds;
23
16
  decision;
24
17
  constructor(decision) {
25
- const reason = decision?.reason?.code ?? "unavailable";
26
- super(reason === "unavailable"
27
- ? "Limitry could not be reached and a limit denies when unreachable"
28
- : reason === "too_many_reservations"
29
- ? "Too many open reservations for this subject"
30
- : refusal(decision));
31
- this.name = "LimitExceeded";
32
- this.reason = reason;
33
- this.limit = decision?.reason?.limit ?? null;
34
- this.retryAfterSeconds = decision?.retryAfterSeconds ?? null;
18
+ // The API's own sentence (reason.message), one wording everywhere.
19
+ super(decision.reason?.message ?? "Refused");
20
+ this.name = "LimitReached";
21
+ this.reason = decision.reason?.code ?? "limit_reached";
22
+ this.limitId = decision.reason?.limitId ?? null;
23
+ this.retryAfterSeconds = decision.retryAfterSeconds ?? null;
35
24
  this.decision = decision;
36
25
  }
37
26
  }
27
+ /**
28
+ * Limitry could not be reached (an error, a 5xx, a timeout, the plan's
29
+ * allowance used up, or the flood guard's 429) and a limit on this
30
+ * subject and action fails closed. With `open`, the work runs instead.
31
+ */
32
+ export class Unavailable extends Error {
33
+ constructor() {
34
+ super("Limitry could not be reached and a limit refuses when unreachable");
35
+ this.name = "Unavailable";
36
+ }
37
+ }
38
38
  const UNREACHABLE = Symbol("unreachable");
39
39
  /** No answer in time: unlike an error, the check may have been decided. */
40
40
  const TIMED_OUT = Symbol("timed out");
@@ -49,15 +49,15 @@ export function createLimits(client, options = {}) {
49
49
  const remember = (s, action, decision) => {
50
50
  if (options.failMode)
51
51
  return;
52
- const closed = decision.remaining.some((r) => r.failMode === "closed");
52
+ const closed = decision.limits.some((r) => r.failMode === "closed");
53
53
  remembered.set(key(s, action), closed ? "closed" : "open");
54
54
  if (remembered.size > 10_000) {
55
55
  remembered.delete(remembered.keys().next().value);
56
56
  }
57
57
  };
58
58
  /**
59
- * The decision — or UNREACHABLE (network error, 5xx, past the plan),
60
- * or TIMED_OUT (no answer within `wait`).
59
+ * The decision — or UNREACHABLE (network error, 5xx, past the plan, the
60
+ * flood guard's 429), or TIMED_OUT (no answer within `wait`).
61
61
  */
62
62
  async function check(body, idempotencyKey, wait = timeoutMs) {
63
63
  let timer;
@@ -75,10 +75,12 @@ export function createLimits(client, options = {}) {
75
75
  });
76
76
  if (data)
77
77
  return data;
78
- // Past the plan's allowance (402 plan_limit_reached): no decision
79
- // — each limit's fail mode applies, as if Limitry were unreachable.
80
- if (response.status === 402)
78
+ // Past the plan's allowance (402 plan_limit_reached), or stopped by
79
+ // the edge's flood guard (429): no decision — each limit's fail
80
+ // mode applies, as if Limitry were unreachable.
81
+ if (response.status === 402 || response.status === 429) {
81
82
  return UNREACHABLE;
83
+ }
82
84
  // A 4xx is the caller's mistake (bad request, bad key): say so.
83
85
  if (response.status < 500 && error) {
84
86
  return new Error(`Limitry refused the check (${response.status}): ${JSON.stringify(error)}`);
@@ -102,9 +104,9 @@ export function createLimits(client, options = {}) {
102
104
  }
103
105
  }
104
106
  /**
105
- * Settling never fails the caller's work: the client retries it (the
106
- * same settle answers the same), and a settle lost all the same expires
107
- * by itself.
107
+ * Committing or releasing never fails the caller's work: the client
108
+ * retries it (the same commit answers the same), and one lost all the
109
+ * same expires by itself.
108
110
  */
109
111
  async function settle(id, action, cost) {
110
112
  try {
@@ -123,12 +125,12 @@ export function createLimits(client, options = {}) {
123
125
  }
124
126
  }
125
127
  catch {
126
- // The hold expires on its own.
128
+ // The reservation expires on its own.
127
129
  }
128
130
  }
129
131
  /**
130
- * After a hold's check timed out: ask again with the same key (a replay
131
- * if Limitry decided it) and release any hold it was granted.
132
+ * After a reservation's check timed out: ask again with the same key (a
133
+ * replay if Limitry decided it) and release any reservation it made.
132
134
  */
133
135
  async function releaseUnheard(body, idempotencyKey) {
134
136
  const replay = await check(body, idempotencyKey, SETTLE_TIMEOUT_MS);
@@ -148,8 +150,7 @@ export function createLimits(client, options = {}) {
148
150
  const { data, error, response } = await client.GET("/usage/standing", {
149
151
  params: {
150
152
  query: {
151
- kind: request.subject.kind,
152
- id: request.subject.id,
153
+ subject: `${request.subject.kind}:${request.subject.id}`,
153
154
  ...(request.shared?.length
154
155
  ? {
155
156
  shared: request.shared
@@ -174,41 +175,46 @@ export function createLimits(client, options = {}) {
174
175
  }, crypto.randomUUID());
175
176
  if (decision === UNREACHABLE || decision === TIMED_OUT) {
176
177
  if (failMode(request.subject, request.action) === "closed") {
177
- throw new LimitExceeded(null);
178
+ throw new Unavailable();
178
179
  }
179
180
  return fn();
180
181
  }
181
182
  remember(request.subject, request.action, decision);
182
183
  if (!decision.allowed)
183
- throw new LimitExceeded(decision);
184
+ throw new LimitReached(decision);
184
185
  return fn();
185
186
  },
186
187
  /**
187
- * Holds `estimate`, runs `fn`, then commits the cost it reported
188
- * (`held.cost(n)`, default the estimate) — or releases the hold if
189
- * `fn` throws, and rethrows. A concurrent limit's slot is held for
190
- * exactly as long as `fn` runs.
188
+ * Reserves `cost`, runs `fn`, then commits the cost it reported
189
+ * (`reservation.cost(n)`, default the reserved cost) — or releases the
190
+ * reservation if `fn` throws, and rethrows. A concurrent limit's slot
191
+ * is taken for exactly as long as `fn` runs.
191
192
  */
192
- async hold(request, fn) {
193
- const estimate = request.estimate ?? 1;
193
+ async reserve(request, fn) {
194
+ const reserved = request.cost ?? 1;
194
195
  const body = {
195
196
  subject: request.subject,
196
197
  shared: request.shared,
197
198
  action: request.action,
198
- cost: estimate,
199
+ cost: reserved,
199
200
  mode: "reserve",
200
201
  ttlSeconds: request.ttlSeconds,
201
202
  };
202
203
  const idempotencyKey = crypto.randomUUID();
203
204
  const decision = await check(body, idempotencyKey);
204
- const unmetered = { cost: () => { }, extend: async () => { } };
205
+ const unmetered = {
206
+ // Checked even unmetered: the mistake shows whether or not
207
+ // Limitry answered.
208
+ cost: (units) => void checkedCost(units),
209
+ extend: async () => { },
210
+ };
205
211
  if (decision === UNREACHABLE || decision === TIMED_OUT) {
206
212
  const unheard = () => decision === TIMED_OUT
207
213
  ? releaseUnheard(body, idempotencyKey)
208
214
  : Promise.resolve();
209
215
  if (failMode(request.subject, request.action) === "closed") {
210
216
  await unheard();
211
- throw new LimitExceeded(null);
217
+ throw new Unavailable();
212
218
  }
213
219
  try {
214
220
  return await fn(unmetered);
@@ -219,25 +225,28 @@ export function createLimits(client, options = {}) {
219
225
  }
220
226
  remember(request.subject, request.action, decision);
221
227
  if (!decision.allowed || !decision.reservation) {
222
- throw new LimitExceeded(decision);
228
+ throw new LimitReached(decision);
223
229
  }
224
230
  const id = decision.reservation.id;
225
- let used = estimate;
226
- const held = {
231
+ let used = reserved;
232
+ const reservation = {
227
233
  cost(units) {
228
- used = units;
234
+ used = checkedCost(units);
229
235
  },
230
236
  async extend(ttlSeconds) {
231
- await client.POST("/reservations/{reservationId}/extend", {
237
+ // Passed as given: the API refuses a bad one (0, past 3600).
238
+ const { error, response } = await client.POST("/reservations/{reservationId}/extend", {
232
239
  params: { path: { reservationId: id } },
233
- body: ttlSeconds ? { ttlSeconds } : {},
240
+ body: ttlSeconds === undefined ? {} : { ttlSeconds },
234
241
  signal: AbortSignal.timeout(timeoutMs),
235
242
  });
243
+ if (error !== undefined)
244
+ throw apiErrorFrom(error, response);
236
245
  },
237
246
  };
238
247
  let result;
239
248
  try {
240
- result = await fn(held);
249
+ result = await fn(reservation);
241
250
  }
242
251
  catch (error) {
243
252
  await settle(id, "release");
@@ -5,11 +5,12 @@ export declare function resources(client: RawClient): {
5
5
  create: (input: Input<"/checks", "post">, options?: RequestOptions) => Promise<{
6
6
  allowed: boolean;
7
7
  id: string;
8
+ limits: import("./v1.gen.ts").components["schemas"]["CheckLimit"][];
8
9
  reason: {
9
- code: "limit_exceeded" | "too_many_reservations";
10
- limit: string | null;
10
+ code: "limit_reached" | "too_many_reservations";
11
+ limitId: string | null;
12
+ message: string;
11
13
  } | null;
12
- remaining: import("./v1.gen.ts").components["schemas"]["CheckRemaining"][];
13
14
  reservation?: {
14
15
  expiresAt: string;
15
16
  id: string;
@@ -28,15 +29,33 @@ export declare function resources(client: RawClient): {
28
29
  every: number;
29
30
  failMode: "open" | "closed";
30
31
  id: string;
32
+ lastCheckedAt: string | null;
31
33
  name: string;
32
34
  rolling: boolean;
33
- start: "calendar" | "anchor" | "first_use";
35
+ start: "calendar" | "billing_date" | "first_use";
34
36
  subject: import("./v1.gen.ts").components["schemas"]["LimitSubject"];
35
37
  unit: string;
36
38
  updatedAt: string;
37
39
  version: number;
38
40
  window: "minute" | "hour" | "day" | "week" | "month" | "year" | "total" | "concurrent";
39
41
  }>;
42
+ credits: {
43
+ create: (limitId: string | number, input: Input<"/limits/{limitId}/credits", "post">, options?: RequestOptions) => Promise<{
44
+ amount: number;
45
+ createdAt: string;
46
+ createdByUserId: string | null;
47
+ id: string;
48
+ limitId: string;
49
+ note: string;
50
+ subject: {
51
+ id: string;
52
+ kind: string;
53
+ };
54
+ }>;
55
+ list: (limitId: string | number, options?: RequestOptions) => Promise<{
56
+ items: import("./v1.gen.ts").components["schemas"]["Credit"][];
57
+ }>;
58
+ };
40
59
  get: (limitId: string | number, options?: RequestOptions) => Promise<{
41
60
  action: string;
42
61
  amount: number;
@@ -46,32 +65,16 @@ export declare function resources(client: RawClient): {
46
65
  every: number;
47
66
  failMode: "open" | "closed";
48
67
  id: string;
68
+ lastCheckedAt: string | null;
49
69
  name: string;
50
70
  rolling: boolean;
51
- start: "calendar" | "anchor" | "first_use";
71
+ start: "calendar" | "billing_date" | "first_use";
52
72
  subject: import("./v1.gen.ts").components["schemas"]["LimitSubject"];
53
73
  unit: string;
54
74
  updatedAt: string;
55
75
  version: number;
56
76
  window: "minute" | "hour" | "day" | "week" | "month" | "year" | "total" | "concurrent";
57
77
  }>;
58
- grants: {
59
- create: (limitId: string | number, input: Input<"/limits/{limitId}/grants", "post">, options?: RequestOptions) => Promise<{
60
- amount: number;
61
- createdAt: string;
62
- createdByUserId: string | null;
63
- id: string;
64
- limitId: string;
65
- note: string;
66
- subject: {
67
- id: string;
68
- kind: string;
69
- };
70
- }>;
71
- list: (limitId: string | number, options?: RequestOptions) => Promise<{
72
- items: import("./v1.gen.ts").components["schemas"]["LimitGrant"][];
73
- }>;
74
- };
75
78
  list: (input?: Input<"/limits", "get">, options?: RequestOptions) => Promise<{
76
79
  items: import("./v1.gen.ts").components["schemas"]["Limit"][];
77
80
  nextCursor: string | null;
@@ -85,9 +88,10 @@ export declare function resources(client: RawClient): {
85
88
  every: number;
86
89
  failMode: "open" | "closed";
87
90
  id: string;
91
+ lastCheckedAt: string | null;
88
92
  name: string;
89
93
  rolling: boolean;
90
- start: "calendar" | "anchor" | "first_use";
94
+ start: "calendar" | "billing_date" | "first_use";
91
95
  subject: import("./v1.gen.ts").components["schemas"]["LimitSubject"];
92
96
  unit: string;
93
97
  updatedAt: string;
@@ -97,7 +101,7 @@ export declare function resources(client: RawClient): {
97
101
  sync: (input: Input<"/limits/sync", "post">, options?: RequestOptions) => Promise<{
98
102
  archived: string[];
99
103
  created: string[];
100
- dryRun: boolean;
104
+ preview: boolean;
101
105
  unchanged: string[];
102
106
  updated: {
103
107
  changes: {
@@ -118,9 +122,10 @@ export declare function resources(client: RawClient): {
118
122
  every: number;
119
123
  failMode: "open" | "closed";
120
124
  id: string;
125
+ lastCheckedAt: string | null;
121
126
  name: string;
122
127
  rolling: boolean;
123
- start: "calendar" | "anchor" | "first_use";
128
+ start: "calendar" | "billing_date" | "first_use";
124
129
  subject: import("./v1.gen.ts").components["schemas"]["LimitSubject"];
125
130
  unit: string;
126
131
  updatedAt: string;
@@ -153,7 +158,7 @@ export declare function resources(client: RawClient): {
153
158
  };
154
159
  subjects: {
155
160
  get: (subjectKind: string | number, subjectId: string | number, options?: RequestOptions) => Promise<{
156
- anchor: string | null;
161
+ billingDate: string | null;
157
162
  subject: import("./v1.gen.ts").components["schemas"]["SubjectRef"];
158
163
  }>;
159
164
  reset: (subjectKind: string | number, subjectId: string | number, input?: Input<"/subjects/{subjectKind}/{subjectId}/reset", "post">, options?: RequestOptions) => Promise<{
@@ -161,7 +166,7 @@ export declare function resources(client: RawClient): {
161
166
  subject: import("./v1.gen.ts").components["schemas"]["SubjectRef"];
162
167
  }>;
163
168
  update: (subjectKind: string | number, subjectId: string | number, input: Input<"/subjects/{subjectKind}/{subjectId}", "patch">, options?: RequestOptions) => Promise<{
164
- anchor: string | null;
169
+ billingDate: string | null;
165
170
  subject: import("./v1.gen.ts").components["schemas"]["SubjectRef"];
166
171
  }>;
167
172
  };
@@ -171,7 +176,7 @@ export declare function resources(client: RawClient): {
171
176
  limitId: string;
172
177
  points: {
173
178
  allowed: number;
174
- denied: number;
179
+ refused: number;
175
180
  start: string;
176
181
  used: number;
177
182
  }[];
@@ -181,7 +186,7 @@ export declare function resources(client: RawClient): {
181
186
  limits: {
182
187
  action: string;
183
188
  amount: number;
184
- limit: string;
189
+ limitId: string;
185
190
  name: string;
186
191
  remaining: number;
187
192
  resetsAt: string | null;
@@ -202,8 +207,7 @@ export declare function resources(client: RawClient): {
202
207
  }>;
203
208
  subjects: (input: Input<"/usage/subjects", "get">, options?: RequestOptions) => Promise<{
204
209
  items: {
205
- denied: number;
206
- over: boolean;
210
+ refused: number;
207
211
  remaining: number;
208
212
  subject: {
209
213
  id: string;
@@ -11,11 +11,11 @@ export function resources(client) {
11
11
  limits: {
12
12
  archive: /** Archive a limit */ (limitId, options) => call(client, { "method": "post", "path": "/limits/{limitId}/archive", "query": [], "hasBody": false }, { "limitId": limitId }, undefined, options),
13
13
  create: /** Create a limit */ (input, options) => call(client, { "method": "post", "path": "/limits", "query": [], "hasBody": true }, {}, input, options),
14
- get: /** Get a limit */ (limitId, options) => call(client, { "method": "get", "path": "/limits/{limitId}", "query": [], "hasBody": false }, { "limitId": limitId }, undefined, options),
15
- grants: {
16
- create: /** Add credits to a balance */ (limitId, input, options) => call(client, { "method": "post", "path": "/limits/{limitId}/grants", "query": [], "hasBody": true }, { "limitId": limitId }, input, options),
17
- list: /** List a balance's grants */ (limitId, options) => call(client, { "method": "get", "path": "/limits/{limitId}/grants", "query": [], "hasBody": false }, { "limitId": limitId }, undefined, options),
14
+ credits: {
15
+ create: /** Add credits to a balance */ (limitId, input, options) => call(client, { "method": "post", "path": "/limits/{limitId}/credits", "query": [], "hasBody": true }, { "limitId": limitId }, input, options),
16
+ list: /** List a balance's credits */ (limitId, options) => call(client, { "method": "get", "path": "/limits/{limitId}/credits", "query": [], "hasBody": false }, { "limitId": limitId }, undefined, options),
18
17
  },
18
+ get: /** Get a limit */ (limitId, options) => call(client, { "method": "get", "path": "/limits/{limitId}", "query": [], "hasBody": false }, { "limitId": limitId }, undefined, options),
19
19
  list: /** List limits */ (input, options) => call(client, { "method": "get", "path": "/limits", "query": ["limit", "cursor", "subjectKind", "action", "archived"], "hasBody": false }, {}, input, options),
20
20
  restore: /** Restore an archived limit */ (limitId, options) => call(client, { "method": "post", "path": "/limits/{limitId}/restore", "query": [], "hasBody": false }, { "limitId": limitId }, undefined, options),
21
21
  sync: /** Sync limits from a list */ (input, options) => call(client, { "method": "post", "path": "/limits/sync", "query": [], "hasBody": true }, {}, input, options),
@@ -29,11 +29,11 @@ export function resources(client) {
29
29
  subjects: {
30
30
  get: /** Get a subject */ (subjectKind, subjectId, options) => call(client, { "method": "get", "path": "/subjects/{subjectKind}/{subjectId}", "query": [], "hasBody": false }, { "subjectKind": subjectKind, "subjectId": subjectId }, undefined, options),
31
31
  reset: /** Reset a subject's limits */ (subjectKind, subjectId, input, options) => call(client, { "method": "post", "path": "/subjects/{subjectKind}/{subjectId}/reset", "query": [], "hasBody": true }, { "subjectKind": subjectKind, "subjectId": subjectId }, input, options),
32
- update: /** Set a subject's anchor */ (subjectKind, subjectId, input, options) => call(client, { "method": "patch", "path": "/subjects/{subjectKind}/{subjectId}", "query": [], "hasBody": true }, { "subjectKind": subjectKind, "subjectId": subjectId }, input, options),
32
+ update: /** Set a subject's billing date */ (subjectKind, subjectId, input, options) => call(client, { "method": "patch", "path": "/subjects/{subjectKind}/{subjectId}", "query": [], "hasBody": true }, { "subjectKind": subjectKind, "subjectId": subjectId }, input, options),
33
33
  },
34
34
  usage: {
35
- query: /** Get a limit's usage over time */ (input, options) => call(client, { "method": "get", "path": "/usage", "query": ["limitId", "subjectKind", "subjectId", "from", "to", "granularity"], "hasBody": false }, {}, input, options),
36
- standing: /** Get where a subject stands */ (input, options) => call(client, { "method": "get", "path": "/usage/standing", "query": ["kind", "id", "shared"], "hasBody": false }, {}, input, options),
35
+ query: /** Get a limit's usage over time */ (input, options) => call(client, { "method": "get", "path": "/usage", "query": ["limitId", "subject", "from", "to", "granularity"], "hasBody": false }, {}, input, options),
36
+ standing: /** Get where a subject stands */ (input, options) => call(client, { "method": "get", "path": "/usage/standing", "query": ["subject", "shared"], "hasBody": false }, {}, input, options),
37
37
  subjects: /** List who is using a limit now */ (input, options) => call(client, { "method": "get", "path": "/usage/subjects", "query": ["limitId", "limit"], "hasBody": false }, {}, input, options),
38
38
  },
39
39
  user: {