mbase-sdk 0.0.4 → 0.0.6
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 +124 -25
- package/dist/index.cjs +165 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +187 -1
- package/dist/index.d.ts +187 -1
- package/dist/index.js +165 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mbase-sdk
|
|
2
2
|
|
|
3
3
|
TypeScript SDK for the [Meterbase](https://github.com/usemeterbase/engine)
|
|
4
|
-
engine. Works on Node
|
|
4
|
+
engine. Works on Node 20+, browsers, and edge runtimes — it uses `fetch` and
|
|
5
5
|
nothing else.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install
|
|
10
|
+
npm install mbase-sdk
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
Two calls, in this order: **`check` → do the work → `track
|
|
15
|
+
Two calls, in this order: **`check` → do the work → `track`** — or, for work
|
|
16
|
+
that must not be done twice, [`reserve`](#work-you-cannot-do-twice).
|
|
16
17
|
|
|
17
18
|
```ts
|
|
18
|
-
import { Meterbase } from "
|
|
19
|
+
import { Meterbase } from "mbase-sdk"
|
|
19
20
|
|
|
20
21
|
const meterbase = new Meterbase({ apiKey: process.env.METERBASE_API_KEY! })
|
|
21
22
|
|
|
@@ -96,12 +97,12 @@ it is within **one hour** of the engine's clock, and keeps the claim behind it
|
|
|
96
97
|
until exactly that hour is up. A retry is therefore recognised for as long as
|
|
97
98
|
it is accepted at all.
|
|
98
99
|
|
|
99
|
-
| The call sends | The engine answers
|
|
100
|
-
| ------------------------------------- |
|
|
101
|
-
| a fresh key | the recorded event, `replayed: false`
|
|
102
|
-
| a key it has already seen | that event with `replayed: true`, writing nothing
|
|
103
|
-
| a key more than an hour old, or ahead | `422 too_late`, writing nothing
|
|
104
|
-
| anything that is not a UUIDv7 | `422 invalid_idempotency_key`, writing nothing
|
|
100
|
+
| The call sends | The engine answers |
|
|
101
|
+
| ------------------------------------- | ------------------------------------------------- |
|
|
102
|
+
| a fresh key | the recorded event, `replayed: false` |
|
|
103
|
+
| a key it has already seen | that event with `replayed: true`, writing nothing |
|
|
104
|
+
| a key more than an hour old, or ahead | `422 too_late`, writing nothing |
|
|
105
|
+
| anything that is not a UUIDv7 | `422 invalid_idempotency_key`, writing nothing |
|
|
105
106
|
|
|
106
107
|
A key it has already seen replays **whatever the rest of the body says**:
|
|
107
108
|
there is one claim behind the key and nothing to compare a retry against, so
|
|
@@ -128,6 +129,68 @@ request id — so that a retry from further out than this SDK replays too. Keys
|
|
|
128
129
|
are retained 35 days; a retry after that records a second event, which is not a
|
|
129
130
|
case this API sets out to serve.
|
|
130
131
|
|
|
132
|
+
### Work you cannot do twice
|
|
133
|
+
|
|
134
|
+
`check` → work → `track` has a hole: two concurrent callers both pass the gate,
|
|
135
|
+
both do the work, and both record it. For an API call that costs nothing that
|
|
136
|
+
is an accepted trade. For an LLM completion, an image, a transcode, it is the
|
|
137
|
+
whole problem — the customer is over their cap and the money is already spent.
|
|
138
|
+
|
|
139
|
+
`reserve` closes it. It makes the same decision `check` makes and then **holds**
|
|
140
|
+
the capacity, so two reserves for the same last units cannot both be granted:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const hold = await meterbase.reserve({
|
|
144
|
+
customer_id: "acct_1",
|
|
145
|
+
meter_id: "ai_tokens",
|
|
146
|
+
quantity: 50_000, // what you are about to spend; defaults to 1
|
|
147
|
+
expires_in_seconds: 60, // how long to hold it; defaults to 60, at most 900
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (!hold.allowed) throw new Error(`Refused: ${hold.reason}`)
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const completion = await generate(prompt)
|
|
154
|
+
await hold.commit(completion.usage.total_tokens) // records what it cost
|
|
155
|
+
} catch (error) {
|
|
156
|
+
await hold.release() // nothing happened, so nothing is recorded
|
|
157
|
+
throw error
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**The rule for choosing: can you afford to do the work twice? `check`. No?
|
|
162
|
+
`reserve`.** A reserve costs a database transaction where a check costs a cached
|
|
163
|
+
read — refusals included — so it is not the call to put in front of cheap work.
|
|
164
|
+
|
|
165
|
+
A reserve answers everything `check` answers, plus the hold: `reservation_id`,
|
|
166
|
+
the `quantity` it set aside, and `expires_at`. `available` is already net of it.
|
|
167
|
+
A refusal holds nothing, so it carries no id and no expiry, and `allowed`
|
|
168
|
+
narrows the type — after `if (!hold.allowed)`, `commit` and `release` are there.
|
|
169
|
+
|
|
170
|
+
`commit` is `track` under the hold's own id, which is what makes it a commit
|
|
171
|
+
rather than a second event beside it. Pass what the work actually cost and the
|
|
172
|
+
difference comes back; omit it to record what was held. Going over is allowed —
|
|
173
|
+
the hold only ever guaranteed what it reserved.
|
|
174
|
+
|
|
175
|
+
`release` gives the capacity back and records nothing. It is built for the
|
|
176
|
+
`catch` block, so it resolves with `null` instead of throwing when there is
|
|
177
|
+
nothing left to release — already committed, already released — because
|
|
178
|
+
throwing there would mask the error you are already handling.
|
|
179
|
+
`meterbase.release({ reservation_id })` throws that 404, as every other call
|
|
180
|
+
does.
|
|
181
|
+
|
|
182
|
+
**Nothing is lost if your process dies.** A hold expires on its own, and the
|
|
183
|
+
capacity comes back with nothing having run. A commit that arrives after
|
|
184
|
+
`expires_at` is still recorded — you lose the guarantee, not the event — and
|
|
185
|
+
the SDK warns when that happens, which is the only feedback there is on an
|
|
186
|
+
`expires_in_seconds` guessed too short.
|
|
187
|
+
|
|
188
|
+
`reservation_id` is a UUIDv7 on the same terms as `idempotency_key`, generated
|
|
189
|
+
when you omit it, and it becomes the event's id once the hold is committed. One
|
|
190
|
+
id names the hold, the claim and the event. An id is for one reserve and its
|
|
191
|
+
retries: once its hold is committed or released, sending it again reserves
|
|
192
|
+
afresh rather than finding anything.
|
|
193
|
+
|
|
131
194
|
### Customers
|
|
132
195
|
|
|
133
196
|
```ts
|
|
@@ -311,7 +374,7 @@ weight, so `changeAtNextCycle` and `changeNow` + `"prorate"` both answer
|
|
|
311
374
|
`changeNow` is the other way out — it keeps the period that is running.
|
|
312
375
|
|
|
313
376
|
```ts
|
|
314
|
-
import { CycleChangeRequiresResetError } from "
|
|
377
|
+
import { CycleChangeRequiresResetError } from "mbase-sdk"
|
|
315
378
|
|
|
316
379
|
try {
|
|
317
380
|
await meterbase.customers.plan.changeAtNextCycle(customer.id, {
|
|
@@ -354,6 +417,42 @@ instruction, and writes no new row of its own — so this reads the plan in forc
|
|
|
354
417
|
and names it back. It returns the assignment still in force, or `null` for a
|
|
355
418
|
customer holding no plan, who can have nothing pending.
|
|
356
419
|
|
|
420
|
+
#### How much they have used
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
const entitlement = await meterbase.customers.entitlement(customer.id) // or null
|
|
424
|
+
|
|
425
|
+
for (const meter of entitlement?.meters ?? []) {
|
|
426
|
+
console.log(meter.meter_id, `${meter.used} of ${meter.amount ?? "unlimited"}`)
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`entitlement` is the plan's side of the current period, one entry per meter the
|
|
431
|
+
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.
|
|
435
|
+
|
|
436
|
+
Unlike `check`, it is not answered from a cache, so read it where usage is
|
|
437
|
+
shown rather than on every request.
|
|
438
|
+
|
|
439
|
+
#### Usage by day
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
const week = await meterbase.customers.usage(customer.id, { days: 7 })
|
|
443
|
+
const period = await meterbase.customers.usage(customer.id, {
|
|
444
|
+
period: "current",
|
|
445
|
+
}) // or null
|
|
446
|
+
const meter = await meterbase.meters.usage(meterId, { days: 30 })
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Usage per UTC day: `days` is 1–90 days ending today, and a customer can also
|
|
450
|
+
be read by `period`, `current` or `last`. Each meter lists every day that has
|
|
451
|
+
begun, zeros included; a customer's meters with no usage in the range are left
|
|
452
|
+
out. A period the customer never had is `null`.
|
|
453
|
+
|
|
454
|
+
Figures can be up to five minutes old. `check` and `entitlement` are live.
|
|
455
|
+
|
|
357
456
|
### A customer's allowances
|
|
358
457
|
|
|
359
458
|
Capacity on top of whatever the plan gives.
|
|
@@ -382,7 +481,7 @@ Every failure is a `MeterbaseError`. Catch the specific one you care about, or
|
|
|
382
481
|
the base class for all of them.
|
|
383
482
|
|
|
384
483
|
```ts
|
|
385
|
-
import { ConflictError, RateLimitError } from "
|
|
484
|
+
import { ConflictError, RateLimitError } from "mbase-sdk"
|
|
386
485
|
|
|
387
486
|
try {
|
|
388
487
|
await meterbase.customers.create({ external_id: "acct_1" })
|
|
@@ -397,19 +496,19 @@ try {
|
|
|
397
496
|
}
|
|
398
497
|
```
|
|
399
498
|
|
|
400
|
-
| Class | When
|
|
401
|
-
| ---------------------------------- |
|
|
402
|
-
| `AuthenticationError` | 401 — unknown, revoked or malformed key
|
|
403
|
-
| `PermissionDeniedError` | 403 — the key may not use this route
|
|
404
|
-
| `NotFoundError` | 404
|
|
405
|
-
| `ConflictError` | 409 — a key or external id is taken
|
|
406
|
-
| `InvalidRequestError` | 422 — a field failed validation
|
|
407
|
-
| `CycleChangeRequiresResetError` | 422 — an `InvalidRequestError`: the two plans' cycles differ, so use `restart`
|
|
408
|
-
| `InvalidIdempotencyKeyError` | 422 — an `InvalidRequestError`: the key was not a UUIDv7
|
|
499
|
+
| Class | When |
|
|
500
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
501
|
+
| `AuthenticationError` | 401 — unknown, revoked or malformed key |
|
|
502
|
+
| `PermissionDeniedError` | 403 — the key may not use this route |
|
|
503
|
+
| `NotFoundError` | 404 |
|
|
504
|
+
| `ConflictError` | 409 — a key or external id is taken |
|
|
505
|
+
| `InvalidRequestError` | 422 — a field failed validation |
|
|
506
|
+
| `CycleChangeRequiresResetError` | 422 — an `InvalidRequestError`: the two plans' cycles differ, so use `restart` |
|
|
507
|
+
| `InvalidIdempotencyKeyError` | 422 — an `InvalidRequestError`: the key was not a UUIDv7 |
|
|
409
508
|
| `TooLateError` | 422 — an `InvalidRequestError`: the key is over an hour from the engine's clock, which it carries on `serverTime`. Nothing was recorded |
|
|
410
|
-
| `RateLimitError` | 429
|
|
411
|
-
| `ServerError` | 5xx
|
|
412
|
-
| `ConnectionError` / `TimeoutError` | the request never got an answer
|
|
509
|
+
| `RateLimitError` | 429 |
|
|
510
|
+
| `ServerError` | 5xx |
|
|
511
|
+
| `ConnectionError` / `TimeoutError` | the request never got an answer |
|
|
413
512
|
|
|
414
513
|
`APIError` carries `status`, `code` and the parsed `body`. Branch on `code`,
|
|
415
514
|
which is stable; `message` is for humans and may change.
|
package/dist/index.cjs
CHANGED
|
@@ -41,6 +41,8 @@ var TooLateError = class extends InvalidRequestError {
|
|
|
41
41
|
this.serverTime = serverTime(args.body);
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
+
var ReservationMismatchError = class extends InvalidRequestError {
|
|
45
|
+
};
|
|
44
46
|
var CycleChangeRequiresResetError = class extends InvalidRequestError {
|
|
45
47
|
};
|
|
46
48
|
var RateLimitError = class extends APIError {
|
|
@@ -73,6 +75,8 @@ function errorFromResponse(args) {
|
|
|
73
75
|
return new CycleChangeRequiresResetError(args);
|
|
74
76
|
case "invalid_idempotency_key":
|
|
75
77
|
return new InvalidIdempotencyKeyError(args);
|
|
78
|
+
case "reservation_mismatch":
|
|
79
|
+
return new ReservationMismatchError(args);
|
|
76
80
|
case "too_late":
|
|
77
81
|
return new TooLateError(args);
|
|
78
82
|
default:
|
|
@@ -133,7 +137,7 @@ var Client = class {
|
|
|
133
137
|
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
134
138
|
if (typeof this.#fetch !== "function") {
|
|
135
139
|
throw new MeterbaseError(
|
|
136
|
-
"No fetch implementation: pass one as `fetch`, or run on Node
|
|
140
|
+
"No fetch implementation: pass one as `fetch`, or run on Node 20+."
|
|
137
141
|
);
|
|
138
142
|
}
|
|
139
143
|
}
|
|
@@ -504,6 +508,36 @@ var Customers = class {
|
|
|
504
508
|
});
|
|
505
509
|
return data[0] ?? null;
|
|
506
510
|
}
|
|
511
|
+
/** `null` for a customer holding no plan, as `plan.retrieve` answers. */
|
|
512
|
+
async entitlement(id, options) {
|
|
513
|
+
try {
|
|
514
|
+
return await this.#client.request({
|
|
515
|
+
...options,
|
|
516
|
+
method: "GET",
|
|
517
|
+
path: `/v1/customers/${encodeURIComponent(id)}/entitlement`
|
|
518
|
+
});
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (error instanceof NotFoundError && error.code === "no_plan_assigned") {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async usage(id, params, options) {
|
|
527
|
+
try {
|
|
528
|
+
return await this.#client.request({
|
|
529
|
+
...options,
|
|
530
|
+
method: "GET",
|
|
531
|
+
path: `/v1/customers/${encodeURIComponent(id)}/usage`,
|
|
532
|
+
query: { days: params.days, period: params.period }
|
|
533
|
+
});
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (error instanceof NotFoundError && (error.code === "no_plan_assigned" || error.code === "no_previous_period")) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
507
541
|
update(id, params, options) {
|
|
508
542
|
return this.#client.request({
|
|
509
543
|
...options,
|
|
@@ -559,6 +593,15 @@ var Meters = class {
|
|
|
559
593
|
body: params
|
|
560
594
|
});
|
|
561
595
|
}
|
|
596
|
+
/** Every customer's usage of the meter per UTC day. */
|
|
597
|
+
usage(id, params, options) {
|
|
598
|
+
return this.#client.request({
|
|
599
|
+
...options,
|
|
600
|
+
method: "GET",
|
|
601
|
+
path: `/v1/meters/${encodeURIComponent(id)}/usage`,
|
|
602
|
+
query: { days: params.days }
|
|
603
|
+
});
|
|
604
|
+
}
|
|
562
605
|
/** Soft delete: plans and usage keep referencing the meter. Idempotent. */
|
|
563
606
|
archive(id, options) {
|
|
564
607
|
return this.#client.request({
|
|
@@ -693,18 +736,83 @@ var Meterbase = class {
|
|
|
693
736
|
// `async` so that generating the key cannot throw synchronously out of a
|
|
694
737
|
// method that otherwise only ever rejects: one call, one way to fail.
|
|
695
738
|
async track(params, options) {
|
|
696
|
-
|
|
739
|
+
return this.#keyed(
|
|
740
|
+
IDEMPOTENCY_WINDOW_MS,
|
|
741
|
+
params.idempotency_key,
|
|
742
|
+
(key) => this.#client.request({
|
|
743
|
+
...options,
|
|
744
|
+
method: "POST",
|
|
745
|
+
path: "/v1/usage/track",
|
|
746
|
+
// Fixed before the retry loop is entered: every attempt of this call
|
|
747
|
+
// carries the same key, which is what makes replaying it safe.
|
|
748
|
+
body: { ...params, idempotency_key: key },
|
|
749
|
+
idempotent: true
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Holds capacity for work that cannot be done twice, or refuses. The same
|
|
755
|
+
* decision `check` makes, and then a hold on the capacity until the work
|
|
756
|
+
* is committed, released, or expires.
|
|
757
|
+
*
|
|
758
|
+
* The rule for choosing: can you afford to do the work twice? `check`. No?
|
|
759
|
+
* `reserve`. A reserve costs a transaction where a check costs a cached
|
|
760
|
+
* read, refusals included, so it is not a per-request gate on cheap work.
|
|
761
|
+
*
|
|
762
|
+
* Two concurrent reserves for the same customer and meter cannot both be
|
|
763
|
+
* granted the same capacity; that is the whole point, and it is what
|
|
764
|
+
* `check` → work → `track` could never promise.
|
|
765
|
+
*/
|
|
766
|
+
async reserve(params, options) {
|
|
767
|
+
const response = await this.#keyed(
|
|
768
|
+
RESERVE_WINDOW_MS,
|
|
769
|
+
params.reservation_id,
|
|
770
|
+
(id) => this.#client.request({
|
|
771
|
+
...options,
|
|
772
|
+
method: "POST",
|
|
773
|
+
path: "/v1/usage/reserve",
|
|
774
|
+
body: { ...params, reservation_id: id },
|
|
775
|
+
// A retry under the same id finds the hold it already made rather
|
|
776
|
+
// than making a second one.
|
|
777
|
+
idempotent: true
|
|
778
|
+
})
|
|
779
|
+
);
|
|
780
|
+
return this.#hold(params, response);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Gives a hold back: the work did not happen, so nothing is recorded.
|
|
784
|
+
*
|
|
785
|
+
* Throws `NotFoundError` when the id names nothing — committed, released
|
|
786
|
+
* already, or never made, which are one absence with one meaning. Prefer
|
|
787
|
+
* `hold.release()`, which treats that as the success it is.
|
|
788
|
+
*/
|
|
789
|
+
release(params, options) {
|
|
790
|
+
return this.#client.request({
|
|
697
791
|
...options,
|
|
698
792
|
method: "POST",
|
|
699
|
-
path: "/v1/usage/
|
|
700
|
-
|
|
701
|
-
// carries the same key, which is what makes replaying it safe.
|
|
702
|
-
body: { ...params, idempotency_key: key2 },
|
|
703
|
-
idempotent: true
|
|
793
|
+
path: "/v1/usage/release",
|
|
794
|
+
body: params
|
|
704
795
|
});
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
796
|
+
}
|
|
797
|
+
/** Reports which workspace this key acts for. */
|
|
798
|
+
whoami(options) {
|
|
799
|
+
return this.#client.request({
|
|
800
|
+
...options,
|
|
801
|
+
method: "GET",
|
|
802
|
+
path: "/v1/whoami"
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Sends a call that carries a caller-minted UUIDv7, minting one when the
|
|
807
|
+
* caller supplied none and re-minting once if this machine's clock put it
|
|
808
|
+
* outside the engine's window.
|
|
809
|
+
*
|
|
810
|
+
* A key the caller supplied is never re-minted: this cannot know whether it
|
|
811
|
+
* names work already recorded, and replacing it could count that work
|
|
812
|
+
* twice.
|
|
813
|
+
*/
|
|
814
|
+
async #keyed(window, supplied, send) {
|
|
815
|
+
if (supplied !== void 0) return send(supplied);
|
|
708
816
|
const key = idempotencyKey(this.#client.now());
|
|
709
817
|
const startedAt = Date.now();
|
|
710
818
|
try {
|
|
@@ -715,23 +823,59 @@ var Meterbase = class {
|
|
|
715
823
|
}
|
|
716
824
|
const elapsed = Date.now() - startedAt;
|
|
717
825
|
const serverAtStart = error.serverTime.getTime() - elapsed;
|
|
718
|
-
if (Math.abs(serverAtStart - mintedAtMs(key)) <=
|
|
719
|
-
throw error;
|
|
720
|
-
}
|
|
826
|
+
if (Math.abs(serverAtStart - mintedAtMs(key)) <= window) throw error;
|
|
721
827
|
this.#client.observeServerTime(error.serverTime);
|
|
722
828
|
return await send(idempotencyKey(this.#client.now()));
|
|
723
829
|
}
|
|
724
830
|
}
|
|
725
|
-
/**
|
|
726
|
-
|
|
727
|
-
return
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
831
|
+
/** Puts `commit` and `release` on a granted hold; a refusal is left as it is. */
|
|
832
|
+
#hold(params, response) {
|
|
833
|
+
if (!response.allowed) return response;
|
|
834
|
+
const id = response.reservation_id;
|
|
835
|
+
const expiresAt = response.expires_at;
|
|
836
|
+
if (id === void 0 || expiresAt === void 0) {
|
|
837
|
+
throw new MeterbaseError(
|
|
838
|
+
"The engine granted a reservation without an id or an expiry, so there is no hold to commit or release. The capacity it set aside comes back on its own."
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
842
|
+
return {
|
|
843
|
+
...response,
|
|
844
|
+
allowed: true,
|
|
845
|
+
reservation_id: id,
|
|
846
|
+
quantity: response.quantity ?? params.quantity ?? 1,
|
|
847
|
+
expires_at: expiresAt,
|
|
848
|
+
commit: (quantity, commitOptions) => {
|
|
849
|
+
if (Number.isFinite(expiresAtMs) && this.#client.now() > expiresAtMs && typeof globalThis.console?.warn === "function") {
|
|
850
|
+
globalThis.console.warn(
|
|
851
|
+
`Meterbase: committing reservation ${id} after it expired at ${expiresAt}. The event is still recorded, but the capacity was no longer held \u2014 raise expires_in_seconds for this work.`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return this.track(
|
|
855
|
+
{
|
|
856
|
+
customer_id: params.customer_id,
|
|
857
|
+
meter_id: params.meter_id,
|
|
858
|
+
quantity,
|
|
859
|
+
// The hold's own id, so the track commits it rather than
|
|
860
|
+
// recording a second event beside it.
|
|
861
|
+
idempotency_key: id
|
|
862
|
+
},
|
|
863
|
+
commitOptions
|
|
864
|
+
);
|
|
865
|
+
},
|
|
866
|
+
release: async (releaseOptions) => {
|
|
867
|
+
try {
|
|
868
|
+
return await this.release({ reservation_id: id }, releaseOptions);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
if (error instanceof NotFoundError) return null;
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
732
875
|
}
|
|
733
876
|
};
|
|
734
877
|
var IDEMPOTENCY_WINDOW_MS = 60 * 60 * 1e3;
|
|
878
|
+
var RESERVE_WINDOW_MS = 30 * 60 * 1e3;
|
|
735
879
|
function mintedAtMs(key) {
|
|
736
880
|
return Number.parseInt(key.replace(/-/g, "").slice(0, 12), 16);
|
|
737
881
|
}
|
|
@@ -776,6 +920,7 @@ exports.MeterbaseError = MeterbaseError;
|
|
|
776
920
|
exports.NotFoundError = NotFoundError;
|
|
777
921
|
exports.PermissionDeniedError = PermissionDeniedError;
|
|
778
922
|
exports.RateLimitError = RateLimitError;
|
|
923
|
+
exports.ReservationMismatchError = ReservationMismatchError;
|
|
779
924
|
exports.ServerError = ServerError;
|
|
780
925
|
exports.TimeoutError = TimeoutError;
|
|
781
926
|
exports.TooLateError = TooLateError;
|