| 1 | /** |
| 2 | * Each project's store as a Durable Object: the logic is ./store.ts's |
| 3 | * `Store`, whose methods are copied onto the object's (and so are |
| 4 | * callable over RPC; ./index.ts types the stub as a `Store`). Each |
| 5 | * workspace's meter likewise: ./meter.ts's `WorkspaceMeter`. |
| 6 | */ |
| 7 | import { DurableObject } from "cloudflare:workers"; |
| 8 | |
| 9 | import { WorkspaceMeter } from "./meter.ts"; |
| 10 | import { TELEMETRY_MICROS_PER_GB, freeMonthlyBytes, telemetryCost, telemetryDetail } from "./quota.ts"; |
| 11 | import { Store } from "./store.ts"; |
| 12 | |
| 13 | export class TelemetryStore extends DurableObject { |
| 14 | constructor(ctx: DurableObjectState, env: unknown) { |
| 15 | super(ctx, env as never); |
| 16 | ctx.blockConcurrencyWhile(async () => Store.prototype.open.call(this, ctx)); |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | for (const name of Object.getOwnPropertyNames(Store.prototype)) { |
| 21 | if (name === "constructor") continue; |
| 22 | Object.defineProperty(TelemetryStore.prototype, name, Object.getOwnPropertyDescriptor(Store.prototype, name)!); |
| 23 | } |
| 24 | |
| 25 | type MeterEnv = { |
| 26 | BILLING?: { fetch: typeof fetch }; |
| 27 | FREE_TELEMETRY_BYTES_PER_MONTH?: string; |
| 28 | }; |
| 29 | |
| 30 | export class TelemetryMeter extends DurableObject<MeterEnv> { |
| 31 | constructor(ctx: DurableObjectState, env: MeterEnv) { |
| 32 | super(ctx, env); |
| 33 | ctx.blockConcurrencyWhile(async () => WorkspaceMeter.prototype.open.call(this, ctx)); |
| 34 | } |
| 35 | |
| 36 | /** Reports the month's total to billing, at cost past the free amount. */ |
| 37 | async alarm(): Promise<void> { |
| 38 | const billing = this.env.BILLING; |
| 39 | if (!billing) return; |
| 40 | const free = freeMonthlyBytes(this.env.FREE_TELEMETRY_BYTES_PER_MONTH); |
| 41 | await (this as unknown as WorkspaceMeter).note(async (workspace, month, bytes) => { |
| 42 | const { billingClient } = await import("@g1t/contracts"); |
| 43 | const client = billingClient(billing as never); |
| 44 | const book = await client.prices().catch(() => null); |
| 45 | const perGb = book?.prices.find((p) => p.meter === "telemetry_gb")?.costMicros ?? TELEMETRY_MICROS_PER_GB; |
| 46 | // Any answer is billing's to keep (a month it has charged already is |
| 47 | // refused for good); only a failure to reach it is tried again. |
| 48 | await client.notePending(workspace, "telemetry", telemetryCost(bytes, free, perGb), telemetryDetail(bytes, free), month); |
| 49 | return true; |
| 50 | }); |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | for (const name of Object.getOwnPropertyNames(WorkspaceMeter.prototype)) { |
| 55 | if (name === "constructor") continue; |
| 56 | Object.defineProperty(TelemetryMeter.prototype, name, Object.getOwnPropertyDescriptor(WorkspaceMeter.prototype, name)!); |
| 57 | } |