g1t/services/deployments/src/metering.ts

64 lines2,702 bytesCodeBlame
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. */
13export 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. */
20export 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. */
27export 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`. */
39export 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
44export 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}