mbase-sdk 0.0.8 → 0.0.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/README.md +121 -11
- package/dist/index.cjs +60 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -9
- package/dist/index.d.ts +99 -9
- package/dist/index.js +60 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,10 +48,11 @@ denial — that is an answer, not a failure. Absence of entitlement is not
|
|
|
48
48
|
permission: a customer with no plan and no grant is denied.
|
|
49
49
|
|
|
50
50
|
`available` is the whole capacity — the plan's unused entitlement for the
|
|
51
|
-
current period plus
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
current period, plus what is left of a [rollover](#plan-allowances) carry, plus
|
|
52
|
+
every open grant — and is `null` under `no_cap`, where capacity is not a
|
|
53
|
+
number. It is one figure and not a breakdown: `check` answers from a single
|
|
54
|
+
cached integer, and itemising where the capacity came from would cost it that.
|
|
55
|
+
For the per-grant detail, read a customer's allowances.
|
|
55
56
|
|
|
56
57
|
A check answers **two gates**, and `allowed` needs both: the capacity has to
|
|
57
58
|
cover the quantity, and every rate limit in force — _at most so much of this
|
|
@@ -63,12 +64,20 @@ applies. A limit is protective and not commercial, so it is never folded into
|
|
|
63
64
|
`available`: a customer well inside their plan can still be limited, and one on
|
|
64
65
|
`no_cap` is precisely the customer a runaway loop costs most.
|
|
65
66
|
|
|
67
|
+
Under a **soft cap** — included usage, then overage; see
|
|
68
|
+
[Plan allowances](#plan-allowances) — the capacity admits any quantity, so only
|
|
69
|
+
a rate limit can refuse. `available` is what is left of the included amount and
|
|
70
|
+
stops at 0, `allow_overage: true` says why a quantity past it was still
|
|
71
|
+
allowed, and the overage in a request is `Math.max(quantity - available, 0)`.
|
|
72
|
+
`allow_overage` is `false` everywhere else, `no_cap` included.
|
|
73
|
+
|
|
66
74
|
### Recording usage
|
|
67
75
|
|
|
68
76
|
`track` counts work that already happened — the tokens were spent, the image
|
|
69
77
|
was generated — so it **never refuses on capacity**. A quantity beyond the
|
|
70
78
|
customer's capacity is recorded, drives `available` to 0, and the next `check`
|
|
71
|
-
says no. The answer is a receipt,
|
|
79
|
+
says no, unless the cap is [soft](#plan-allowances). The answer is a receipt,
|
|
80
|
+
with no verdict and nothing to branch on:
|
|
72
81
|
|
|
73
82
|
```ts
|
|
74
83
|
const { event } = await meterbase.track({
|
|
@@ -164,6 +173,7 @@ read — refusals included — so it is not the call to put in front of cheap wo
|
|
|
164
173
|
|
|
165
174
|
A reserve answers everything `check` answers, plus the hold: `reservation_id`,
|
|
166
175
|
the `quantity` it set aside, and `expires_at`. `available` is already net of it.
|
|
176
|
+
Past a soft cap it holds rather than refuses, and `available` stops at 0.
|
|
167
177
|
A refusal holds nothing, so it carries no id and no expiry, and `allowed`
|
|
168
178
|
narrows the type — after `if (!hold.allowed)`, `commit` and `release` are there.
|
|
169
179
|
|
|
@@ -311,6 +321,50 @@ const { data } = await meterbase.plans.allowances.list(plan.id)
|
|
|
311
321
|
Each `(plan, meter)` pair carries up to two lineages, `recurring` and
|
|
312
322
|
`one_time`, numbering themselves separately. `kind` defaults to `recurring`.
|
|
313
323
|
|
|
324
|
+
**A soft cap** keeps the amount and drops the refusal: included usage, then
|
|
325
|
+
overage.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
await meterbase.plans.allowances.set(plan.id, {
|
|
329
|
+
meter_id: meter.id,
|
|
330
|
+
amount: 5_000,
|
|
331
|
+
allow_overage: true,
|
|
332
|
+
})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Once `amount` is used up, `check` and `reserve` admit rather than refuse, with
|
|
336
|
+
`allow_overage: true` on the answer, and the usage past it is overage: read it
|
|
337
|
+
as `overage_used` on [the entitlement](#how-much-they-have-used). Rate limits
|
|
338
|
+
still refuse, thresholds still fire — a mark over 100 is an overage alert — and
|
|
339
|
+
a grant is spent before any overage is. `allow_overage` defaults to `false` and
|
|
340
|
+
is refused beside `no_cap` and on `one_time`. Like the amount, it is a term of
|
|
341
|
+
the version, so turning it on or off follows `apply_mode`: at each customer's
|
|
342
|
+
next cycle by default.
|
|
343
|
+
|
|
344
|
+
**Rollover** carries what a period leaves unused of `amount` into the next
|
|
345
|
+
period, which spends it first.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
await meterbase.plans.allowances.set(plan.id, {
|
|
349
|
+
meter_id: meter.id,
|
|
350
|
+
amount: 1_000,
|
|
351
|
+
rollover: true,
|
|
352
|
+
})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
A customer who uses 300 of September's 1,000 starts October with 1,700: the
|
|
356
|
+
700 carried goes first, and whatever October leaves of it is lost. A carry
|
|
357
|
+
lasts one period and never stacks, because only a period's own amount rolls on.
|
|
358
|
+
Both periods' versions have to roll over, so a move to a plan that does not
|
|
359
|
+
ends the carry, and a `restart` passes nothing on. `check` and `reserve` count
|
|
360
|
+
what is left of the carry in `available`; read it as `carried_in` and
|
|
361
|
+
`carried_used` on [the entitlement](#how-much-they-have-used). Thresholds
|
|
362
|
+
measure the amount alone, so usage the carry paid reaches no mark. `rollover`
|
|
363
|
+
defaults to `false`, is refused beside `no_cap` and on `one_time`, and is
|
|
364
|
+
allowed beside `allow_overage`, where the carry is spent before any overage.
|
|
365
|
+
It follows `apply_mode` like the amount, and the first carry arrives a boundary
|
|
366
|
+
after the version lands.
|
|
367
|
+
|
|
314
368
|
### A customer's plan
|
|
315
369
|
|
|
316
370
|
A customer's plan is an append-only history, not a field: assigning is the only
|
|
@@ -429,13 +483,60 @@ for (const meter of entitlement?.meters ?? []) {
|
|
|
429
483
|
|
|
430
484
|
`entitlement` is the plan's side of the current period, one entry per meter the
|
|
431
485
|
plan meters: the read behind a usage bar in your own app. `used` is what
|
|
432
|
-
`amount` is measured against, `total` adds what grants paid for,
|
|
433
|
-
`available` is what `check` would answer.
|
|
434
|
-
|
|
486
|
+
`amount` is measured against, `total` adds what the carry and grants paid for,
|
|
487
|
+
and `available` is what `check` would answer. `overage_used` is how far `used`
|
|
488
|
+
has run past `amount` — what a [soft cap](#plan-allowances) bills for, and
|
|
489
|
+
reported under a hard cap too — and `null` where there is no amount;
|
|
490
|
+
`allow_overage` says whether the cap in effect is soft. `carried_in` is what
|
|
491
|
+
the period before left unused and this one took in, `0` unless both versions
|
|
492
|
+
[roll over](#plan-allowances), and `carried_used` is what this period has spent
|
|
493
|
+
of it; what is left of it when the period ends is lost. Both are `null` where
|
|
494
|
+
there is no amount, and `carried_in` settles a minute after the boundary, with
|
|
495
|
+
the period it comes from. `rollover` says whether the version in effect rolls
|
|
496
|
+
over. Meters are named by id, and like `retrieve` it is `null` for a customer
|
|
497
|
+
holding no plan.
|
|
435
498
|
|
|
436
499
|
Unlike `check`, it is not answered from a cache, so read it where usage is
|
|
437
500
|
shown rather than on every request.
|
|
438
501
|
|
|
502
|
+
#### Closed periods
|
|
503
|
+
|
|
504
|
+
`customers.periods.last` is the newest period that has ended, with the figures
|
|
505
|
+
to bill from: per meter, the cap `check` answered at its last instant, `used`
|
|
506
|
+
against it, and `overage_used`, which is what a soft cap bills. `carried_in`
|
|
507
|
+
and `carried_used` are what a [rolling](#plan-allowances) allowance took from
|
|
508
|
+
the period before and spent of it, `carried_out` what it passed to the next,
|
|
509
|
+
and `total - used - carried_used` is what grants absorbed. They come from the
|
|
510
|
+
counters behind `check`, which count each event exactly once.
|
|
511
|
+
[Usage by day](#usage-by-day) is for where usage went, not for billing.
|
|
512
|
+
|
|
513
|
+
A billing job reads the newest, then walks back by passing each `from` as
|
|
514
|
+
`before`, until it reaches the `to` it last billed. `null` ends the walk:
|
|
515
|
+
nothing closed before that, or no plan at all.
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
const unbilled: ClosedPeriod[] = []
|
|
519
|
+
let period = await meterbase.customers.periods.last(customer.id)
|
|
520
|
+
|
|
521
|
+
while (period && period.to !== lastBilledTo) {
|
|
522
|
+
unbilled.unshift(period) // oldest first
|
|
523
|
+
period = await meterbase.customers.periods.last(customer.id, {
|
|
524
|
+
before: period.from, // exactly as it came
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Pass `from` back verbatim.** It carries microseconds and a `Date` keeps
|
|
530
|
+
milliseconds, so a `before` that went through a `Date` lands inside the older
|
|
531
|
+
period and skips it. That is why `before` is a string. Keep `to` as it came
|
|
532
|
+
too: it is what the loop compares.
|
|
533
|
+
|
|
534
|
+
For a minute after a period ends, while a track dated inside it may still be
|
|
535
|
+
committing, `last` throws `PeriodSettlingError`. Read again after its
|
|
536
|
+
`retryAfter` seconds; from then on the answer never changes. It is not retried
|
|
537
|
+
for you: the wait runs to a minute, and whether to sit it out is the job's
|
|
538
|
+
call.
|
|
539
|
+
|
|
439
540
|
#### Usage by day
|
|
440
541
|
|
|
441
542
|
```ts
|
|
@@ -508,8 +609,9 @@ secret, shown once when you add it and revealable from its settings.
|
|
|
508
609
|
```
|
|
509
610
|
|
|
510
611
|
`used` and `entitlement` are the plan's figures at the crossing; neither counts
|
|
511
|
-
grants, so ask `check` for capacity. `entitlement.reached` marks the
|
|
512
|
-
entitlement, not spent capacity: a grant can still have balance
|
|
612
|
+
grants or a carry, so ask `check` for capacity. `entitlement.reached` marks the
|
|
613
|
+
plan's entitlement, not spent capacity: a grant can still have balance, and a
|
|
614
|
+
soft cap admits past it.
|
|
513
615
|
|
|
514
616
|
### Verifying a delivery
|
|
515
617
|
|
|
@@ -608,7 +710,8 @@ try {
|
|
|
608
710
|
| `PermissionDeniedError` | 403 — the key may not use this route |
|
|
609
711
|
| `NotFoundError` | 404 |
|
|
610
712
|
| `ConflictError` | 409 — a key or external id is taken |
|
|
611
|
-
| `
|
|
713
|
+
| `PeriodSettlingError` | 409 — a `ConflictError`: the period ended under a minute ago, so read it again after `retryAfter` seconds |
|
|
714
|
+
| `InvalidRequestError` | 422 — a field failed validation; or `400 invalid_query`, a query parameter that did |
|
|
612
715
|
| `CycleChangeRequiresResetError` | 422 — an `InvalidRequestError`: the two plans' cycles differ, so use `restart` |
|
|
613
716
|
| `InvalidIdempotencyKeyError` | 422 — an `InvalidRequestError`: the key was not a UUIDv7 |
|
|
614
717
|
| `TooLateError` | 422 — an `InvalidRequestError`: the key is over an hour from the engine's clock, which it carries on `serverTime`. Nothing was recorded |
|
|
@@ -731,6 +834,13 @@ and the minted bonus summed into `available`; that tracked usage takes exactly
|
|
|
731
834
|
that much away, that a replay takes nothing, and that a reused key naming a
|
|
732
835
|
different event is refused; that a rate limit set on a meter comes back on
|
|
733
836
|
every `check` as a live window, and refuses with the allowance untouched; that
|
|
837
|
+
a soft cap admits and holds past its amount and reports the overage, and that
|
|
838
|
+
`allow_overage` is refused beside `no_cap` and on a one-time bonus; that a
|
|
839
|
+
period a `restart` closes settles for a minute, which the run waits out, and
|
|
840
|
+
then reads final with the overage it closed on; that a rolling allowance
|
|
841
|
+
carries what a period left unused into the next, which spends it first, and
|
|
842
|
+
the closed period reads what it passed on, and that `rollover` is refused
|
|
843
|
+
beside `no_cap` and on a one-time bonus; that
|
|
734
844
|
a scheduled plan change is dated ahead and reads as `pending`, that cancelling
|
|
735
845
|
it supersedes it, that a `restart` re-anchors the customer, and that two plans
|
|
736
846
|
on different cycles refuse a scheduled change; that soft deletes and archiving
|
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,14 @@ var ReservationMismatchError = class extends InvalidRequestError {
|
|
|
45
45
|
};
|
|
46
46
|
var CycleChangeRequiresResetError = class extends InvalidRequestError {
|
|
47
47
|
};
|
|
48
|
+
var PeriodSettlingError = class extends ConflictError {
|
|
49
|
+
/** Seconds until the period is final, from `Retry-After`. */
|
|
50
|
+
retryAfter;
|
|
51
|
+
constructor(args) {
|
|
52
|
+
super(args);
|
|
53
|
+
this.retryAfter = args.retryAfter;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
48
56
|
var RateLimitError = class extends APIError {
|
|
49
57
|
/** Seconds to wait, from `Retry-After`, when the engine sends one. */
|
|
50
58
|
retryAfter;
|
|
@@ -61,6 +69,14 @@ var TimeoutError = class extends ConnectionError {
|
|
|
61
69
|
};
|
|
62
70
|
function errorFromResponse(args) {
|
|
63
71
|
switch (args.status) {
|
|
72
|
+
case 400:
|
|
73
|
+
switch (args.code) {
|
|
74
|
+
// A parameter that failed validation, as a 422 is a field that did.
|
|
75
|
+
case "invalid_query":
|
|
76
|
+
return new InvalidRequestError(args);
|
|
77
|
+
default:
|
|
78
|
+
return new APIError(args);
|
|
79
|
+
}
|
|
64
80
|
case 401:
|
|
65
81
|
return new AuthenticationError(args);
|
|
66
82
|
case 403:
|
|
@@ -68,7 +84,12 @@ function errorFromResponse(args) {
|
|
|
68
84
|
case 404:
|
|
69
85
|
return new NotFoundError(args);
|
|
70
86
|
case 409:
|
|
71
|
-
|
|
87
|
+
switch (args.code) {
|
|
88
|
+
case "period_settling":
|
|
89
|
+
return new PeriodSettlingError(args);
|
|
90
|
+
default:
|
|
91
|
+
return new ConflictError(args);
|
|
92
|
+
}
|
|
72
93
|
case 422:
|
|
73
94
|
switch (args.code) {
|
|
74
95
|
case "cycle_change_requires_reset":
|
|
@@ -319,6 +340,35 @@ var CustomerAllowances = class {
|
|
|
319
340
|
}
|
|
320
341
|
};
|
|
321
342
|
|
|
343
|
+
// src/resources/customer-periods.ts
|
|
344
|
+
var CustomerPeriods = class {
|
|
345
|
+
#client;
|
|
346
|
+
constructor(client) {
|
|
347
|
+
this.#client = client;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The newest period that has ended, or that ended at or before `before`;
|
|
351
|
+
* `null` when there is none. Walk back by passing each period's `from` as
|
|
352
|
+
* `before`, exactly as it came. Throws `PeriodSettlingError` for a minute
|
|
353
|
+
* after a period ends.
|
|
354
|
+
*/
|
|
355
|
+
async last(id, params = {}, options) {
|
|
356
|
+
try {
|
|
357
|
+
return await this.#client.request({
|
|
358
|
+
...options,
|
|
359
|
+
method: "GET",
|
|
360
|
+
path: `/v1/customers/${encodeURIComponent(id)}/periods/last`,
|
|
361
|
+
query: { before: params.before }
|
|
362
|
+
});
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (error instanceof NotFoundError && (error.code === "no_plan_assigned" || error.code === "no_previous_period")) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
322
372
|
// src/resources/customer-plan.ts
|
|
323
373
|
var CustomerPlan = class {
|
|
324
374
|
#client;
|
|
@@ -477,11 +527,14 @@ var Customers = class {
|
|
|
477
527
|
plan;
|
|
478
528
|
/** Grants, which sit on top of whatever the plan gives. */
|
|
479
529
|
allowances;
|
|
530
|
+
/** Periods that have ended, with the figures to bill them from. */
|
|
531
|
+
periods;
|
|
480
532
|
#client;
|
|
481
533
|
constructor(client) {
|
|
482
534
|
this.#client = client;
|
|
483
535
|
this.plan = new CustomerPlan(client);
|
|
484
536
|
this.allowances = new CustomerAllowances(client);
|
|
537
|
+
this.periods = new CustomerPeriods(client);
|
|
485
538
|
}
|
|
486
539
|
create(params, options) {
|
|
487
540
|
return this.#client.request({
|
|
@@ -808,7 +861,9 @@ var Meterbase = class {
|
|
|
808
861
|
* to cover the quantity, and every rate-limit window in force has to admit
|
|
809
862
|
* it. `reason` names the one that said no. `rate_limits` reports each window
|
|
810
863
|
* whether or not it refused, so a caller can ease off as its headroom closes
|
|
811
|
-
* instead of discovering the ceiling by hitting it.
|
|
864
|
+
* instead of discovering the ceiling by hitting it. Under a soft cap
|
|
865
|
+
* (`allow_overage`) the capacity admits any quantity, so only a window can
|
|
866
|
+
* refuse.
|
|
812
867
|
*/
|
|
813
868
|
check(params, options) {
|
|
814
869
|
return this.#client.request({
|
|
@@ -822,7 +877,8 @@ var Meterbase = class {
|
|
|
822
877
|
* Records usage that already happened — the tokens were spent, the image
|
|
823
878
|
* was generated — so it never refuses on capacity. A quantity beyond the
|
|
824
879
|
* customer's capacity is recorded, drives `available` to 0, and the next
|
|
825
|
-
* `check` says no. The answer is a receipt,
|
|
880
|
+
* `check` says no, unless the plan's cap is soft. The answer is a receipt,
|
|
881
|
+
* not a verdict.
|
|
826
882
|
*
|
|
827
883
|
* `idempotency_key` is a UUIDv7, generated when omitted, so retrying this
|
|
828
884
|
* call — the SDK's own retries included — replays the original rather than
|
|
@@ -1014,6 +1070,7 @@ exports.InvalidRequestError = InvalidRequestError;
|
|
|
1014
1070
|
exports.Meterbase = Meterbase;
|
|
1015
1071
|
exports.MeterbaseError = MeterbaseError;
|
|
1016
1072
|
exports.NotFoundError = NotFoundError;
|
|
1073
|
+
exports.PeriodSettlingError = PeriodSettlingError;
|
|
1017
1074
|
exports.PermissionDeniedError = PermissionDeniedError;
|
|
1018
1075
|
exports.RateLimitError = RateLimitError;
|
|
1019
1076
|
exports.ReservationMismatchError = ReservationMismatchError;
|