337 lines · 13.5 KB
import { env, waitUntil } from "cloudflare:workers";
import { accountDb, stmt } from "../scoped-db";
import type { ScopedStatement } from "../scoped-db";
import { locate } from "../scoped-db/locators";
import { creditDrawStatements } from "./credits";
import { claimLocator, noteScratchAccount } from "./directory";
import { randomId } from "../crypto";
import { meterDef } from "~/lib/meters";
import type { MeterId } from "~/lib/meters";

/** Billing periods are UTC calendar months, `YYYY-MM`. */
export function periodOf(now = Date.now()): string {
  return new Date(now).toISOString().slice(0, 7);
}

export type UsageInput = {
  /** The account billed: a user or org id, or a `scratch:` id for anonymous usage. */
  accountId: string;
  meter: MeterId;
  /** Exact quantity in the meter's unit; compute is per second (fractions allowed), never rounded up. */
  quantity: number;
  /** Optional sanity check against the catalog unit. */
  unit?: string;
  repoId?: string | null;
  attributes?: Record<string, string | number | boolean | null>;
  /** Stable for the logical event: a retry with the same key is ignored. Prefix with your subsystem. */
  idempotencyKey: string;
  /** Event time, defaults to now; decides the billing period. */
  at?: number;
};

const MAX_KEY_LENGTH = 200;

export class MeteringError extends Error {}

export function validate(input: UsageInput): void {
  const def = meterDef(input.meter);
  if (!def) throw new MeteringError(`Unknown meter ${input.meter}`);
  if (def.kind !== "counter") throw new MeteringError(`${input.meter} is a gauge; use setGauge`);
  if (input.unit && input.unit !== def.unit) throw new MeteringError(`${input.meter} is measured in ${def.unit}`);