@limitry/sdk 1.0.0-rc.7 → 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.
@@ -3,30 +3,38 @@ import { apiErrorFrom } from "../errors.js";
3
3
  const MAX_COST = 1_000_000_000;
4
4
  function checkedCost(units) {
5
5
  if (!Number.isInteger(units) || units < 0 || units > MAX_COST) {
6
- throw new RangeError(`held.cost(${units}): the cost must be a whole number from 0 to ${MAX_COST}`);
6
+ throw new RangeError(`reservation.cost(${units}): the cost must be a whole number from 0 to ${MAX_COST}`);
7
7
  }
8
8
  return units;
9
9
  }
10
- /** Not allowed: a limit is used up — or Limitry is unreachable and a limit fails closed. */
11
- 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 {
12
12
  reason;
13
- /** The limit that refused; null when unavailable or too many holds. */
14
- limit;
13
+ /** The limit that refused; null for too many open reservations. */
14
+ limitId;
15
15
  retryAfterSeconds;
16
16
  decision;
17
17
  constructor(decision) {
18
- const reason = decision?.reason?.code ?? "unavailable";
19
- super(reason === "unavailable"
20
- ? "Limitry could not be reached and a limit denies when unreachable"
21
- : // The API's own sentence (reason.message), one wording everywhere.
22
- (decision?.reason?.message ?? "Refused"));
23
- this.name = "LimitExceeded";
24
- this.reason = reason;
25
- this.limit = decision?.reason?.limit ?? null;
26
- 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;
27
24
  this.decision = decision;
28
25
  }
29
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
+ }
30
38
  const UNREACHABLE = Symbol("unreachable");
31
39
  /** No answer in time: unlike an error, the check may have been decided. */
32
40
  const TIMED_OUT = Symbol("timed out");
@@ -41,15 +49,15 @@ export function createLimits(client, options = {}) {
41
49
  const remember = (s, action, decision) => {
42
50
  if (options.failMode)
43
51
  return;
44
- const closed = decision.remaining.some((r) => r.failMode === "closed");
52
+ const closed = decision.limits.some((r) => r.failMode === "closed");
45
53
  remembered.set(key(s, action), closed ? "closed" : "open");
46
54
  if (remembered.size > 10_000) {
47
55
  remembered.delete(remembered.keys().next().value);
48
56
  }
49
57
  };
50
58
  /**
51
- * The decision — or UNREACHABLE (network error, 5xx, past the plan),
52
- * 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`).
53
61
  */
54
62
  async function check(body, idempotencyKey, wait = timeoutMs) {
55
63
  let timer;
@@ -67,10 +75,12 @@ export function createLimits(client, options = {}) {
67
75
  });
68
76
  if (data)
69
77
  return data;
70
- // Past the plan's allowance (402 plan_limit_reached): no decision
71
- // — each limit's fail mode applies, as if Limitry were unreachable.
72
- 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) {
73
82
  return UNREACHABLE;
83
+ }
74
84
  // A 4xx is the caller's mistake (bad request, bad key): say so.
75
85
  if (response.status < 500 && error) {
76
86
  return new Error(`Limitry refused the check (${response.status}): ${JSON.stringify(error)}`);
@@ -94,9 +104,9 @@ export function createLimits(client, options = {}) {
94
104
  }
95
105
  }
96
106
  /**
97
- * Settling never fails the caller's work: the client retries it (the
98
- * same settle answers the same), and a settle lost all the same expires
99
- * 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.
100
110
  */
101
111
  async function settle(id, action, cost) {
102
112
  try {
@@ -115,12 +125,12 @@ export function createLimits(client, options = {}) {
115
125
  }
116
126
  }
117
127
  catch {
118
- // The hold expires on its own.
128
+ // The reservation expires on its own.
119
129
  }
120
130
  }
121
131
  /**
122
- * After a hold's check timed out: ask again with the same key (a replay
123
- * 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.
124
134
  */
