@usagekit/meter 0.3.0 → 0.4.0

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.
@@ -0,0 +1,15 @@
1
+ import type { ConnectionPolicy, MeterReserveInput, Operation, PricingCatalog, ReserveInput, ValidationFailure } from "@usagekit/core";
2
+ /** Host lookup of a connection's provider, plan, manual prices and tracking policy. */
3
+ export type ConnectionResolver = (connectionId: string) => Promise<ConnectionPolicy | undefined> | ConnectionPolicy | undefined;
4
+ /** What reserve does with a request after the catalog and the connection policy spoke. */
5
+ export type ReserveResolution = {
6
+ kind: "reserve";
7
+ input: ReserveInput;
8
+ } | {
9
+ kind: "count";
10
+ state: "passthrough" | "unpriced";
11
+ } | {
12
+ kind: "invalid";
13
+ failure: ValidationFailure;
14
+ };
15
+ export declare function resolveReserve(requested: MeterReserveInput, existing: Operation | null, catalog?: PricingCatalog, resolveConnection?: ConnectionResolver): Promise<ReserveResolution>;
@@ -0,0 +1,62 @@
1
+ const invalid = (field, reason) => ({
2
+ kind: "invalid",
3
+ failure: { outcome: "invalid", field, reason },
4
+ });
5
+ /**
6
+ * Order: a stored operation replays with its own estimate; then the connection's tracking
7
+ * policy and free catalog operations count as passthrough; then a given estimate is used as is;
8
+ * then the catalog resolves one (manual, measured, list, unknown), and an operation the catalog
9
+ * does not know counts as unpriced.
10
+ */
11
+ const provenanceOf = (estimateSource, providerPriceVersion) => ({
12
+ ...(estimateSource ? { estimateSource } : {}),
13
+ ...(providerPriceVersion ? { providerPriceVersion } : {}),
14
+ });
15
+ export async function resolveReserve(requested, existing, catalog, resolveConnection) {
16
+ const { options = {}, estimate, estimateSource, providerPriceVersion, ...rest } = requested;
17
+ // A retry that omits the estimate replays the stored one, including provenance. A retry that
18
+ // sends an estimate keeps only the provenance on that request.
19
+ if (existing) {
20
+ if (estimate === undefined)
21
+ return {
22
+ kind: "reserve",
23
+ input: {
24
+ ...rest,
25
+ estimate: existing.estimate,
26
+ ...provenanceOf(existing.estimateSource, existing.providerPriceVersion),
27
+ },
28
+ };
29
+ return {
30
+ kind: "reserve",
31
+ input: { ...rest, estimate, ...provenanceOf(estimateSource, providerPriceVersion) },
32
+ };
33
+ }
34
+ const policy = catalog && resolveConnection ? await resolveConnection(rest.scope.connection) : undefined;
35
+ if (policy && policy.provider !== rest.provider)
36
+ return invalid("provider", "differs from the connection provider");
37
+ if (policy && catalog) {
38
+ const known = catalog.operationsOf(policy.provider).find((o) => o.id === rest.operation);
39
+ if (policy.tracking?.[rest.operation] === "passthrough" || known?.billable === false)
40
+ return { kind: "count", state: "passthrough" };
41
+ if (estimate === undefined) {
42
+ if (!known)
43
+ return { kind: "count", state: "unpriced" };
44
+ const { tracking: _, ...connection } = policy, resolved = catalog.estimate({ ...connection, id: rest.scope.connection }, rest.operation, options);
45
+ return {
46
+ kind: "reserve",
47
+ input: {
48
+ ...rest,
49
+ estimate: resolved.quantities,
50
+ estimateSource: resolved.source,
51
+ ...(resolved.priceVersion ? { providerPriceVersion: resolved.priceVersion } : {}),
52
+ },
53
+ };
54
+ }
55
+ }
56
+ if (estimate === undefined)
57
+ return invalid("estimate", catalog && resolveConnection ? "unknown connection" : "required without a catalog");
58
+ return {
59
+ kind: "reserve",
60
+ input: { ...rest, estimate, ...provenanceOf(estimateSource, providerPriceVersion) },
61
+ };
62
+ }
package/dist/index.d.ts CHANGED
@@ -3,3 +3,5 @@ export { createMeter } from "./meter.js";
3
3
  export { defaultPolicy } from "./policy.js";
4
4
  export type { MeterPolicy } from "./policy.js";
