mbase-sdk 0.0.2 → 0.0.5
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 +133 -32
- package/dist/index.cjs +209 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +219 -24
- package/dist/index.d.ts +219 -24
- package/dist/index.js +207 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ type Request = RequestOptions & {
|
|
|
29
29
|
};
|
|
30
30
|
declare class Client {
|
|
31
31
|
#private;
|
|
32
|
+
/** The engine's clock, as well as this client knows it. */
|
|
33
|
+
now(): number;
|
|
34
|
+
/** Off by up to one round trip, which a window in hours does not notice. */
|
|
35
|
+
observeServerTime(at: Date): void;
|
|
32
36
|
constructor(options: MeterbaseOptions);
|
|
33
37
|
request<T>(req: Request): Promise<T>;
|
|
34
38
|
}
|
|
@@ -164,9 +168,47 @@ type CheckParams = {
|
|
|
164
168
|
* asks whether there is any capacity at all. */
|
|
165
169
|
quantity?: number;
|
|
166
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* Which gate turned the request away. `entitlement` is named ahead of
|
|
173
|
+
* `rate_limit` when both refuse: running out of plan is the one the customer
|
|
174
|
+
* can act on, and being told about the window instead would send them to
|
|
175
|
+
* support over the wrong thing.
|
|
176
|
+
*
|
|
177
|
+
* Open on purpose. The engine's contract says this list will grow, and a
|
|
178
|
+
* closed union would turn every code the engine adds into a build the caller
|
|
179
|
+
* has to wait on the SDK for. Branch on the codes you know; anything else is
|
|
180
|
+
* still a refusal, and still means do not do the work.
|
|
181
|
+
*/
|
|
182
|
+
type CheckReason = "entitlement" | "rate_limit" | (string & {});
|
|
183
|
+
/**
|
|
184
|
+
* One rate-limit window in force for this customer and meter.
|
|
185
|
+
*
|
|
186
|
+
* Windows are fixed and clock-aligned — a cell starts at a multiple of the
|
|
187
|
+
* window counted from the epoch — which is what lets `used` be a literal count
|
|
188
|
+
* of what the current cell holds and `resets_at` a clock time. A sliding
|
|
189
|
+
* window would owe the reader a tooltip for both.
|
|
190
|
+
*/
|
|
191
|
+
type CheckRateLimit = {
|
|
192
|
+
/** A whole number of seconds from `60` to `2592000`: a minute to 30 days. */
|
|
193
|
+
window_seconds: number;
|
|
194
|
+
max: number;
|
|
195
|
+
used: number;
|
|
196
|
+
/** `max - used`, floored at 0. */
|
|
197
|
+
remaining: number;
|
|
198
|
+
/** `used + quantity > max`: this request does not fit in this window. */
|
|
199
|
+
exceeded: boolean;
|
|
200
|
+
/** End of the current cell, when `used` goes back to 0. Waiting for it is
|
|
201
|
+
* the fix for a window that refused — unless the quantity is larger than
|
|
202
|
+
* `max`, which never fits however long the caller waits. */
|
|
203
|
+
resets_at: string;
|
|
204
|
+
};
|
|
167
205
|
type CheckResult = {
|
|
206
|
+
/** Both gates: the capacity covers the quantity **and** every rate-limit
|
|
207
|
+
* window admits it. */
|
|
168
208
|
allowed: boolean;
|
|
169
|
-
/** Unlimited for this meter. `available` is `null`.
|
|
209
|
+
/** Unlimited for this meter. `available` is `null`. Rate limits still
|
|
210
|
+
* apply — a customer who is never refused on capacity is the one a runaway
|
|
211
|
+
* loop costs most. */
|
|
170
212
|
no_cap: boolean;
|
|
171
213
|
/**
|
|
172
214
|
* The whole capacity: the plan's unused entitlement for the current period,
|
|
@@ -178,6 +220,20 @@ type CheckResult = {
|
|
|
178
220
|
* per-grant detail, read `customers.allowances.list`.
|
|
179
221
|
*/
|
|
180
222
|
available: number | null;
|
|
223
|
+
/** Which gate said no. Absent when allowed, so its presence is the denial
|
|
224
|
+
* and reading it is never a branch on a truthy string. */
|
|
225
|
+
reason?: CheckReason;
|
|
226
|
+
/**
|
|
227
|
+
* Every window in force, refused or not, which is why the engine reports
|
|
228
|
+
* them on every call: a caller that only heard about a limit when it was
|
|
229
|
+
* refused could not slow down before it, only after.
|
|
230
|
+
*
|
|
231
|
+
* A limit is protective, not commercial, so it sits beside `available` and
|
|
232
|
+
* is never folded into it — a customer well inside their plan can still be
|
|
233
|
+
* limited, and the two are fixed by different people for different reasons.
|
|
234
|
+
* `[]` when no rule applies, which is the ordinary case.
|
|
235
|
+
*/
|
|
236
|
+
rate_limits: CheckRateLimit[];
|
|
181
237
|
};
|
|
182
238
|
/**
|
|
183
239
|
* `track` records work that already happened, so it never refuses on capacity
|
|
@@ -426,13 +482,58 @@ type WhoAmI = {
|
|
|
426
482
|
caller: string;
|
|
427
483
|
kind: "api_key" | "service";
|
|
428
484
|
};
|
|
485
|
+
/**
|
|
486
|
+
* A reserve is a check that means it: the same decision, and then a **hold**
|
|
487
|
+
* on the capacity until a track commits it, a release gives it back, or it
|
|
488
|
+
* expires. It is the gate for work that cannot be done twice.
|
|
489
|
+
*/
|
|
490
|
+
type ReserveParams = {
|
|
491
|
+
customer_id: string;
|
|
492
|
+
meter_id: string;
|
|
493
|
+
/** What the hold sets aside, `1 … 100000000000`. Defaults to `1`. */
|
|
494
|
+
quantity?: number;
|
|
495
|
+
/**
|
|
496
|
+
* The hold's id, and the event's id once a track commits it. A UUIDv7, and
|
|
497
|
+
* the SDK mints one when it is omitted.
|
|
498
|
+
*
|
|
499
|
+
* An id is for one reserve and its retries. Once its hold is committed or
|
|
500
|
+
* released the row is gone, so sending the same id again reserves afresh
|
|
501
|
+
* rather than finding anything.
|
|
502
|
+
*/
|
|
503
|
+
reservation_id?: string;
|
|
504
|
+
/**
|
|
505
|
+
* How long the hold counts against the pair before it comes back on its
|
|
506
|
+
* own: `1 … 900`, defaulting to `60`.
|
|
507
|
+
*
|
|
508
|
+
* It is a timeout on your own work, and the field callers get wrong. Too
|
|
509
|
+
* short and the hold evaporates mid-operation, which is worse than no hold
|
|
510
|
+
* at all because you believe you are protected; too long and a crash wedges
|
|
511
|
+
* the customer's capacity until it expires.
|
|
512
|
+
*/
|
|
513
|
+
expires_in_seconds?: number;
|
|
514
|
+
};
|
|
515
|
+
/** What the engine answers a reserve with, before the SDK adds its helpers. */
|
|
516
|
+
type ReserveResponse = CheckResult & {
|
|
517
|
+
/** Present only when allowed: a refusal holds nothing. */
|
|
518
|
+
reservation_id?: string;
|
|
519
|
+
quantity?: number;
|
|
520
|
+
expires_at?: string;
|
|
521
|
+
};
|
|
522
|
+
type ReleaseParams = {
|
|
523
|
+
reservation_id: string;
|
|
524
|
+
};
|
|
525
|
+
/** The hold that was given back, and what it had set aside. */
|
|
526
|
+
type ReleaseResult = {
|
|
527
|
+
reservation_id: string;
|
|
528
|
+
quantity: number;
|
|
529
|
+
};
|
|
429
530
|
|
|
430
531
|
/**
|
|
431
532
|
* Capacity handed to one customer on top of their plan. A grant with
|
|
432
533
|
* `source: "plan"` is the engine's own, minted when a plan is assigned, and
|
|
433
534
|
* cannot be created here.
|
|
434
535
|
*/
|
|
435
|
-
declare class CustomerAllowances {
|
|
536
|
+
declare class CustomerAllowances$1 {
|
|
436
537
|
#private;
|
|
437
538
|
constructor(client: Client);
|
|
438
539
|
grant(customerId: string, params: AllowanceGrantParams, options?: RequestOptions): Promise<Allowance>;
|
|
@@ -453,7 +554,7 @@ declare class CustomerAllowances {
|
|
|
453
554
|
* that picking one does not mean knowing what `effective` and `reconciliation`
|
|
454
555
|
* do to a half-used period.
|
|
455
556
|
*/
|
|
456
|
-
declare class CustomerPlan {
|
|
557
|
+
declare class CustomerPlan$1 {
|
|
457
558
|
#private;
|
|
458
559
|
constructor(client: Client);
|
|
459
560
|
/**
|
|
@@ -542,12 +643,12 @@ declare class CustomerPlan {
|
|
|
542
643
|
history(customerId: string, options?: RequestOptions): Promise<List<Assignment>>;
|
|
543
644
|
}
|
|
544
645
|
|
|
545
|
-
declare class Customers {
|
|
646
|
+
declare class Customers$1 {
|
|
546
647
|
#private;
|
|
547
648
|
/** The plan they hold, and the trail of instructions that got them there. */
|
|
548
|
-
readonly plan: CustomerPlan;
|
|
649
|
+
readonly plan: CustomerPlan$1;
|
|
549
650
|
/** Grants, which sit on top of whatever the plan gives. */
|
|
550
|
-
readonly allowances: CustomerAllowances;
|
|
651
|
+
readonly allowances: CustomerAllowances$1;
|
|
551
652
|
constructor(client: Client);
|
|
552
653
|
create(params: CustomerCreateParams, options?: RequestOptions): Promise<Customer>;
|
|
553
654
|
/** Lean: a listing does not embed the plan. Read one customer for that. */
|
|
@@ -569,7 +670,7 @@ declare class Customers {
|
|
|
569
670
|
delete(id: string, options?: RequestOptions): Promise<Customer>;
|
|
570
671
|
}
|
|
571
672
|
|
|
572
|
-
declare class Meters {
|
|
673
|
+
declare class Meters$1 {
|
|
573
674
|
#private;
|
|
574
675
|
constructor(client: Client);
|
|
575
676
|
create(params: MeterCreateParams, options?: RequestOptions): Promise<Meter>;
|
|
@@ -584,16 +685,16 @@ declare class Meters {
|
|
|
584
685
|
* A plan's entitlement, versioned. There is no update and no delete: an edit
|
|
585
686
|
* appends the next version, so which amount applied when stays derivable.
|
|
586
687
|
*/
|
|
587
|
-
declare class PlanAllowances {
|
|
688
|
+
declare class PlanAllowances$1 {
|
|
588
689
|
#private;
|
|
589
690
|
constructor(client: Client);
|
|
590
691
|
set(planId: string, params: PlanAllowanceSetParams, options?: RequestOptions): Promise<PlanAllowance>;
|
|
591
692
|
/** Every version of every meter, newest first — history, not just current. */
|
|
592
693
|
list(planId: string, params?: PlanAllowanceListParams, options?: RequestOptions): Promise<List<PlanAllowance>>;
|
|
593
694
|
}
|
|
594
|
-
declare class Plans {
|
|
695
|
+
declare class Plans$1 {
|
|
595
696
|
#private;
|
|
596
|
-
readonly allowances: PlanAllowances;
|
|
697
|
+
readonly allowances: PlanAllowances$1;
|
|
597
698
|
constructor(client: Client);
|
|
598
699
|
/** A new plan grants nothing; attach entitlement with `allowances.set`. */
|
|
599
700
|
create(params: PlanCreateParams, options?: RequestOptions): Promise<Plan>;
|
|
@@ -634,21 +735,37 @@ declare class NotFoundError extends APIError {
|
|
|
634
735
|
}
|
|
635
736
|
declare class ConflictError extends APIError {
|
|
636
737
|
}
|
|
738
|
+
declare class InvalidRequestError extends APIError {
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* `422 invalid_idempotency_key`: the key was not a UUID version 7.
|
|
742
|
+
* `crypto.randomUUID()` mints a v4, so omit `idempotency_key` and let this
|
|
743
|
+
* SDK mint the right thing.
|
|
744
|
+
*/
|
|
745
|
+
declare class InvalidIdempotencyKeyError extends InvalidRequestError {
|
|
746
|
+
}
|
|
637
747
|
/**
|
|
638
|
-
* `
|
|
639
|
-
*
|
|
640
|
-
*
|
|
748
|
+
* `422 too_late`: the key's timestamp is more than an hour from the engine's
|
|
749
|
+
* clock, so a retry can no longer be told from a new event. **Nothing was
|
|
750
|
+
* recorded** — the check runs before any write.
|
|
641
751
|
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
752
|
+
* The usual cause is this machine's clock. When this SDK minted the key it
|
|
753
|
+
* corrects the offset from `serverTime` and retries once, so you only see
|
|
754
|
+
* this for a key you supplied. If that key is a retry of a call made over an
|
|
755
|
+
* hour ago, do not resend it under a new one: it may already be recorded.
|
|
644
756
|
*/
|
|
645
|
-
declare class
|
|
646
|
-
/** The
|
|
647
|
-
|
|
648
|
-
readonly existingEventId: string | undefined;
|
|
757
|
+
declare class TooLateError extends InvalidRequestError {
|
|
758
|
+
/** The engine's clock when it refused. */
|
|
759
|
+
readonly serverTime: Date | undefined;
|
|
649
760
|
constructor(args: ConstructorParameters<typeof APIError>[0]);
|
|
650
761
|
}
|
|
651
|
-
|
|
762
|
+
/**
|
|
763
|
+
* `422 reservation_mismatch`: the reservation id names a hold made for a
|
|
764
|
+
* different customer or meter than the call sends. A caller bug, refused
|
|
765
|
+
* before anything is written — nothing was recorded and the hold still
|
|
766
|
+
* stands.
|
|
767
|
+
*/
|
|
768
|
+
declare class ReservationMismatchError extends InvalidRequestError {
|
|
652
769
|
}
|
|
653
770
|
/**
|
|
654
771
|
* `422 cycle_change_requires_reset`: the two plans measure different cycles,
|
|
@@ -684,6 +801,43 @@ declare function errorFromResponse(args: {
|
|
|
684
801
|
body?: unknown;
|
|
685
802
|
}): APIError;
|
|
686
803
|
|
|
804
|
+
/**
|
|
805
|
+
* A granted hold, with the two calls that close it. `commit` is `track` under
|
|
806
|
+
* the hold's own id; `release` gives the capacity back.
|
|
807
|
+
*/
|
|
808
|
+
type Hold = ReserveResponse & {
|
|
809
|
+
allowed: true;
|
|
810
|
+
reservation_id: string;
|
|
811
|
+
quantity: number;
|
|
812
|
+
expires_at: string;
|
|
813
|
+
/**
|
|
814
|
+
* Records what the work actually cost and releases the rest. Omit the
|
|
815
|
+
* quantity to record what was held.
|
|
816
|
+
*
|
|
817
|
+
* Warns when it runs after `expires_at`: the hold stopped holding anything
|
|
818
|
+
* at that instant, so the work was no longer protected. That warning is the
|
|
819
|
+
* only feedback there is on an `expires_in_seconds` guessed too short.
|
|
820
|
+
*/
|
|
821
|
+
commit(quantity?: number, options?: RequestOptions): Promise<TrackResult>;
|
|
822
|
+
/**
|
|
823
|
+
* Gives the hold back. Nothing is recorded.
|
|
824
|
+
*
|
|
825
|
+
* Resolves with `null` rather than throwing when there is nothing left to
|
|
826
|
+
* release — already committed, already released. This is the one call meant
|
|
827
|
+
* to run on the failure path, where throwing would mask the error the
|
|
828
|
+
* caller is already handling, and "nothing left to release" is the outcome
|
|
829
|
+
* a release wanted anyway. `meterbase.release` throws the 404, as every
|
|
830
|
+
* other call does.
|
|
831
|
+
*/
|
|
832
|
+
release(options?: RequestOptions): Promise<ReleaseResult | null>;
|
|
833
|
+
};
|
|
834
|
+
/** A refused reserve holds nothing, so it carries no id and no expiry. */
|
|
835
|
+
type ReserveRefused = ReserveResponse & {
|
|
836
|
+
allowed: false;
|
|
837
|
+
reservation_id?: undefined;
|
|
838
|
+
expires_at?: undefined;
|
|
839
|
+
};
|
|
840
|
+
type ReserveResult = Hold | ReserveRefused;
|
|
687
841
|
declare class Meterbase {
|
|
688
842
|
#private;
|
|
689
843
|
readonly customers: Customers;
|
|
@@ -697,6 +851,12 @@ declare class Meterbase {
|
|
|
697
851
|
*
|
|
698
852
|
* This is the gate, and the only call that ever refuses. Run it before the
|
|
699
853
|
* work; record the work with `track` afterwards.
|
|
854
|
+
*
|
|
855
|
+
* Two gates answer it, and `allowed` needs both: the customer's capacity has
|
|
856
|
+
* to cover the quantity, and every rate-limit window in force has to admit
|
|
857
|
+
* it. `reason` names the one that said no. `rate_limits` reports each window
|
|
858
|
+
* whether or not it refused, so a caller can ease off as its headroom closes
|
|
859
|
+
* instead of discovering the ceiling by hitting it.
|
|
700
860
|
*/
|
|
701
861
|
check(params: CheckParams, options?: RequestOptions): Promise<CheckResult>;
|
|
702
862
|
/**
|
|
@@ -705,13 +865,48 @@ declare class Meterbase {
|
|
|
705
865
|
* customer's capacity is recorded, drives `available` to 0, and the next
|
|
706
866
|
* `check` says no. The answer is a receipt, not a verdict.
|
|
707
867
|
*
|
|
708
|
-
* `idempotency_key` is generated when omitted, so retrying this
|
|
709
|
-
* SDK's own retries included — replays the original rather than
|
|
710
|
-
* the same work twice.
|
|
868
|
+
* `idempotency_key` is a UUIDv7, generated when omitted, so retrying this
|
|
869
|
+
* call — the SDK's own retries included — replays the original rather than
|
|
870
|
+
* counting the same work twice. The engine keeps that claim for an hour,
|
|
871
|
+
* which is how long a retry stays recognisable.
|
|
711
872
|
*/
|
|
712
873
|
track(params: TrackParams, options?: RequestOptions): Promise<TrackResult>;
|
|
874
|
+
/**
|
|
875
|
+
* Holds capacity for work that cannot be done twice, or refuses. The same
|
|
876
|
+
* decision `check` makes, and then a hold on the capacity until the work
|
|
877
|
+
* is committed, released, or expires.
|
|
878
|
+
*
|
|
879
|
+
* The rule for choosing: can you afford to do the work twice? `check`. No?
|
|
880
|
+
* `reserve`. A reserve costs a transaction where a check costs a cached
|
|
881
|
+
* read, refusals included, so it is not a per-request gate on cheap work.
|
|
882
|
+
*
|
|
883
|
+
* Two concurrent reserves for the same customer and meter cannot both be
|
|
884
|
+
* granted the same capacity; that is the whole point, and it is what
|
|
885
|
+
* `check` → work → `track` could never promise.
|
|
886
|
+
*/
|
|
887
|
+
reserve(params: ReserveParams, options?: RequestOptions): Promise<ReserveResult>;
|
|
888
|
+
/**
|
|
889
|
+
* Gives a hold back: the work did not happen, so nothing is recorded.
|
|
890
|
+
*
|
|
891
|
+
* Throws `NotFoundError` when the id names nothing — committed, released
|
|
892
|
+
* already, or never made, which are one absence with one meaning. Prefer
|
|
893
|
+
* `hold.release()`, which treats that as the success it is.
|
|
894
|
+
*/
|
|
895
|
+
release(params: ReleaseParams, options?: RequestOptions): Promise<ReleaseResult>;
|
|
713
896
|
/** Reports which workspace this key acts for. */
|
|
714
897
|
whoami(options?: RequestOptions): Promise<WhoAmI>;
|
|
715
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* The resource namespaces, type-only: none is constructible without the
|
|
901
|
+
* unexported Client. Instance types rather than `export type { Customers }`,
|
|
902
|
+
* which the declaration bundler re-emits as a value export — promising a
|
|
903
|
+
* runtime binding the bundle never has.
|
|
904
|
+
*/
|
|
905
|
+
type CustomerAllowances = InstanceType<typeof CustomerAllowances$1>;
|
|
906
|
+
type CustomerPlan = InstanceType<typeof CustomerPlan$1>;
|
|
907
|
+
type Customers = InstanceType<typeof Customers$1>;
|
|
908
|
+
type Meters = InstanceType<typeof Meters$1>;
|
|
909
|
+
type PlanAllowances = InstanceType<typeof PlanAllowances$1>;
|
|
910
|
+
type Plans = InstanceType<typeof Plans$1>;
|
|
716
911
|
|
|
717
|
-
export { APIError, type Allowance, type AllowanceGrantParams, type AllowanceListParams, type AllowanceSource, type ApplyMode, type AssignPlanParams, type Assignment, AuthenticationError, type CheckParams, type CheckResult, ConflictError, ConnectionError, type Customer, CustomerAllowances, type CustomerCreateParams, type CustomerListParams, CustomerPlan, type CustomerUpdateParams, type CustomerWithPlan, Customers, CycleChangeRequiresResetError, type EmbeddedPlan,
|
|
912
|
+
export { APIError, type Allowance, type AllowanceGrantParams, type AllowanceListParams, type AllowanceSource, type ApplyMode, type AssignPlanParams, type Assignment, AuthenticationError, type CheckParams, type CheckRateLimit, type CheckReason, type CheckResult, ConflictError, ConnectionError, type Customer, type CustomerAllowances, type CustomerCreateParams, type CustomerListParams, type CustomerPlan, type CustomerUpdateParams, type CustomerWithPlan, type Customers, CycleChangeRequiresResetError, type EmbeddedPlan, type Hold, InvalidIdempotencyKeyError, InvalidRequestError, type List, type Meter, type MeterCreateParams, type MeterListParams, type MeterUpdateParams, Meterbase, MeterbaseError, type MeterbaseOptions, type Meters, NotFoundError, PermissionDeniedError, type Plan, type PlanAllowance, type PlanAllowanceKind, type PlanAllowanceListParams, type PlanAllowanceSetParams, type PlanAllowances, type PlanChangeNowParams, type PlanChangeParams, type PlanCreateParams, type PlanCycle, type PlanListParams, type PlanMoveParams, type PlanUpdateParams, type Plans, RateLimitError, type Reconciliation, type RecordedReconciliation, type ReleaseParams, type ReleaseResult, type RequestOptions, ReservationMismatchError, type ReserveParams, type ReserveRefused, type ReserveResponse, type ReserveResult, ServerError, TimeoutError, TooLateError, type TrackParams, type TrackResult, type UsageEvent, type Validity, type WhoAmI, errorFromResponse };
|
package/dist/index.js
CHANGED
|
@@ -27,16 +27,19 @@ var NotFoundError = class extends APIError {
|
|
|
27
27
|
};
|
|
28
28
|
var ConflictError = class extends APIError {
|
|
29
29
|
};
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
var InvalidRequestError = class extends APIError {
|
|
31
|
+
};
|
|
32
|
+
var InvalidIdempotencyKeyError = class extends InvalidRequestError {
|
|
33
|
+
};
|
|
34
|
+
var TooLateError = class extends InvalidRequestError {
|
|
35
|
+
/** The engine's clock when it refused. */
|
|
36
|
+
serverTime;
|
|
34
37
|
constructor(args) {
|
|
35
38
|
super(args);
|
|
36
|
-
this.
|
|
39
|
+
this.serverTime = serverTime(args.body);
|
|
37
40
|
}
|
|
38
41
|
};
|
|
39
|
-
var
|
|
42
|
+
var ReservationMismatchError = class extends InvalidRequestError {
|
|
40
43
|
};
|
|
41
44
|
var CycleChangeRequiresResetError = class extends InvalidRequestError {
|
|
42
45
|
};
|
|
@@ -63,21 +66,34 @@ function errorFromResponse(args) {
|
|
|
63
66
|
case 404:
|
|
64
67
|
return new NotFoundError(args);
|
|
65
68
|
case 409:
|
|
66
|
-
return
|
|
69
|
+
return new ConflictError(args);
|
|
67
70
|
case 422:
|
|
68
|
-
|
|
71
|
+
switch (args.code) {
|
|
72
|
+
case "cycle_change_requires_reset":
|
|
73
|
+
return new CycleChangeRequiresResetError(args);
|
|
74
|
+
case "invalid_idempotency_key":
|
|
75
|
+
return new InvalidIdempotencyKeyError(args);
|
|
76
|
+
case "reservation_mismatch":
|
|
77
|
+
return new ReservationMismatchError(args);
|
|
78
|
+
case "too_late":
|
|
79
|
+
return new TooLateError(args);
|
|
80
|
+
default:
|
|
81
|
+
return new InvalidRequestError(args);
|
|
82
|
+
}
|
|
69
83
|
case 429:
|
|
70
84
|
return new RateLimitError(args);
|
|
71
85
|
default:
|
|
72
86
|
return args.status >= 500 ? new ServerError(args) : new APIError(args);
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
|
-
function
|
|
89
|
+
function serverTime(body) {
|
|
76
90
|
if (typeof body !== "object" || body === null) return void 0;
|
|
77
91
|
const error = body.error;
|
|
78
92
|
if (typeof error !== "object" || error === null) return void 0;
|
|
79
|
-
const
|
|
80
|
-
|
|
93
|
+
const at = error.server_time;
|
|
94
|
+
if (typeof at !== "string") return void 0;
|
|
95
|
+
const parsed = new Date(at);
|
|
96
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
// src/client.ts
|
|
@@ -91,6 +107,21 @@ var Client = class {
|
|
|
91
107
|
#timeout;
|
|
92
108
|
#maxRetries;
|
|
93
109
|
#fetch;
|
|
110
|
+
/**
|
|
111
|
+
* How far the engine's clock is ahead of this machine's, in milliseconds.
|
|
112
|
+
* Idempotency keys are minted here and the engine refuses one dated more
|
|
113
|
+
* than an hour from its own clock, so without this a device with a wrong
|
|
114
|
+
* clock would fail every call forever.
|
|
115
|
+
*/
|
|
116
|
+
#clockOffset = 0;
|
|
117
|
+
/** The engine's clock, as well as this client knows it. */
|
|
118
|
+
now() {
|
|
119
|
+
return Date.now() + this.#clockOffset;
|
|
120
|
+
}
|
|
121
|
+
/** Off by up to one round trip, which a window in hours does not notice. */
|
|
122
|
+
observeServerTime(at) {
|
|
123
|
+
this.#clockOffset = at.getTime() - Date.now();
|
|
124
|
+
}
|
|
94
125
|
constructor(options) {
|
|
95
126
|
if (!options.apiKey) {
|
|
96
127
|
throw new MeterbaseError(
|
|
@@ -635,6 +666,12 @@ var Meterbase = class {
|
|
|
635
666
|
*
|
|
636
667
|
* This is the gate, and the only call that ever refuses. Run it before the
|
|
637
668
|
* work; record the work with `track` afterwards.
|
|
669
|
+
*
|
|
670
|
+
* Two gates answer it, and `allowed` needs both: the customer's capacity has
|
|
671
|
+
* to cover the quantity, and every rate-limit window in force has to admit
|
|
672
|
+
* it. `reason` names the one that said no. `rate_limits` reports each window
|
|
673
|
+
* whether or not it refused, so a caller can ease off as its headroom closes
|
|
674
|
+
* instead of discovering the ceiling by hitting it.
|
|
638
675
|
*/
|
|
639
676
|
check(params, options) {
|
|
640
677
|
return this.#client.request({
|
|
@@ -650,24 +687,70 @@ var Meterbase = class {
|
|
|
650
687
|
* customer's capacity is recorded, drives `available` to 0, and the next
|
|
651
688
|
* `check` says no. The answer is a receipt, not a verdict.
|
|
652
689
|
*
|
|
653
|
-
* `idempotency_key` is generated when omitted, so retrying this
|
|
654
|
-
* SDK's own retries included — replays the original rather than
|
|
655
|
-
* the same work twice.
|
|
690
|
+
* `idempotency_key` is a UUIDv7, generated when omitted, so retrying this
|
|
691
|
+
* call — the SDK's own retries included — replays the original rather than
|
|
692
|
+
* counting the same work twice. The engine keeps that claim for an hour,
|
|
693
|
+
* which is how long a retry stays recognisable.
|
|
656
694
|
*/
|
|
657
695
|
// `async` so that generating the key cannot throw synchronously out of a
|
|
658
696
|
// method that otherwise only ever rejects: one call, one way to fail.
|
|
659
697
|
async track(params, options) {
|
|
698
|
+
return this.#keyed(
|
|
699
|
+
IDEMPOTENCY_WINDOW_MS,
|
|
700
|
+
params.idempotency_key,
|
|
701
|
+
(key) => this.#client.request({
|
|
702
|
+
...options,
|
|
703
|
+
method: "POST",
|
|
704
|
+
path: "/v1/usage/track",
|
|
705
|
+
// Fixed before the retry loop is entered: every attempt of this call
|
|
706
|
+
// carries the same key, which is what makes replaying it safe.
|
|
707
|
+
body: { ...params, idempotency_key: key },
|
|
708
|
+
idempotent: true
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Holds capacity for work that cannot be done twice, or refuses. The same
|
|
714
|
+
* decision `check` makes, and then a hold on the capacity until the work
|
|
715
|
+
* is committed, released, or expires.
|
|
716
|
+
*
|
|
717
|
+
* The rule for choosing: can you afford to do the work twice? `check`. No?
|
|
718
|
+
* `reserve`. A reserve costs a transaction where a check costs a cached
|
|
719
|
+
* read, refusals included, so it is not a per-request gate on cheap work.
|
|
720
|
+
*
|
|
721
|
+
* Two concurrent reserves for the same customer and meter cannot both be
|
|
722
|
+
* granted the same capacity; that is the whole point, and it is what
|
|
723
|
+
* `check` → work → `track` could never promise.
|
|
724
|
+
*/
|
|
725
|
+
async reserve(params, options) {
|
|
726
|
+
const response = await this.#keyed(
|
|
727
|
+
RESERVE_WINDOW_MS,
|
|
728
|
+
params.reservation_id,
|
|
729
|
+
(id) => this.#client.request({
|
|
730
|
+
...options,
|
|
731
|
+
method: "POST",
|
|
732
|
+
path: "/v1/usage/reserve",
|
|
733
|
+
body: { ...params, reservation_id: id },
|
|
734
|
+
// A retry under the same id finds the hold it already made rather
|
|
735
|
+
// than making a second one.
|
|
736
|
+
idempotent: true
|
|
737
|
+
})
|
|
738
|
+
);
|
|
739
|
+
return this.#hold(params, response);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Gives a hold back: the work did not happen, so nothing is recorded.
|
|
743
|
+
*
|
|
744
|
+
* Throws `NotFoundError` when the id names nothing — committed, released
|
|
745
|
+
* already, or never made, which are one absence with one meaning. Prefer
|
|
746
|
+
* `hold.release()`, which treats that as the success it is.
|
|
747
|
+
*/
|
|
748
|
+
release(params, options) {
|
|
660
749
|
return this.#client.request({
|
|
661
750
|
...options,
|
|
662
751
|
method: "POST",
|
|
663
|
-
path: "/v1/usage/
|
|
664
|
-
|
|
665
|
-
// this call carries the same key, which is what makes replaying it safe.
|
|
666
|
-
body: {
|
|
667
|
-
...params,
|
|
668
|
-
idempotency_key: params.idempotency_key ?? idempotencyKey()
|
|
669
|
-
},
|
|
670
|
-
idempotent: true
|
|
752
|
+
path: "/v1/usage/release",
|
|
753
|
+
body: params
|
|
671
754
|
});
|
|
672
755
|
}
|
|
673
756
|
/** Reports which workspace this key acts for. */
|
|
@@ -678,21 +761,112 @@ var Meterbase = class {
|
|
|
678
761
|
path: "/v1/whoami"
|
|
679
762
|
});
|
|
680
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Sends a call that carries a caller-minted UUIDv7, minting one when the
|
|
766
|
+
* caller supplied none and re-minting once if this machine's clock put it
|
|
767
|
+
* outside the engine's window.
|
|
768
|
+
*
|
|
769
|
+
* A key the caller supplied is never re-minted: this cannot know whether it
|
|
770
|
+
* names work already recorded, and replacing it could count that work
|
|
771
|
+
* twice.
|
|
772
|
+
*/
|
|
773
|
+
async #keyed(window, supplied, send) {
|
|
774
|
+
if (supplied !== void 0) return send(supplied);
|
|
775
|
+
const key = idempotencyKey(this.#client.now());
|
|
776
|
+
const startedAt = Date.now();
|
|
777
|
+
try {
|
|
778
|
+
return await send(key);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
if (!(error instanceof TooLateError) || error.serverTime === void 0) {
|
|
781
|
+
throw error;
|
|
782
|
+
}
|
|
783
|
+
const elapsed = Date.now() - startedAt;
|
|
784
|
+
const serverAtStart = error.serverTime.getTime() - elapsed;
|
|
785
|
+
if (Math.abs(serverAtStart - mintedAtMs(key)) <= window) throw error;
|
|
786
|
+
this.#client.observeServerTime(error.serverTime);
|
|
787
|
+
return await send(idempotencyKey(this.#client.now()));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/** Puts `commit` and `release` on a granted hold; a refusal is left as it is. */
|
|
791
|
+
#hold(params, response) {
|
|
792
|
+
if (!response.allowed) return response;
|
|
793
|
+
const id = response.reservation_id;
|
|
794
|
+
const expiresAt = response.expires_at;
|
|
795
|
+
if (id === void 0 || expiresAt === void 0) {
|
|
796
|
+
throw new MeterbaseError(
|
|
797
|
+
"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."
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
801
|
+
return {
|
|
802
|
+
...response,
|
|
803
|
+
allowed: true,
|
|
804
|
+
reservation_id: id,
|
|
805
|
+
quantity: response.quantity ?? params.quantity ?? 1,
|
|
806
|
+
expires_at: expiresAt,
|
|
807
|
+
commit: (quantity, commitOptions) => {
|
|
808
|
+
if (Number.isFinite(expiresAtMs) && this.#client.now() > expiresAtMs && typeof globalThis.console?.warn === "function") {
|
|
809
|
+
globalThis.console.warn(
|
|
810
|
+
`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.`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
return this.track(
|
|
814
|
+
{
|
|
815
|
+
customer_id: params.customer_id,
|
|
816
|
+
meter_id: params.meter_id,
|
|
817
|
+
quantity,
|
|
818
|
+
// The hold's own id, so the track commits it rather than
|
|
819
|
+
// recording a second event beside it.
|
|
820
|
+
idempotency_key: id
|
|
821
|
+
},
|
|
822
|
+
commitOptions
|
|
823
|
+
);
|
|
824
|
+
},
|
|
825
|
+
release: async (releaseOptions) => {
|
|
826
|
+
try {
|
|
827
|
+
return await this.release({ reservation_id: id }, releaseOptions);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (error instanceof NotFoundError) return null;
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
681
835
|
};
|
|
682
|
-
|
|
836
|
+
var IDEMPOTENCY_WINDOW_MS = 60 * 60 * 1e3;
|
|
837
|
+
var RESERVE_WINDOW_MS = 30 * 60 * 1e3;
|
|
838
|
+
function mintedAtMs(key) {
|
|
839
|
+
return Number.parseInt(key.replace(/-/g, "").slice(0, 12), 16);
|
|
840
|
+
}
|
|
841
|
+
function idempotencyKey(atMs) {
|
|
683
842
|
const webcrypto = globalThis.crypto;
|
|
684
|
-
if (typeof webcrypto?.
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const bytes = webcrypto.getRandomValues(new Uint8Array(16));
|
|
689
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
843
|
+
if (typeof webcrypto?.getRandomValues !== "function") {
|
|
844
|
+
throw new MeterbaseError(
|
|
845
|
+
"No crypto to generate an idempotency key with: pass `idempotency_key` yourself (a UUIDv7), or run somewhere `crypto.getRandomValues` exists."
|
|
846
|
+
);
|
|
690
847
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
);
|
|
848
|
+
const bytes = webcrypto.getRandomValues(new Uint8Array(16));
|
|
849
|
+
const ms = Math.max(0, Math.trunc(atMs));
|
|
850
|
+
const high = Math.floor(ms / 4294967296);
|
|
851
|
+
const low = ms >>> 0;
|
|
852
|
+
bytes[0] = high >>> 8 & 255;
|
|
853
|
+
bytes[1] = high & 255;
|
|
854
|
+
bytes[2] = low >>> 24 & 255;
|
|
855
|
+
bytes[3] = low >>> 16 & 255;
|
|
856
|
+
bytes[4] = low >>> 8 & 255;
|
|
857
|
+
bytes[5] = low & 255;
|
|
858
|
+
bytes[6] = bytes[6] & 15 | 112;
|
|
859
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
860
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
861
|
+
return [
|
|
862
|
+
hex.slice(0, 8),
|
|
863
|
+
hex.slice(8, 12),
|
|
864
|
+
hex.slice(12, 16),
|
|
865
|
+
hex.slice(16, 20),
|
|
866
|
+
hex.slice(20)
|
|
867
|
+
].join("-");
|
|
694
868
|
}
|
|
695
869
|
|
|
696
|
-
export { APIError, AuthenticationError, ConflictError, ConnectionError, CycleChangeRequiresResetError,
|
|
870
|
+
export { APIError, AuthenticationError, ConflictError, ConnectionError, CycleChangeRequiresResetError, InvalidIdempotencyKeyError, InvalidRequestError, Meterbase, MeterbaseError, NotFoundError, PermissionDeniedError, RateLimitError, ReservationMismatchError, ServerError, TimeoutError, TooLateError, errorFromResponse };
|
|
697
871
|
//# sourceMappingURL=index.js.map
|
|
698
872
|
//# sourceMappingURL=index.js.map
|