@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 +122 -0
- package/README.md +15 -13
- package/dist/index.d.ts +35 -32
- package/dist/index.js +2 -2
- package/dist/product/index.d.ts +4 -4
- package/dist/product/index.js +7 -3
- package/dist/product/limits.d.ts +72 -45
- package/dist/product/limits.js +139 -62
- package/dist/resources.gen.d.ts +35 -32
- package/dist/resources.gen.js +7 -7
- package/dist/retry.d.ts +38 -0
- package/dist/retry.js +85 -1
- package/dist/v1.gen.d.ts +202 -171
- package/package.json +1 -1
package/dist/product/limits.js
CHANGED
|
@@ -1,39 +1,57 @@
|
|
|
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) {
|
|
5
6
|
if (!Number.isInteger(units) || units < 0 || units > MAX_COST) {
|
|
6
|
-
throw new RangeError(`
|
|
7
|
+
throw new RangeError(`reservation.cost(${units}): the cost must be a whole number from 0 to ${MAX_COST}`);
|
|
7
8
|
}
|
|
8
9
|
return units;
|
|
9
10
|
}
|
|
10
|
-
/**
|
|
11
|
-
export class
|
|
11
|
+
/** Refused: a limit is reached, or the subject holds too many open reservations. */
|
|
12
|
+
export class LimitReached extends Error {
|
|
12
13
|
reason;
|
|
13
|
-
/** The limit that refused; null
|
|
14
|
-
|
|
14
|
+
/** The limit that refused; null for too many open reservations. */
|
|
15
|
+
limitId;
|
|
15
16
|
retryAfterSeconds;
|
|
16
17
|
decision;
|
|
17
18
|
constructor(decision) {
|
|
18
|
-
|
|
19
|
-
super(reason
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
this.reason = reason;
|
|
25
|
-
this.limit = decision?.reason?.limit ?? null;
|
|
26
|
-
this.retryAfterSeconds = decision?.retryAfterSeconds ?? null;
|
|
19
|
+
// The API's own sentence (reason.message), one wording everywhere.
|
|
20
|
+
super(decision.reason?.message ?? "Refused");
|
|
21
|
+
this.name = "LimitReached";
|
|
22
|
+
this.reason = decision.reason?.code ?? "limit_reached";
|
|
23
|
+
this.limitId = decision.reason?.limitId ?? null;
|
|
24
|
+
this.retryAfterSeconds = decision.retryAfterSeconds ?? null;
|
|
27
25
|
this.decision = decision;
|
|
28
26
|
}
|
|
29
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Limitry could not be reached (an error, a 5xx, a timeout, the plan's
|
|
30
|
+
* allowance used up, or the flood guard's 429) and a limit on this
|
|
31
|
+
* subject and action fails closed. With `open`, the work runs instead.
|
|
32
|
+
*/
|
|
33
|
+
export class Unavailable extends Error {
|
|
34
|
+
constructor() {
|
|
35
|
+
super("Limitry could not be reached and a limit refuses when unreachable");
|
|
36
|
+
this.name = "Unavailable";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
30
39
|
const UNREACHABLE = Symbol("unreachable");
|
|
31
40
|
/** No answer in time: unlike an error, the check may have been decided. */
|
|
32
41
|
const TIMED_OUT = Symbol("timed out");
|
|
33
42
|
/** How long a settle (or a replay to find a hold) may take, retries included. */
|
|
34
43
|
const SETTLE_TIMEOUT_MS = 5000;
|
|
35
|
-
|
|
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 = {}) {
|
|
36
52
|
const timeoutMs = options.timeoutMs ?? 1500;
|
|
53
|
+
const sleep = internals.sleep ?? wait;
|
|
54
|
+
const random = internals.random ?? Math.random;
|
|
37
55
|
/** The last fail mode seen per subject + action (closed if any limit is). */
|
|
38
56
|
const remembered = new Map();
|
|
39
57
|
const key = (s, action) => `${s.kind}\u0000${s.id}\u0000${action}`;
|
|
@@ -41,17 +59,32 @@ export function createLimits(client, options = {}) {
|
|
|
41
59
|
const remember = (s, action, decision) => {
|
|
42
60
|
if (options.failMode)
|
|
43
61
|
return;
|
|
44
|
-
const closed = decision.
|
|
62
|
+
const closed = decision.limits.some((r) => r.failMode === "closed");
|
|
45
63
|
remembered.set(key(s, action), closed ? "closed" : "open");
|
|
46
64
|
if (remembered.size > 10_000) {
|
|
47
65
|
remembered.delete(remembered.keys().next().value);
|
|
48
66
|
}
|
|
49
67
|
};
|
|
50
68
|
/**
|
|
51
|
-
* The decision — or UNREACHABLE (network error, 5xx, past the plan
|
|
52
|
-
* or TIMED_OUT (no answer within `wait`).
|
|
69
|
+
* The decision — or UNREACHABLE (network error, 5xx, past the plan, the
|
|
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`.
|
|
53
73
|
*/
|
|
54
|
-
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) {
|
|
55
88
|
let timer;
|
|
56
89
|
// Raced as well as aborted: a custom fetch (a service binding, a
|
|
57
90
|
// test) may ignore the signal.
|
|
@@ -62,15 +95,25 @@ export function createLimits(client, options = {}) {
|
|
|
62
95
|
try {
|
|
63
96
|
const { data, error, response } = await client.POST("/checks", {
|
|
64
97
|
body,
|
|
65
|
-
headers: {
|
|
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
|
+
},
|
|
66
105
|
signal: AbortSignal.timeout(wait),
|
|
67
106
|
});
|
|
68
107
|
if (data)
|
|
69
108
|
return data;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
109
|
+
if (isRetryLater(response, error))
|
|
110
|
+
return { later: retryAfterMs(response) };
|
|
111
|
+
// Past the plan's allowance (402 plan_limit_reached), or stopped by
|
|
112
|
+
// the edge's flood guard (429): no decision — each limit's fail
|
|
113
|
+
// mode applies, as if Limitry were unreachable.
|
|
114
|
+
if (response.status === 402 || response.status === 429) {
|
|
73
115
|
return UNREACHABLE;
|
|
116
|
+
}
|
|
74
117
|
// A 4xx is the caller's mistake (bad request, bad key): say so.
|
|
75
118
|
if (response.status < 500 && error) {
|
|
76
119
|
return new Error(`Limitry refused the check (${response.status}): ${JSON.stringify(error)}`);
|
|
@@ -87,40 +130,59 @@ export function createLimits(client, options = {}) {
|
|
|
87
130
|
const answer = await Promise.race([asked, late]);
|
|
88
131
|
if (answer instanceof Error)
|
|
89
132
|
throw answer;
|
|
90
|
-
|
|
133
|
+
if (typeof answer === "object" && "later" in answer)
|
|
134
|
+
return { kind: "later", retryAfterMs: answer.later };
|
|
135
|
+
return { kind: "answer", value: answer };
|
|
91
136
|
}
|
|
92
137
|
finally {
|
|
93
138
|
clearTimeout(timer);
|
|
94
139
|
}
|
|
95
140
|
}
|
|
96
141
|
/**
|
|
97
|
-
*
|
|
98
|
-
* same
|
|
99
|
-
* by itself.
|
|
142
|
+
* Committing or releasing never fails the caller's work: the client
|
|
143
|
+
* retries it (the same commit answers the same), and one lost all the
|
|
144
|
+
* same expires by itself.
|
|
100
145
|
*/
|
|
101
146
|
async function settle(id, action, cost) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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);
|
|
109
173
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
params: { path: { reservationId: id } },
|
|
113
|
-
signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS),
|
|
114
|
-
});
|
|
174
|
+
catch {
|
|
175
|
+
// The reservation expires on its own.
|
|
115
176
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
177
|
+
if (retryMs === null || waited + retryMs > MOVING_MS)
|
|
178
|
+
return;
|
|
179
|
+
waited += retryMs;
|
|
180
|
+
await sleep(retryMs);
|
|
119
181
|
}
|
|
120
182
|
}
|
|
121
183
|
/**
|
|
122
|
-
* After a
|
|
123
|
-
* if Limitry decided it) and release any
|
|
184
|
+
* After a reservation's check timed out: ask again with the same key (a
|
|
185
|
+
* replay if Limitry decided it) and release any reservation it made.
|
|
124
186
|
*/
|
|
125
187
|
async function releaseUnheard(body, idempotencyKey) {
|
|
126
188
|
const replay = await check(body, idempotencyKey, SETTLE_TIMEOUT_MS);
|
|
@@ -140,8 +202,7 @@ export function createLimits(client, options = {}) {
|
|
|
140
202
|
const { data, error, response } = await client.GET("/usage/standing", {
|
|
141
203
|
params: {
|
|
142
204
|
query: {
|
|
143
|
-
|
|
144
|
-
id: request.subject.id,
|
|
205
|
+
subject: `${request.subject.kind}:${request.subject.id}`,
|
|
145
206
|
...(request.shared?.length
|
|
146
207
|
? {
|
|
147
208
|
shared: request.shared
|
|
@@ -158,36 +219,46 @@ export function createLimits(client, options = {}) {
|
|
|
158
219
|
},
|
|
159
220
|
/** Checks `cost`, runs `fn` only if allowed, and returns its result. */
|
|
160
221
|
async guard(request, fn) {
|
|
161
|
-
const
|
|
222
|
+
const body = {
|
|
162
223
|
subject: request.subject,
|
|
163
224
|
shared: request.shared,
|
|
164
225
|
action: request.action,
|
|
165
226
|
cost: request.cost ?? 1,
|
|
166
|
-
}
|
|
227
|
+
};
|
|
228
|
+
const idempotencyKey = crypto.randomUUID();
|
|
229
|
+
const decision = await check(body, idempotencyKey);
|
|
167
230
|
if (decision === UNREACHABLE || decision === TIMED_OUT) {
|
|
168
231
|
if (failMode(request.subject, request.action) === "closed") {
|
|
169
|
-
throw new
|
|
232
|
+
throw new Unavailable();
|
|
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(() => { });
|
|
170
241
|
}
|
|
171
242
|
return fn();
|
|
172
243
|
}
|
|
173
244
|
remember(request.subject, request.action, decision);
|
|
174
245
|
if (!decision.allowed)
|
|
175
|
-
throw new
|
|
246
|
+
throw new LimitReached(decision);
|
|
176
247
|
return fn();
|
|
177
248
|
},
|
|
178
249
|
/**
|
|
179
|
-
*
|
|
180
|
-
* (`
|
|
181
|
-
* `fn` throws, and rethrows. A concurrent limit's slot
|
|
182
|
-
* exactly as long as `fn` runs.
|
|
250
|
+
* Reserves `cost`, runs `fn`, then commits the cost it reported
|
|
251
|
+
* (`reservation.cost(n)`, default the reserved cost) — or releases the
|
|
252
|
+
* reservation if `fn` throws, and rethrows. A concurrent limit's slot
|
|
253
|
+
* is taken for exactly as long as `fn` runs.
|
|
183
254
|
*/
|
|
184
|
-
async
|
|
185
|
-
const
|
|
255
|
+
async reserve(request, fn) {
|
|
256
|
+
const reserved = request.cost ?? 1;
|
|
186
257
|
const body = {
|
|
187
258
|
subject: request.subject,
|
|
188
259
|
shared: request.shared,
|
|
189
260
|
action: request.action,
|
|
190
|
-
cost:
|
|
261
|
+
cost: reserved,
|
|
191
262
|
mode: "reserve",
|
|
192
263
|
ttlSeconds: request.ttlSeconds,
|
|
193
264
|
};
|
|
@@ -205,7 +276,7 @@ export function createLimits(client, options = {}) {
|
|
|
205
276
|
: Promise.resolve();
|
|
206
277
|
if (failMode(request.subject, request.action) === "closed") {
|
|
207
278
|
await unheard();
|
|
208
|
-
throw new
|
|
279
|
+
throw new Unavailable();
|
|
209
280
|
}
|
|
210
281
|
try {
|
|
211
282
|
return await fn(unmetered);
|
|
@@ -216,11 +287,11 @@ export function createLimits(client, options = {}) {
|
|
|
216
287
|
}
|
|
217
288
|
remember(request.subject, request.action, decision);
|
|
218
289
|
if (!decision.allowed || !decision.reservation) {
|
|
219
|
-
throw new
|
|
290
|
+
throw new LimitReached(decision);
|
|
220
291
|
}
|
|
221
292
|
const id = decision.reservation.id;
|
|
222
|
-
let used =
|
|
223
|
-
const
|
|
293
|
+
let used = reserved;
|
|
294
|
+
const reservation = {
|
|
224
295
|
cost(units) {
|
|
225
296
|
used = checkedCost(units);
|
|
226
297
|
},
|
|
@@ -237,7 +308,7 @@ export function createLimits(client, options = {}) {
|
|
|
237
308
|
};
|
|
238
309
|
let result;
|
|
239
310
|
try {
|
|
240
|
-
result = await fn(
|
|
311
|
+
result = await fn(reservation);
|
|
241
312
|
}
|
|
242
313
|
catch (error) {
|
|
243
314
|
await settle(id, "release");
|
|
@@ -248,3 +319,9 @@ export function createLimits(client, options = {}) {
|
|
|
248
319
|
},
|
|
249
320
|
};
|
|
250
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
|
+
}
|
package/dist/resources.gen.d.ts
CHANGED
|
@@ -5,18 +5,18 @@ 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: "
|
|
10
|
-
|
|
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;
|
|
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>;
|
|
@@ -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" | "
|
|
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" | "
|
|
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" | "
|
|
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
|
-
|
|
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" | "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
over: boolean;
|
|
210
|
+
refused: number;
|
|
208
211
|
remaining: number;
|
|
209
212
|
subject: {
|
|
210
213
|
id: string;
|
package/dist/resources.gen.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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", "
|
|
36
|
-
standing: /** Get where a subject stands */ (input, options) => call(client, { "method": "get", "path": "/usage/standing", "query": ["
|
|
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: {
|
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
|
/**
|