5
5
  export type { OwnershipResolver } from "./access.js";
6
+ export { resolveReserve } from "./catalog.js";
7
+ export type { ConnectionResolver, ReserveResolution } from "./catalog.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { createMeter } from "./meter.js";
2
2
  export { defaultPolicy } from "./policy.js";
3
+ export { resolveReserve } from "./catalog.js";
package/dist/meter.d.ts CHANGED
@@ -1,9 +1,13 @@
1
- import type { Meter } from "@usagekit/core";
1
+ import type { Meter, PricingCatalog } from "@usagekit/core";
2
+ import type { ConnectionResolver } from "./catalog.js";
2
3
  import type { Store, Clock } from "@usagekit/store";
3
4
  import type { MeterPolicy } from "./policy.js";
4
- export declare function createMeter({ store, policy, clock, resolveOwnership, }: {
5
+ export declare function createMeter({ store, policy, clock, resolveOwnership, catalog, resolveConnection, }: {
5
6
  store: Store;
6
7
  policy?: MeterPolicy;
7
8
  clock: Clock;
8
9
  resolveOwnership?: import("./access.js").OwnershipResolver;
10
+ /** With resolveConnection, lets reserve omit estimate and applies tracking policy. */
11
+ catalog?: PricingCatalog;
12
+ resolveConnection?: ConnectionResolver;
9
13
  }): Meter;
package/dist/meter.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { resolveReserve } from "./catalog.js";
1
2
  import { InvalidInput } from "@usagekit/store";
2
3
  import { defaultPolicy } from "./policy.js";
3
- import { validate, reserveValidation, usageValidation, operationsValidation, sourceValidation, } from "./validation.js";
4
+ import { validate, reserveValidation, usageValidation, operationsValidation, sourceValidation, provenanceValidation, requestCountsValidation, } from "./validation.js";
4
5
  import { canRead, canReadBudget, canReadShared, isShared } from "./access.js";
5
6
  import { defaultBudgetOrder, normalizeTags } from "@usagekit/core";
6
- export function createMeter({ store, policy = defaultPolicy, clock, resolveOwnership, }) {
7
+ export function createMeter({ store, policy = defaultPolicy, clock, resolveOwnership, catalog, resolveConnection, }) {
7
8
  if (!Number.isFinite(clock.now().getTime()))
8
9
  throw new Error("InvalidInput: clock");
9
10
  if (policy.budgetOrder.length !== defaultBudgetOrder.length ||
@@ -30,18 +31,48 @@ export function createMeter({ store, policy = defaultPolicy, clock, resolveOwner
30
31
  const read = async (fn) => safely(async () => ({ outcome: "ok", value: await fn() }));
31
32
  return {
32
33
  expireReservations: (i) => command(i, () => store.expireReservations(i)),
33
- reserve: async (raw) => {
34
- const invalid = reserveValidation(raw);
35
- if (invalid)
36
- return invalid;
37
- // Tags are attribution: sorted here so every adapter stores one canonical order.
38
- const tags = raw.scope.tags === undefined ? undefined : normalizeTags(raw.scope.tags), i = tags ? { ...raw, scope: { ...raw.scope, tags } } : raw;
34
+ reserve: async (requested) => {
35
+ const early = validate({ ...requested, estimate: requested.estimate ?? [] }) ??
36
+ sourceValidation(requested) ??
37
+ provenanceValidation(requested);
38
+ if (early)
39
+ return early;
39
40
  return safely(async () => {
40
41
  const existing = await store.getOperation({
41
- namespace: i.scope.namespace,
42
- principal: i.scope.principal,
43
- operationId: i.operationId,
42
+ namespace: requested.scope.namespace,
43
+ principal: requested.scope.principal,
44
+ operationId: requested.operationId,
44
45
  });
46
+ const resolution = await resolveReserve(requested, existing, catalog, resolveConnection);
47
+ if (resolution.kind === "invalid")
48
+ return resolution.failure;
49
+ if (resolution.kind === "count") {
50
+ const { scope, surface, source, provider, operation, platformPools } = requested;
51
+ const counted = await store.countRequest({
52
+ commandId: requested.operationId,
53
+ scope,
54
+ surface,
55
+ source,
56
+ provider,
57
+ operation,
58
+ state: resolution.state,
59
+ ...(platformPools ? { platformPools } : {}),
60
+ });
61
+ if (counted.outcome === "conflict")
62
+ return {
63
+ outcome: "invalid",
64
+ field: "operationId",
65
+ reason: "reused for a different counted request",
66
+ };
67
+ if (counted.outcome === "invalid")
68
+ return counted;
69
+ return { outcome: resolution.state, replayed: counted.replayed, operation: null };
70
+ }
71
+ const raw = resolution.input, invalid = reserveValidation(raw);
72
+ if (invalid)
73
+ return invalid;
74
+ // Tags are attribution: sorted here so every adapter stores one canonical order.
75
+ const tags = raw.scope.tags === undefined ? undefined : normalizeTags(raw.scope.tags), i = tags ? { ...raw, scope: { ...raw.scope, tags } } : raw;
45
76
  if (existing)
46
77
  return store.reserve(i, { budgetOrder: policy.budgetOrder });
47
78
  const statuses = await store.applicableBudgets({
@@ -68,6 +99,18 @@ export function createMeter({ store, policy = defaultPolicy, clock, resolveOwner
68
99
  settle: (i) => command(i, () => store.settle(i)),
69
100
  correct: (i) => command(i, () => store.correct(i)),
70
101
  releaseUndispatched: (i) => command(i, () => store.releaseUndispatched(i)),
102
+ countRequest: (i) => {
103
+ const invalid = validate(i) ?? sourceValidation(i);
104
+ return invalid ? Promise.resolve(invalid) : safely(() => store.countRequest(i));
105
+ },
106
+ requestCounts: async (access, q) => {
107
+ const invalid = requestCountsValidation(q);
108
+ if (invalid)
109
+ return invalid;
110
+ if (!access.canReadBillingDetail || !canRead(access, q.scope))
111
+ return { outcome: "forbidden" };
112
+ return read(() => store.requestCounts(q));
113
+ },
71
114
  getOperation: async (access, i) => {
72
115
  const invalid = validate(i);
73
116
  if (invalid)
@@ -1,7 +1,12 @@
1
- import type { ValidationFailure, ReserveInput, UsageQuery, ApplicableBudgetsQuery, OperationsQuery } from "@usagekit/core";
1
+ import type { ValidationFailure, ReserveInput, UsageQuery, ApplicableBudgetsQuery, OperationsQuery, RequestCountsQuery } from "@usagekit/core";
2
2
  export declare function validate(value: unknown, field?: string): ValidationFailure | null;
3
3
  /** The host sets both fields at authentication; a source outside its surface group is a forgery or a bug. */
4
4
  export declare function sourceValidation(i: Pick<ApplicableBudgetsQuery, "surface" | "source">): ValidationFailure | null;
5
5
  export declare function reserveValidation(i: ReserveInput): ValidationFailure | null;
6
6
  export declare function operationsValidation(q: OperationsQuery): ValidationFailure | null;
7
7
  export declare function usageValidation(q: UsageQuery): ValidationFailure | null;
8
+ /** Hosts may name the source; a value outside the catalog order is rejected before Store. */
9
+ export declare function provenanceValidation(i: {
10
+ estimateSource?: string;
11
+ }): ValidationFailure | null;
12
+ export declare function requestCountsValidation(q: RequestCountsQuery): ValidationFailure | null;
@@ -74,3 +74,14 @@ export function usageValidation(q) {
74
74
  };
75
75
  return null;
76
76
  }
77
+ const estimateSources = ["manual", "measured", "list", "unknown"];
78
+ /** Hosts may name the source; a value outside the catalog order is rejected before Store. */
79
+ export function provenanceValidation(i) {
80
+ if (i.estimateSource !== undefined &&
81
+ !estimateSources.includes(i.estimateSource))
82
+ return { outcome: "invalid", field: "estimateSource", reason: "unknown estimate source" };
83
+ return null;
84
+ }
85
+ export function requestCountsValidation(q) {
86
+ return validate(q) ?? windowValidation(q);
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagekit/meter",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Admission, reservations, settlement and recovery.",
@@ -23,8 +23,8 @@
23
23
  "prepack": "node ../../scripts/package-license.mjs"
24
24
  },
25
25
  "dependencies": {
26
- "@usagekit/core": "0.3.0",
27
- "@usagekit/store": "0.3.0"
26
+ "@usagekit/core": "0.4.0",
27
+ "@usagekit/store": "0.4.0"
28
28
  },
29
29
  "sideEffects": false,
30
30
  "publishConfig": {