g1t/services/deployments/src/metering.ts
| 1 | /** |
| 2 | * What a workspace's deployments cost in a month, from its meter: every |
| 3 | * request, CPU millisecond and custom domain, from the first, at what each |
| 4 | * costs g1t. Billing adds the margin and draws on the plan's included usage |
| 5 | * first. There are no allowances and no counts that stop anything: apps |
| 6 | * (production and previews) are not metered at all, since Cloudflare's |
| 7 | * Workers for Platforms includes far more scripts than g1t runs. |
| 8 | * |
| 9 | * No Workers imports, so it can be tested under Node. |
| 10 | */ |
| 11 | |
| 12 | /** What each unit costs g1t, in millionths of a dollar. */ |
| 13 | export type UnitCosts = { |
| 14 | millionRequests: number; |
| 15 | millionCpuMs: number; |
| 16 | domainMonth: number; |
| 17 | }; |
| 18 | |
| 19 | /** A month's meter row, as the `meters` table keeps it. */ |
| 20 | export type MonthMeter = { |
| 21 | requests: number; |
| 22 | cpu_ms: number; |
| 23 | peak_domains: number | null; |
| 24 | }; |
| 25 | |
| 26 | /** What the month's traffic and domains cost g1t, and how much there was. */ |
| 27 | export type MonthCost = { |
| 28 | /** App requests and CPU time. */ |
| 29 | traffic: { micros: number; detail: string | null }; |
| 30 | /** Custom domains: the most the workspace had at once this month, each for the month. */ |
| 31 | domains: { micros: number; detail: string | null }; |
| 32 | /** Both, rounded up: what a month's close charges. */ |
| 33 | micros: number; |
| 34 | /** For the ledger: `1,200,000 requests, 3,400,000 CPU ms, 2 custom domains`. */ |
| 35 | description: string; |
| 36 | }; |
| 37 | |
| 38 | /** `1.2 million`, `840,000`. */ |
| 39 | export function amount(n: number): string { |
| 40 | if (n >= 1_000_000) return `${(Math.round(n / 100_000) / 10).toLocaleString("en-US")} million`; |
| 41 | return Math.round(n).toLocaleString("en-US"); |
| 42 | } |
| 43 | |
| 44 | export function monthCost(meter: MonthMeter, costs: UnitCosts): MonthCost { |
| 45 | const requests = Math.max(0, meter.requests ?? 0); |
| 46 | const cpuMs = Math.max(0, meter.cpu_ms ?? 0); |
| 47 | const domains = Math.max(0, meter.peak_domains ?? 0); |
| 48 | const traffic = Math.ceil((requests / 1_000_000) * costs.millionRequests + (cpuMs / 1_000_000) * costs.millionCpuMs); |
| 49 | const domainMicros = Math.ceil(domains * costs.domainMonth); |
| 50 | const plural = (n: number, word: string) => `${n.toLocaleString("en-US")} ${word}${n === 1 ? "" : "s"}`; |
| 51 | const trafficDetail = requests || cpuMs ? `${amount(requests)} requests and ${amount(cpuMs)} CPU ms` : null; |
| 52 | const domainDetail = domains ? plural(domains, "custom domain") : null; |
| 53 | const parts = [ |
| 54 | requests && `${requests.toLocaleString("en-US")} requests`, |
| 55 | cpuMs && `${cpuMs.toLocaleString("en-US")} CPU ms`, |
| 56 | domains && plural(domains, "custom domain"), |
| 57 | ].filter(Boolean); |
| 58 | return { |
| 59 | traffic: { micros: traffic, detail: trafficDetail }, |
| 60 | domains: { micros: domainMicros, detail: domainDetail }, |
| 61 | micros: traffic + domainMicros, |
| 62 | description: parts.join(", "), |
| 63 | }; |
| 64 | } |