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 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 every open grant — and is `null` under `no_cap`, where
52
- capacity is not a number. It is one figure and not a breakdown: `check` answers
53
- from a single cached integer, and itemising where the capacity came from would
54
- cost it that. For the per-grant detail, read a customer's allowances.
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, with no verdict and nothing to branch on:
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, and
433
- `available` is what `check` would answer. Meters are named by id, and like
434
- `retrieve` it is `null` for a customer holding no plan.
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 plan's
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
- | `InvalidRequestError` | 422 — a field failed validation |
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
- return new ConflictError(args);
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, not a verdict.
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;