125
135
  async function releaseUnheard(body, idempotencyKey) {
126
136
  const replay = await check(body, idempotencyKey, SETTLE_TIMEOUT_MS);
@@ -140,8 +150,7 @@ export function createLimits(client, options = {}) {
140
150
  const { data, error, response } = await client.GET("/usage/standing", {
141
151
  params: {
142
152
  query: {
143
- kind: request.subject.kind,
144
- id: request.subject.id,
153
+ subject: `${request.subject.kind}:${request.subject.id}`,
145
154
  ...(request.shared?.length
146
155
  ? {
147
156
  shared: request.shared
@@ -166,28 +175,28 @@ export function createLimits(client, options = {}) {
166
175
  }, crypto.randomUUID());
167
176
  if (decision === UNREACHABLE || decision === TIMED_OUT) {
168
177
  if (failMode(request.subject, request.action) === "closed") {
169
- throw new LimitExceeded(null);
178
+ throw new Unavailable();
170
179
  }
171
180
  return fn();
172
181
  }
173
182
  remember(request.subject, request.action, decision);
174
183
  if (!decision.allowed)
175
- throw new LimitExceeded(decision);
184
+ throw new LimitReached(decision);
176
185
  return fn();
177
186
  },
178
187
  /**
179
- * Holds `estimate`, runs `fn`, then commits the cost it reported
180
- * (`held.cost(n)`, default the estimate) — or releases the hold if
181
- * `fn` throws, and rethrows. A concurrent limit's slot is held for
182
- * 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.
183
192
  */
184
- async hold(request, fn) {
185
- const estimate = request.estimate ?? 1;
193
+ async reserve(request, fn) {
194
+ const reserved = request.cost ?? 1;
186
195
  const body = {
187
196
  subject: request.subject,
188
197
  shared: request.shared,
189
198
  action: request.action,
190
- cost: estimate,
199
+ cost: reserved,
191
200
  mode: "reserve",
192
201
  ttlSeconds: request.ttlSeconds,
193
202
  };
@@ -205,7 +214,7 @@ export function createLimits(client, options = {}) {
205
214
  : Promise.resolve();
206
215
  if (failMode(request.subject, request.action) === "closed") {
207
216
  await unheard();
208
- throw new LimitExceeded(null);
217
+ throw new Unavailable();
209
218
  }
210
219
  try {
211
220
  return await fn(unmetered);
@@ -216,11 +225,11 @@ export function createLimits(client, options = {}) {
216
225
  }
217
226
  remember(request.subject, request.action, decision);
218
227
  if (!decision.allowed || !decision.reservation) {
219
- throw new LimitExceeded(decision);
228
+ throw new LimitReached(decision);
220
229
  }
221
230
  const id = decision.reservation.id;
222
- let used = estimate;
223
- const held = {
231
+ let used = reserved;
232
+ const reservation = {
224
233
  cost(units) {
225
234
  used = checkedCost(units);
226
235
  },
@@ -237,7 +246,7 @@ export function createLimits(client, options = {}) {
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,12 +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;
11
12
  message: string;
12
13
  } | null;
13
- remaining: import("./v1.gen.ts").components["schemas"]["CheckRemaining"][];
14
14
  reservation?: {
15
15
  expiresAt: string;
16
16
  id: string;
@@ -29,15 +29,33 @@ export declare function resources(client: RawClient): {
29
29
  every: number;
30
30
  failMode: "open" | "closed";
31
31
  id: string;
32
+ lastCheckedAt: string | null;
32
33
  name: string;
33
34
  rolling: boolean;
34
- start: "calendar" | "anchor" | "first_use";
35
+ start: "calendar" | "billing_date" | "first_use";
35
36
  subject: import("./v1.gen.ts").components["schemas"]["LimitSubject"];
36
37
  unit: string;
37
38
  updatedAt: string;
38
39
  version: number;
39
40
  window: "minute" | "hour" | "day" | "week" | "month" | "year" | "total" | "concurrent";
40
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
+ };
41
59
  get: (limitId: string | number, options?: RequestOptions) => Promise<{
42
60
  action: string;
43
61
  amount: number;
@@ -47,32 +65,16 @@ export declare function resources(client: RawClient): {
47
65
  every: number;
48
66
  failMode: "open" | "closed";
49
67
  id: string;
68
+ lastCheckedAt: string | null;
50
69
  name: string;
51
70
  rolling: boolean;
52
- start: "calendar" | "anchor" | "first_use";
71
+ start: "calendar" | "billing_date" | "first_use";
53
72
  subject: import("./v1.gen.ts").components["schemas"]["LimitSubject"];
54
73
  unit: string;
55
74
  updatedAt: string;
56
75
  version: number;
57
76
  window: "minute" | "hour" | "day" | "week" | "month" | "year" | "total" | "concurrent";
58
77
  }>;
59
- grants: {
60
- create: (limitId: string | number, input: Input<"/limits/{limitId}/grants", "post">, options?: RequestOptions) => Promise<{
61
- amount: number;
62
- createdAt: string;
63
- createdByUserId: string | null;
64
- id: string;
65
- limitId: string;
66
- note: string;
67
- subject: {
68
- id: string;
69
- kind: string;
70
- };
71
- }>;
72
- list: (limitId: string | number, options?: RequestOptions) => Promise<{
73
- items: import("./v1.gen.ts").components["schemas"]["LimitGrant"][];
74
- }>;
75
- };
76
78
  list: (input?: Input<"/limits", "get">, options?: RequestOptions) => Promise<{
77
79
  items: import("./v1.gen.ts").components["schemas"]["Limit"][];
78
80
  nextCursor: string | null;
@@ -86,9 +88,10 @@ export declare function resources(client: RawClient): {
86
88
  every: number;
87
89
  failMode: "open" | "closed";
88
90
  id: string;
91
+ lastCheckedAt: string | null;
89
92
  name: string;
90
93
  rolling: boolean;
91
- start: "calendar" | "anchor" | "first_use";
94
+ start: "calendar" | "billing_date" | "first_use";
92
95
  subject: import("./v1.gen.ts").components["schemas"]["LimitSubject"];
93
96
  unit: string;
94
97
  updatedAt: string;
@@ -98,7 +101,7 @@ export declare function resources(client: RawClient): {
98
101
  sync: (input: Input<"/limits/sync", "post">, options?: RequestOptions) => Promise<{
99
102
  archived: string[];
100
103
  created: string[];
101
- dryRun: boolean;
104
+ preview: boolean;
102
105
  unchanged: string[];
103
106
  updated: {
104
107
  changes: {
@@ -119,9 +122,10 @@ export declare function resources(client: RawClient): {
119
122
  every: number;
120
123
  failMode: "open" | "closed";
121
124
  id: string;
125
+ lastCheckedAt: string | null;
122
126
  name: string;
123
127
  rolling: boolean;
124
- start: "calendar" | "anchor" | "first_use";
128
+ start: "calendar" | "billing_date" | "first_use";
125
129
  subject: import("./v1.gen.ts").components["schemas"]["LimitSubject"];
126
130
  unit: string;
127
131
  updatedAt: string;
@@ -154,7 +158,7 @@ export declare function resources(client: RawClient): {
154
158
  };
155
159
  subjects: {
156
160
  get: (subjectKind: string | number, subjectId: string | number, options?: RequestOptions) => Promise<{
157
- anchor: string | null;
161
+ billingDate: string | null;
158
162
  subject: import("./v1.gen.ts").components["schemas"]["SubjectRef"];
159
163
  }>;
160
164
  reset: (subjectKind: string | number, subjectId: string | number, input?: Input<"/subjects/{subjectKind}/{subjectId}/reset", "post">, options?: RequestOptions) => Promise<{
@@ -162,7 +166,7 @@ export declare function resources(client: RawClient): {
162
166
  subject: import("./v1.gen.ts").components["schemas"]["SubjectRef"];
163
167
  }>;
164
168
  update: (subjectKind: string | number, subjectId: string | number, input: Input<"/subjects/{subjectKind}/{subjectId}", "patch">, options?: RequestOptions) => Promise<{
165
- anchor: string | null;
169
+ billingDate: string | null;
166
170
  subject: import("./v1.gen.ts").components["schemas"]["SubjectRef"];
167
171
  }>;
168
172
  };
@@ -172,7 +176,7 @@ export declare function resources(client: RawClient): {
172
176
  limitId: string;
173
177
  points: {
174
178
  allowed: number;
175
- denied: number;
179
+ refused: number;
176
180
  start: string;
177
181
  used: number;
178
182
  }[];
@@ -182,7 +186,7 @@ export declare function resources(client: RawClient): {
182
186
  limits: {
183
187
  action: string;
184
188
  amount: number;
185
- limit: string;
189
+ limitId: string;
186
190
  name: string;
187
191
  remaining: number;
188
192
  resetsAt: string | null;
@@ -203,8 +207,7 @@ export declare function resources(client: RawClient): {
203
207
  }>;
204
208
  subjects: (input: Input<"/usage/subjects", "get">, options?: RequestOptions) => Promise<{
205
209
  items: {
206
- denied: number;
207
- over: boolean;
210
+ refused: number;
208
211
  remaining: number;
209
212
  subject: {
210
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: {