@rebasepro/server 0.21.0 → 0.21.1

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.
Files changed (56) hide show
  1. package/README.md +29 -11
  2. package/dist/api/errors.d.ts +41 -0
  3. package/dist/auth/admin-user-ops.d.ts +23 -1
  4. package/dist/auth/auth-hooks.d.ts +4 -0
  5. package/dist/auth/reset-password-admin.d.ts +4 -0
  6. package/dist/auth/token-revocation.d.ts +39 -5
  7. package/dist/{auth-gzoRjnuh.js → auth-BS4WBA10.js} +1636 -1565
  8. package/dist/auth-BS4WBA10.js.map +1 -0
  9. package/dist/backup/backup-common.d.ts +2 -1
  10. package/dist/{backup-DGu0v9Ku.js → backup-DzI9jLwc.js} +38 -10
  11. package/dist/backup-DzI9jLwc.js.map +1 -0
  12. package/dist/collections/validate-config.d.ts +4 -2
  13. package/dist/{cron-routes-BamgO0Xr.js → cron-routes-B_wlLybo.js} +3 -3
  14. package/dist/{cron-routes-BamgO0Xr.js.map → cron-routes-B_wlLybo.js.map} +1 -1
  15. package/dist/{cron-scheduler-D47tdB9T.js → cron-scheduler-COPQxlEq.js} +59 -10
  16. package/dist/cron-scheduler-COPQxlEq.js.map +1 -0
  17. package/dist/{cron-store-yXOyIEmn.js → cron-store-Dp0X7zRB.js} +2 -2
  18. package/dist/{cron-store-yXOyIEmn.js.map → cron-store-Dp0X7zRB.js.map} +1 -1
  19. package/dist/ddl-bootstrap-CfNvxMuK.js.map +1 -1
  20. package/dist/deploy/pod-contract.d.ts +14 -0
  21. package/dist/{errors-DMImyqyR.js → errors-DWsX4yTd.js} +55 -18
  22. package/dist/errors-DWsX4yTd.js.map +1 -0
  23. package/dist/{function-routes-C4nB2h0z.js → function-routes-Chet4-lB.js} +2 -2
  24. package/dist/{function-routes-C4nB2h0z.js.map → function-routes-Chet4-lB.js.map} +1 -1
  25. package/dist/functions/index.js.map +1 -1
  26. package/dist/{history-recorder-hBGuXcZc.js → history-recorder-5VKT7nss.js} +2 -2
  27. package/dist/{history-recorder-hBGuXcZc.js.map → history-recorder-5VKT7nss.js.map} +1 -1
  28. package/dist/{history-store-C9EtGtoL.js → history-store-By2R_G1b.js} +2 -2
  29. package/dist/{history-store-C9EtGtoL.js.map → history-store-By2R_G1b.js.map} +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.es.js +286 -245
  32. package/dist/index.es.js.map +1 -1
  33. package/dist/init/middlewares.d.ts +19 -1
  34. package/dist/init/storage.d.ts +17 -0
  35. package/dist/init.d.ts +6 -2
  36. package/dist/{jobs-DZ8DLLcU.js → jobs-1IkI4Lle.js} +2 -2
  37. package/dist/{jobs-DZ8DLLcU.js.map → jobs-1IkI4Lle.js.map} +1 -1
  38. package/dist/{logs-routes-DnJINsMu.js → logs-routes-3EEzPjhl.js} +2 -2
  39. package/dist/{logs-routes-DnJINsMu.js.map → logs-routes-3EEzPjhl.js.map} +1 -1
  40. package/dist/{openapi-generator-CAFKF36E.js → openapi-generator-BDpoKZQ9.js} +2 -2
  41. package/dist/{openapi-generator-CAFKF36E.js.map → openapi-generator-BDpoKZQ9.js.map} +1 -1
  42. package/dist/{query-parser-uqzkZ82i.js → query-parser-C-rl30ce.js} +3 -3
  43. package/dist/{query-parser-uqzkZ82i.js.map → query-parser-C-rl30ce.js.map} +1 -1
  44. package/dist/{request-timeout-BR-OBwES.js → request-timeout-C_4C2BeR.js} +2 -2
  45. package/dist/{request-timeout-BR-OBwES.js.map → request-timeout-C_4C2BeR.js.map} +1 -1
  46. package/dist/{schema-editor-routes-BKOmdf4M.js → schema-editor-routes-DdLihzp0.js} +2 -2
  47. package/dist/{schema-editor-routes-BKOmdf4M.js.map → schema-editor-routes-DdLihzp0.js.map} +1 -1
  48. package/dist/src-Br6ARbs6.js.map +1 -1
  49. package/dist/{src-DW1cQ_28.js → src-Caz7nREj.js} +2 -3
  50. package/dist/src-Caz7nREj.js.map +1 -0
  51. package/package.json +5 -5
  52. package/dist/auth-gzoRjnuh.js.map +0 -1
  53. package/dist/backup-DGu0v9Ku.js.map +0 -1
  54. package/dist/cron-scheduler-D47tdB9T.js.map +0 -1
  55. package/dist/errors-DMImyqyR.js.map +0 -1
  56. package/dist/src-DW1cQ_28.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"history-recorder-hBGuXcZc.js","names":[],"sources":["../src/metrics/history-recorder.ts"],"sourcesContent":["/**\n * Wiring the history store to a driver and a timer.\n *\n * Split from `history-store.ts` so the store stays pure — every rule in it is\n * testable with a fake executor, and nothing there knows what a DataDriver is.\n * This half is the part that cannot be unit-tested meaningfully: it opens a\n * connection and starts an interval.\n */\nimport { monitorEventLoopDelay } from \"node:perf_hooks\";\nimport { logger } from \"../utils/logger.js\";\nimport type { DataDriver } from \"@rebasepro/types\";\nimport {\n ensureMetricsHistory,\n recordSamples,\n readSeries,\n sampleSelf,\n SAMPLE_INTERVAL_MS,\n type Exec,\n type MetricSeries,\n type SeriesPoint\n} from \"./history-store.js\";\n\nexport interface MetricsHistory {\n /** Create the table and sweep what has aged out. */\n ensure(): Promise<void>;\n /** Begin sampling this process. Returns a stop. */\n start(): () => void;\n /** Read one series, for the route and for anything else that asks. */\n read(series: MetricSeries, sinceMinutes: number): Promise<SeriesPoint[]>;\n}\n\n/** Narrow structural check, matching how the job store decides the same thing. */\nfunction sqlExecutorOf(driver: DataDriver): Exec | undefined {\n const admin = (driver as { admin?: { executeSql?: unknown } }).admin;\n if (!admin || typeof admin.executeSql !== \"function\") return undefined;\n const executeSql = admin.executeSql as (sql: string, opts?: { params?: unknown[] }) => Promise<unknown>;\n return (sql, params) => executeSql(sql, params ? { params } : undefined);\n}\n\n/**\n * History for this deployment, or `undefined` when it cannot have any.\n *\n * Undefined rather than a no-op: the route turns it into a named 501, because a\n * chart that renders empty is indistinguishable from a quiet period and this is\n * exactly the class of silence the panel it feeds exists to remove.\n */\nexport function createMetricsHistory(driver: DataDriver): MetricsHistory | undefined {\n const exec = sqlExecutorOf(driver);\n if (!exec) {\n logger.debug(\"[metrics] driver has no SQL admin — no metrics history will be kept.\");\n return undefined;\n }\n\n return {\n ensure: () => ensureMetricsHistory(exec),\n read: (series, sinceMinutes) => readSeries(exec, series, sinceMinutes),\n start(): () => void {\n // Which process this is. A tenant's replicas share one database and\n // each records its own numbers, so a row needs to say whose they\n // are — without it they overwrite each other and a scaled-out app\n // charts one arbitrary pod.\n //\n // HOSTNAME is the pod name on Kubernetes and the container id under\n // Docker; the pid fallback keeps two local processes distinct.\n const instance = process.env.HOSTNAME?.trim() || `pid-${process.pid}`;\n let cursor: { cpu: NodeJS.CpuUsage; at: number } | null = null;\n let stopped = false;\n\n // Event-loop delay: how long a callback waited past its schedule.\n // The one number here that says whether this process is *healthy*\n // rather than how much it is consuming — a pod can sit at 20% CPU\n // and still be unable to answer, and nothing else recorded would\n // show it.\n //\n // `monitorEventLoopDelay` samples in libuv at a fixed resolution and\n // costs effectively nothing; `.enable()` is required, and the\n // histogram is reset each tick so every sample describes its own\n // minute rather than the process's whole life.\n interface LoopHistogram { mean: number; enable(): void; reset(): void }\n let loop: LoopHistogram | null = null;\n try {\n loop = monitorEventLoopDelay({ resolution: 20 }) as LoopHistogram;\n loop.enable();\n } catch {\n // A runtime without it still records the other two.\n loop = null;\n }\n\n const tick = async () => {\n if (stopped) return;\n // Nanoseconds from the histogram, milliseconds on the wire.\n const delayMs = loop ? loop.mean / 1e6 : undefined;\n loop?.reset();\n const { samples, cursor: next } = sampleSelf(cursor, Date.now(), delayMs);\n cursor = next;\n try {\n await recordSamples(exec, samples, instance);\n } catch (err) {\n // Never fatal, and never noisy: a sampler that crash-loops a\n // pod over a chart would be a far worse trade than a gap in\n // one. The gap is visible in the data; a restart loop is not.\n logger.debug(\"[metrics] could not record a sample\", { err });\n }\n };\n\n // The first tick establishes the CPU cursor and publishes memory;\n // the rate needs a second reading, which is why nothing claims a CPU\n // figure until one interval has passed.\n void tick();\n const timer = setInterval(() => void tick(), SAMPLE_INTERVAL_MS);\n // Not the reason this process should stay alive.\n timer.unref?.();\n\n return () => { stopped = true; clearInterval(timer); };\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,SAAS,cAAc,QAAsC;CACzD,MAAM,QAAS,OAAgD;CAC/D,IAAI,CAAC,SAAS,OAAO,MAAM,eAAe,YAAY,OAAO,KAAA;CAC7D,MAAM,aAAa,MAAM;CACzB,QAAQ,KAAK,WAAW,WAAW,KAAK,SAAS,EAAE,OAAO,IAAI,KAAA,CAAS;AAC3E;;;;;;;;AASA,SAAgB,qBAAqB,QAAgD;CACjF,MAAM,OAAO,cAAc,MAAM;CACjC,IAAI,CAAC,MAAM;EACP,OAAO,MAAM,sEAAsE;EACnF;CACJ;CAEA,OAAO;EACH,cAAc,qBAAqB,IAAI;EACvC,OAAO,QAAQ,iBAAiB,WAAW,MAAM,QAAQ,YAAY;EACrE,QAAoB;GAQhB,MAAM,WAAW,QAAQ,IAAI,UAAU,KAAK,KAAK,OAAO,QAAQ;GAChE,IAAI,SAAsD;GAC1D,IAAI,UAAU;GAad,IAAI,OAA6B;GACjC,IAAI;IACA,OAAO,sBAAsB,EAAE,YAAY,GAAG,CAAC;IAC/C,KAAK,OAAO;GAChB,QAAQ;IAEJ,OAAO;GACX;GAEA,MAAM,OAAO,YAAY;IACrB,IAAI,SAAS;IAEb,MAAM,UAAU,OAAO,KAAK,OAAO,MAAM,KAAA;IACzC,MAAM,MAAM;IACZ,MAAM,EAAE,SAAS,QAAQ,SAAS,WAAW,QAAQ,KAAK,IAAI,GAAG,OAAO;IACxE,SAAS;IACT,IAAI;KACA,MAAM,cAAc,MAAM,SAAS,QAAQ;IAC/C,SAAS,KAAK;KAIV,OAAO,MAAM,uCAAuC,EAAE,IAAI,CAAC;IAC/D;GACJ;GAKA,KAAU;GACV,MAAM,QAAQ,kBAAkB,KAAK,KAAK,GAAG,kBAAkB;GAE/D,MAAM,QAAQ;GAEd,aAAa;IAAE,UAAU;IAAM,cAAc,KAAK;GAAG;EACzD;CACJ;AACJ"}
1
+ {"version":3,"file":"history-recorder-5VKT7nss.js","names":[],"sources":["../src/metrics/history-recorder.ts"],"sourcesContent":["/**\n * Wiring the history store to a driver and a timer.\n *\n * Split from `history-store.ts` so the store stays pure — every rule in it is\n * testable with a fake executor, and nothing there knows what a DataDriver is.\n * This half is the part that cannot be unit-tested meaningfully: it opens a\n * connection and starts an interval.\n */\nimport { monitorEventLoopDelay } from \"node:perf_hooks\";\nimport { logger } from \"../utils/logger.js\";\nimport type { DataDriver } from \"@rebasepro/types\";\nimport {\n ensureMetricsHistory,\n recordSamples,\n readSeries,\n sampleSelf,\n SAMPLE_INTERVAL_MS,\n type Exec,\n type MetricSeries,\n type SeriesPoint\n} from \"./history-store.js\";\n\nexport interface MetricsHistory {\n /** Create the table and sweep what has aged out. */\n ensure(): Promise<void>;\n /** Begin sampling this process. Returns a stop. */\n start(): () => void;\n /** Read one series, for the route and for anything else that asks. */\n read(series: MetricSeries, sinceMinutes: number): Promise<SeriesPoint[]>;\n}\n\n/** Narrow structural check, matching how the job store decides the same thing. */\nfunction sqlExecutorOf(driver: DataDriver): Exec | undefined {\n const admin = (driver as { admin?: { executeSql?: unknown } }).admin;\n if (!admin || typeof admin.executeSql !== \"function\") return undefined;\n const executeSql = admin.executeSql as (sql: string, opts?: { params?: unknown[] }) => Promise<unknown>;\n return (sql, params) => executeSql(sql, params ? { params } : undefined);\n}\n\n/**\n * History for this deployment, or `undefined` when it cannot have any.\n *\n * Undefined rather than a no-op: the route turns it into a named 501, because a\n * chart that renders empty is indistinguishable from a quiet period and this is\n * exactly the class of silence the panel it feeds exists to remove.\n */\nexport function createMetricsHistory(driver: DataDriver): MetricsHistory | undefined {\n const exec = sqlExecutorOf(driver);\n if (!exec) {\n logger.debug(\"[metrics] driver has no SQL admin — no metrics history will be kept.\");\n return undefined;\n }\n\n return {\n ensure: () => ensureMetricsHistory(exec),\n read: (series, sinceMinutes) => readSeries(exec, series, sinceMinutes),\n start(): () => void {\n // Which process this is. A tenant's replicas share one database and\n // each records its own numbers, so a row needs to say whose they\n // are — without it they overwrite each other and a scaled-out app\n // charts one arbitrary pod.\n //\n // HOSTNAME is the pod name on Kubernetes and the container id under\n // Docker; the pid fallback keeps two local processes distinct.\n const instance = process.env.HOSTNAME?.trim() || `pid-${process.pid}`;\n let cursor: { cpu: NodeJS.CpuUsage; at: number } | null = null;\n let stopped = false;\n\n // Event-loop delay: how long a callback waited past its schedule.\n // The one number here that says whether this process is *healthy*\n // rather than how much it is consuming — a pod can sit at 20% CPU\n // and still be unable to answer, and nothing else recorded would\n // show it.\n //\n // `monitorEventLoopDelay` samples in libuv at a fixed resolution and\n // costs effectively nothing; `.enable()` is required, and the\n // histogram is reset each tick so every sample describes its own\n // minute rather than the process's whole life.\n interface LoopHistogram { mean: number; enable(): void; reset(): void }\n let loop: LoopHistogram | null = null;\n try {\n loop = monitorEventLoopDelay({ resolution: 20 }) as LoopHistogram;\n loop.enable();\n } catch {\n // A runtime without it still records the other two.\n loop = null;\n }\n\n const tick = async () => {\n if (stopped) return;\n // Nanoseconds from the histogram, milliseconds on the wire.\n const delayMs = loop ? loop.mean / 1e6 : undefined;\n loop?.reset();\n const { samples, cursor: next } = sampleSelf(cursor, Date.now(), delayMs);\n cursor = next;\n try {\n await recordSamples(exec, samples, instance);\n } catch (err) {\n // Never fatal, and never noisy: a sampler that crash-loops a\n // pod over a chart would be a far worse trade than a gap in\n // one. The gap is visible in the data; a restart loop is not.\n logger.debug(\"[metrics] could not record a sample\", { err });\n }\n };\n\n // The first tick establishes the CPU cursor and publishes memory;\n // the rate needs a second reading, which is why nothing claims a CPU\n // figure until one interval has passed.\n void tick();\n const timer = setInterval(() => void tick(), SAMPLE_INTERVAL_MS);\n // Not the reason this process should stay alive.\n timer.unref?.();\n\n return () => { stopped = true; clearInterval(timer); };\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,SAAS,cAAc,QAAsC;CACzD,MAAM,QAAS,OAAgD;CAC/D,IAAI,CAAC,SAAS,OAAO,MAAM,eAAe,YAAY,OAAO,KAAA;CAC7D,MAAM,aAAa,MAAM;CACzB,QAAQ,KAAK,WAAW,WAAW,KAAK,SAAS,EAAE,OAAO,IAAI,KAAA,CAAS;AAC3E;;;;;;;;AASA,SAAgB,qBAAqB,QAAgD;CACjF,MAAM,OAAO,cAAc,MAAM;CACjC,IAAI,CAAC,MAAM;EACP,OAAO,MAAM,sEAAsE;EACnF;CACJ;CAEA,OAAO;EACH,cAAc,qBAAqB,IAAI;EACvC,OAAO,QAAQ,iBAAiB,WAAW,MAAM,QAAQ,YAAY;EACrE,QAAoB;GAQhB,MAAM,WAAW,QAAQ,IAAI,UAAU,KAAK,KAAK,OAAO,QAAQ;GAChE,IAAI,SAAsD;GAC1D,IAAI,UAAU;GAad,IAAI,OAA6B;GACjC,IAAI;IACA,OAAO,sBAAsB,EAAE,YAAY,GAAG,CAAC;IAC/C,KAAK,OAAO;GAChB,QAAQ;IAEJ,OAAO;GACX;GAEA,MAAM,OAAO,YAAY;IACrB,IAAI,SAAS;IAEb,MAAM,UAAU,OAAO,KAAK,OAAO,MAAM,KAAA;IACzC,MAAM,MAAM;IACZ,MAAM,EAAE,SAAS,QAAQ,SAAS,WAAW,QAAQ,KAAK,IAAI,GAAG,OAAO;IACxE,SAAS;IACT,IAAI;KACA,MAAM,cAAc,MAAM,SAAS,QAAQ;IAC/C,SAAS,KAAK;KAIV,OAAO,MAAM,uCAAuC,EAAE,IAAI,CAAC;IAC/D;GACJ;GAKA,KAAU;GACV,MAAM,QAAQ,kBAAkB,KAAK,KAAK,GAAG,kBAAkB;GAE/D,MAAM,QAAQ;GAEd,aAAa;IAAE,UAAU;IAAM,cAAc,KAAK;GAAG;EACzD;CACJ;AACJ"}
@@ -2,7 +2,7 @@ import { createRequire as __rebaseCreateRequire } from "module";
2
2
  import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
- import { P as revokeInternalTableSql } from "./src-DW1cQ_28.js";
5
+ import { P as revokeInternalTableSql } from "./src-Caz7nREj.js";
6
6
  import { n as sqlRows } from "./sql-rows-C6GEc2oE.js";
7
7
  //#region src/metrics/history-store.ts
8
8
  /**
@@ -206,4 +206,4 @@ async function readSeries(exec, series, sinceMinutes) {
206
206
  //#endregion
207
207
  export { recordSamples as a, readSeries as i, SAMPLE_INTERVAL_MS as n, sampleSelf as o, ensureMetricsHistory as r, METRIC_SERIES as t };
208
208
 
209
- //# sourceMappingURL=history-store-C9EtGtoL.js.map
209
+ //# sourceMappingURL=history-store-By2R_G1b.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"history-store-C9EtGtoL.js","names":[],"sources":["../src/metrics/history-store.ts"],"sourcesContent":["/**\n * A little history for the metrics this process already keeps.\n *\n * ## Why this is in the framework and not in the cloud console\n *\n * The console wants to draw \"CPU over the last hour\". The obvious way to get it\n * on GKE is Cloud Monitoring, which already collects exactly this — and which\n * would make the panel unportable the day the platform moves, for a feature\n * every self-hoster also wants. So the history lives where the runtime lives:\n * the process samples ITSELF, into its OWN database, and anything that can read\n * the database can draw the chart. No cluster, no metrics-server, no vendor.\n *\n * That is the same rule the binder follows — the cloud is a better\n * implementation behind the same interface, never a different one.\n *\n * ## Why it stays cheap\n *\n * One row per series per instance per minute. Three series across two replicas\n * is 8,640 rows a day, and the sweep below bounds the window, so the table settles\n * at a size measured in megabytes. It is deliberately NOT a general time-series\n * store: no labels, no cardinality to explode, no per-request rows.\n *\n * A minute is the resolution because that is what the question needs — \"was it\n * slow at 15:40\", \"did my deploy cause that\" — and because a finer grain buys\n * nothing a reader can see on a chart of an hour.\n */\n/**\n * The positional-parameter shape every store in this package settles on.\n *\n * The bootstrapper's own `SqlExec` takes an options object; each store wraps it\n * once and reads better for it. Same two shapes, same reason, as `job-store`.\n */\nimport { revokeInternalTableSql, sqlRows } from \"@rebasepro/common\";\n\n\nexport type Exec = (sql: string, params?: unknown[]) => Promise<unknown>;\n\n/** Where the samples live. Framework-owned, like `rebase.jobs`. */\nexport const METRICS_HISTORY_TABLE = \"rebase.metric_samples\";\n\n/**\n * How long a sample is kept.\n *\n * Two weeks answers \"is this worse than last week\" and stops well short of\n * being an archive. Anything that needs to outlive it — billing, capacity\n * planning — is a rollup somebody else owns, not a longer retention here.\n *\n * Read it as \"14 days, or since this process started, whichever is longer\": the\n * sweep runs at boot and nowhere else, so a pod up for 90 days holds 90 days.\n * That is a deliberate trade rather than an oversight — a cron for it would be\n * machinery for a table that stays trivial either way, and reads are bounded by\n * the index regardless — but the table does not \"reach a steady size and stay\n * there\" on a long-lived pod, which an earlier version of this comment claimed.\n */\nexport const RETENTION_DAYS = 14;\n\n/** How often the process samples itself. Matched to the resolution it stores. */\nexport const SAMPLE_INTERVAL_MS = 60_000;\n\n/**\n * The series this records. A closed set on purpose — see the cardinality note.\n *\n * A list rather than only a union, because the route validates against it: an\n * unknown `?series=` must be a named 400 rather than an empty chart, which\n * reads exactly like a quiet period.\n *\n * **It lists what `sampleSelf` actually writes, and nothing else.** It used to\n * also name `requests_total`, `errors_total` and `event_loop_delay_ms`, none of\n * which anything ever recorded — so those three were *valid* parameters that\n * returned `points: []`, which is precisely the empty chart the 400 two\n * paragraphs up exists to prevent. A declared-but-unwritten series is worse\n * than an absent one: the 400 tells you the name is wrong, and the empty array\n * tells you the app was quiet.\n *\n * `event_loop_delay_ms` kept its place by gaining a sampler, because it is the\n * one signal here that says whether the process is *healthy* rather than how\n * much it is using, and it has no counter semantics to get wrong.\n *\n * `requests_total` and `errors_total` were dropped rather than wired up. The\n * registry does hold them, but they reset on restart, and a resetting counter\n * summed across replicas is not monotonic — a rolling deploy would draw a\n * cliff that looks like traffic collapsing. That needs a deliberate decision\n * about deltas and resets, not a line added at 2am. Adding either back means\n * adding its sampler in the same commit.\n */\nexport const METRIC_SERIES = [\n \"cpu_millicores\",\n \"memory_bytes\",\n \"event_loop_delay_ms\"\n] as const;\n\nexport type MetricSeries = (typeof METRIC_SERIES)[number];\n\nexport interface MetricSample {\n at: Date;\n series: MetricSeries;\n value: number;\n}\n\n/**\n * Create the table and sweep what has aged out.\n *\n * Called at boot, beside the job and cron stores, and for the same reason they\n * do it there: the only moment the schema is guaranteed to be reachable and\n * nobody is mid-request.\n */\nexport async function ensureMetricsHistory(exec: Exec): Promise<void> {\n await exec(`\n CREATE TABLE IF NOT EXISTS ${METRICS_HISTORY_TABLE} (\n at timestamptz NOT NULL,\n series text NOT NULL,\n instance text NOT NULL,\n value double precision NOT NULL,\n PRIMARY KEY (series, instance, at)\n )\n `, []);\n\n // (series, at DESC) rather than the primary key's order: every read is\n // \"this series across all instances, bounded by time\", and the PK leads with\n // instance, which is the wrong first column for that scan.\n await exec(`\n CREATE INDEX IF NOT EXISTS metric_samples_series_recent\n ON ${METRICS_HISTORY_TABLE} (series, at DESC)\n `, []);\n\n // Framework-internal, so the end-user role must not be able to address it.\n //\n // The driver grants `rebase_user` full DML across the schemas a project\n // uses, plus `ALTER DEFAULT PRIVILEGES` so tables created later inherit it —\n // and this one is created later, at boot. Without the revoke, an\n // authenticated request could read and write another deployment's process\n // metrics, and `pnpm rls:check` correctly called that `[critical]\n // rls-disabled` the first time CI saw the table.\n //\n // REVOKE rather than `ENABLE ROW LEVEL SECURITY`, matching every other table\n // in `REBASE_INTERNAL_TABLES`: RLS with no policy denies the same rows but\n // is the weaker statement, because the grant survives and a later policy\n // reopens it. There is no row here any end user should reach, so \"this role\n // has no privilege at all\" is the honest encoding. The owner connection the\n // recorder runs on is unaffected.\n await exec(revokeInternalTableSql(\"rebase\", \"metric_samples\"), []);\n\n // Swept here rather than by a cron, so a deployment that runs no scheduler\n // still stays bounded. A DELETE is the right tool at this size — a few\n // thousand rows a day — and partitioning would be machinery for a table\n // that never gets big.\n await exec(\n `DELETE FROM ${METRICS_HISTORY_TABLE} WHERE at < now() - make_interval(days => $1)`,\n [RETENTION_DAYS]\n );\n}\n\n/**\n * What this process is using right now.\n *\n * `process.cpuUsage()` is cumulative, so a rate needs two readings and the gap\n * between them — which is why the previous one is threaded through rather than\n * held in a module global: a module global is shared by every test in a file\n * and makes the first assertion depend on whatever ran before it.\n */\nexport function sampleSelf(\n previous: { cpu: NodeJS.CpuUsage; at: number } | null,\n now = Date.now(),\n /**\n * Mean event-loop delay over the last window, in milliseconds, or undefined\n * where it cannot be measured. Passed in rather than read here so this\n * function stays pure: the histogram is a stateful handle the recorder owns.\n */\n eventLoopDelayMs?: number\n): { samples: Omit<MetricSample, \"at\">[]; cursor: { cpu: NodeJS.CpuUsage; at: number } } {\n const cpu = process.cpuUsage();\n const memory = process.memoryUsage();\n const samples: Omit<MetricSample, \"at\">[] = [\n { series: \"memory_bytes\", value: memory.rss }\n ];\n\n // Only when it was actually measured. Zero is a real and common reading for\n // an idle process, so a `?? 0` here would be indistinguishable from a\n // healthy one — the same substitution this module rejects everywhere else.\n if (typeof eventLoopDelayMs === \"number\" && Number.isFinite(eventLoopDelayMs)) {\n samples.push({ series: \"event_loop_delay_ms\", value: eventLoopDelayMs });\n }\n\n if (previous) {\n const elapsedMs = now - previous.at;\n if (elapsedMs > 0) {\n // Microseconds of CPU over milliseconds of wall clock, as\n // millicores: 1000m is one core saturated for the whole window.\n const usedMicros = (cpu.user - previous.cpu.user) + (cpu.system - previous.cpu.system);\n samples.push({ series: \"cpu_millicores\", value: (usedMicros / 1000 / elapsedMs) * 1000 });\n }\n }\n\n return { samples, cursor: { cpu, at: now } };\n}\n\n/**\n * Write one tick's samples, for one instance.\n *\n * ## Why `instance` is part of the key\n *\n * A tenant's replicas share one database, and each records its OWN process. Key\n * a row by `(series, minute)` alone and the pods overwrite each other every\n * tick: one pod at 5m and another at 500m leave whichever wrote last, so a\n * scaled-out tenant charts one arbitrary replica and calls it the app. Adding\n * the instance makes each pod its own row, and lets the read decide whether the\n * question is \"the whole deployment\" or \"which pod is hot\".\n *\n * Cardinality stays bounded: rows are series × replicas × minutes, and replicas\n * are capped by the autoscaling ceiling. Six pods is ~43k rows a day and a\n * fortnight of them is well under a million.\n */\nexport async function recordSamples(\n exec: Exec,\n samples: Omit<MetricSample, \"at\">[],\n instance: string,\n at: Date = new Date()\n): Promise<void> {\n if (samples.length === 0) return;\n // Truncated to the minute, so a pod restarting mid-minute overwrites its own\n // earlier row rather than adding a second one for the same instant.\n const bucket = new Date(Math.floor(at.getTime() / 60_000) * 60_000);\n for (const s of samples) {\n if (!Number.isFinite(s.value)) continue;\n await exec(\n `INSERT INTO ${METRICS_HISTORY_TABLE} (at, series, instance, value)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (series, instance, at) DO UPDATE SET value = EXCLUDED.value`,\n [bucket, s.series, instance, s.value]\n );\n }\n}\n\n/**\n * How a series combines across the replicas that reported it.\n *\n * Not one answer for everything, because the right one differs by what the\n * number means. CPU and memory are consumption: the deployment's figure is the\n * sum, and a mean would make scaling out look like it reduced usage. A queue\n * depth or an event-loop delay is a condition each process is independently in,\n * and summing those produces a number no single pod ever experienced.\n */\nconst COMBINE: Record<MetricSeries, \"sum\" | \"avg\"> = {\n cpu_millicores: \"sum\",\n memory_bytes: \"sum\",\n event_loop_delay_ms: \"avg\"\n};\n\nexport interface SeriesPoint {\n at: string;\n /** The deployment's figure, combined per `COMBINE`. */\n value: number;\n /** How many instances reported in this bucket. */\n instances: number;\n}\n\n/**\n * Read one series over a window, oldest first — the order a chart draws in.\n *\n * Combined across instances rather than returned per-pod. A chart of \"this\n * app's CPU\" is the question people ask; \"which pod is hot\" is answered by the\n * live panel, which already lists instances individually and does not need\n * history to do it.\n *\n * `instances` rides along because a sum whose contributor count changed is not\n * comparable with itself: CPU doubling because the app got busy and CPU\n * doubling because it scaled from one replica to two are different events, and\n * a line chart alone cannot tell them apart.\n */\nexport async function readSeries(\n exec: Exec,\n series: MetricSeries,\n sinceMinutes: number\n): Promise<SeriesPoint[]> {\n const combine = COMBINE[series] === \"avg\" ? \"avg\" : \"sum\";\n // The current minute is EXCLUDED, and that is not tidiness.\n //\n // Each replica samples on its own phase — `setInterval` from whenever that\n // pod booted — so at 10:45:20 the bucket for 10:45 holds rows from whichever\n // pods have ticked so far, typically one of three. The chart takes the last\n // point as its headline figure, so a three-replica app displayed one\n // replica's CPU as the deployment's, the line ended in a cliff, and the\n // instance step dropped underneath it — which the chart's own caption\n // explains to the reader as a scale-down that never happened.\n //\n // Every live pod has written bucket M-1 before the clock enters M, so the\n // newest bucket returned is complete and its `instances` is the true\n // contributor count. `date_trunc` rather than arithmetic because it matches\n // the recorder's `floor(t / 60_000) * 60_000` exactly, and is\n // timezone-independent on a timestamptz.\n const result = await exec(\n `SELECT at, ${combine}(value) AS value, count(*) AS instances\n FROM ${METRICS_HISTORY_TABLE}\n WHERE series = $1\n AND at >= now() - make_interval(mins => $2)\n AND at < date_trunc('minute', now())\n GROUP BY at\n ORDER BY at ASC`,\n [series, sinceMinutes]\n );\n\n return sqlRows<RawPoint>(result).map(r => ({\n at: r.at instanceof Date ? r.at.toISOString() : String(r.at),\n value: Number(r.value),\n instances: Number(r.instances ?? 1)\n }));\n}\n\ninterface RawPoint { at: Date | string; value: number; instances?: number | string }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,IAAa,wBAAwB;;AAmBrC,IAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BlC,IAAa,gBAAgB;CACzB;CACA;CACA;AACJ;;;;;;;;AAiBA,eAAsB,qBAAqB,MAA2B;CAClE,MAAM,KAAK;qCACsB,sBAAsB;;;;;;;OAOpD,CAAC,CAAC;CAKL,MAAM,KAAK;;iBAEE,sBAAsB;OAChC,CAAC,CAAC;CAiBL,MAAM,KAAK,uBAAuB,UAAU,gBAAgB,GAAG,CAAC,CAAC;CAMjE,MAAM,KACF,eAAe,sBAAsB,gDACrC,CAAA,EAAe,CACnB;AACJ;;;;;;;;;AAUA,SAAgB,WACZ,UACA,MAAM,KAAK,IAAI,GAMf,kBACqF;CACrF,MAAM,MAAM,QAAQ,SAAS;CAE7B,MAAM,UAAsC,CACxC;EAAE,QAAQ;EAAgB,OAFf,QAAQ,YAEc,CAAA,CAAO;CAAI,CAChD;CAKA,IAAI,OAAO,qBAAqB,YAAY,OAAO,SAAS,gBAAgB,GACxE,QAAQ,KAAK;EAAE,QAAQ;EAAuB,OAAO;CAAiB,CAAC;CAG3E,IAAI,UAAU;EACV,MAAM,YAAY,MAAM,SAAS;EACjC,IAAI,YAAY,GAAG;GAGf,MAAM,aAAc,IAAI,OAAO,SAAS,IAAI,QAAS,IAAI,SAAS,SAAS,IAAI;GAC/E,QAAQ,KAAK;IAAE,QAAQ;IAAkB,OAAQ,aAAa,MAAO,YAAa;GAAK,CAAC;EAC5F;CACJ;CAEA,OAAO;EAAE;EAAS,QAAQ;GAAE;GAAK,IAAI;EAAI;CAAE;AAC/C;;;;;;;;;;;;;;;;;AAkBA,eAAsB,cAClB,MACA,SACA,UACA,qBAAW,IAAI,KAAK,GACP;CACb,IAAI,QAAQ,WAAW,GAAG;CAG1B,MAAM,yBAAS,IAAI,KAAK,KAAK,MAAM,GAAG,QAAQ,IAAI,GAAM,IAAI,GAAM;CAClE,KAAK,MAAM,KAAK,SAAS;EACrB,IAAI,CAAC,OAAO,SAAS,EAAE,KAAK,GAAG;EAC/B,MAAM,KACF,eAAe,sBAAsB;;uFAGrC;GAAC;GAAQ,EAAE;GAAQ;GAAU,EAAE;EAAK,CACxC;CACJ;AACJ;;;;;;;;;;AAWA,IAAM,UAA+C;CACjD,gBAAgB;CAChB,cAAc;CACd,qBAAqB;AACzB;;;;;;;;;;;;;;AAuBA,eAAsB,WAClB,MACA,QACA,cACsB;CA4BtB,OAAO,QAAkB,MAXJ,KACjB,cAjBY,QAAQ,YAAY,QAAQ,QAAQ,MAiB1B;kBACZ,sBAAsB;;;;;4BAMhC,CAAC,QAAQ,YAAY,CACzB,CAE+B,CAAC,CAAC,KAAI,OAAM;EACvC,IAAI,EAAE,cAAc,OAAO,EAAE,GAAG,YAAY,IAAI,OAAO,EAAE,EAAE;EAC3D,OAAO,OAAO,EAAE,KAAK;EACrB,WAAW,OAAO,EAAE,aAAa,CAAC;CACtC,EAAE;AACN"}
1
+ {"version":3,"file":"history-store-By2R_G1b.js","names":[],"sources":["../src/metrics/history-store.ts"],"sourcesContent":["/**\n * A little history for the metrics this process already keeps.\n *\n * ## Why this is in the framework and not in the cloud console\n *\n * The console wants to draw \"CPU over the last hour\". The obvious way to get it\n * on GKE is Cloud Monitoring, which already collects exactly this — and which\n * would make the panel unportable the day the platform moves, for a feature\n * every self-hoster also wants. So the history lives where the runtime lives:\n * the process samples ITSELF, into its OWN database, and anything that can read\n * the database can draw the chart. No cluster, no metrics-server, no vendor.\n *\n * That is the same rule the binder follows — the cloud is a better\n * implementation behind the same interface, never a different one.\n *\n * ## Why it stays cheap\n *\n * One row per series per instance per minute. Three series across two replicas\n * is 8,640 rows a day, and the sweep below bounds the window, so the table settles\n * at a size measured in megabytes. It is deliberately NOT a general time-series\n * store: no labels, no cardinality to explode, no per-request rows.\n *\n * A minute is the resolution because that is what the question needs — \"was it\n * slow at 15:40\", \"did my deploy cause that\" — and because a finer grain buys\n * nothing a reader can see on a chart of an hour.\n */\n/**\n * The positional-parameter shape every store in this package settles on.\n *\n * The bootstrapper's own `SqlExec` takes an options object; each store wraps it\n * once and reads better for it. Same two shapes, same reason, as `job-store`.\n */\nimport { revokeInternalTableSql, sqlRows } from \"@rebasepro/common\";\n\n\nexport type Exec = (sql: string, params?: unknown[]) => Promise<unknown>;\n\n/** Where the samples live. Framework-owned, like `rebase.jobs`. */\nexport const METRICS_HISTORY_TABLE = \"rebase.metric_samples\";\n\n/**\n * How long a sample is kept.\n *\n * Two weeks answers \"is this worse than last week\" and stops well short of\n * being an archive. Anything that needs to outlive it — billing, capacity\n * planning — is a rollup somebody else owns, not a longer retention here.\n *\n * Read it as \"14 days, or since this process started, whichever is longer\": the\n * sweep runs at boot and nowhere else, so a pod up for 90 days holds 90 days.\n * That is a deliberate trade rather than an oversight — a cron for it would be\n * machinery for a table that stays trivial either way, and reads are bounded by\n * the index regardless — but the table does not \"reach a steady size and stay\n * there\" on a long-lived pod, which an earlier version of this comment claimed.\n */\nexport const RETENTION_DAYS = 14;\n\n/** How often the process samples itself. Matched to the resolution it stores. */\nexport const SAMPLE_INTERVAL_MS = 60_000;\n\n/**\n * The series this records. A closed set on purpose — see the cardinality note.\n *\n * A list rather than only a union, because the route validates against it: an\n * unknown `?series=` must be a named 400 rather than an empty chart, which\n * reads exactly like a quiet period.\n *\n * **It lists what `sampleSelf` actually writes, and nothing else.** It used to\n * also name `requests_total`, `errors_total` and `event_loop_delay_ms`, none of\n * which anything ever recorded — so those three were *valid* parameters that\n * returned `points: []`, which is precisely the empty chart the 400 two\n * paragraphs up exists to prevent. A declared-but-unwritten series is worse\n * than an absent one: the 400 tells you the name is wrong, and the empty array\n * tells you the app was quiet.\n *\n * `event_loop_delay_ms` kept its place by gaining a sampler, because it is the\n * one signal here that says whether the process is *healthy* rather than how\n * much it is using, and it has no counter semantics to get wrong.\n *\n * `requests_total` and `errors_total` were dropped rather than wired up. The\n * registry does hold them, but they reset on restart, and a resetting counter\n * summed across replicas is not monotonic — a rolling deploy would draw a\n * cliff that looks like traffic collapsing. That needs a deliberate decision\n * about deltas and resets, not a line added at 2am. Adding either back means\n * adding its sampler in the same commit.\n */\nexport const METRIC_SERIES = [\n \"cpu_millicores\",\n \"memory_bytes\",\n \"event_loop_delay_ms\"\n] as const;\n\nexport type MetricSeries = (typeof METRIC_SERIES)[number];\n\nexport interface MetricSample {\n at: Date;\n series: MetricSeries;\n value: number;\n}\n\n/**\n * Create the table and sweep what has aged out.\n *\n * Called at boot, beside the job and cron stores, and for the same reason they\n * do it there: the only moment the schema is guaranteed to be reachable and\n * nobody is mid-request.\n */\nexport async function ensureMetricsHistory(exec: Exec): Promise<void> {\n await exec(`\n CREATE TABLE IF NOT EXISTS ${METRICS_HISTORY_TABLE} (\n at timestamptz NOT NULL,\n series text NOT NULL,\n instance text NOT NULL,\n value double precision NOT NULL,\n PRIMARY KEY (series, instance, at)\n )\n `, []);\n\n // (series, at DESC) rather than the primary key's order: every read is\n // \"this series across all instances, bounded by time\", and the PK leads with\n // instance, which is the wrong first column for that scan.\n await exec(`\n CREATE INDEX IF NOT EXISTS metric_samples_series_recent\n ON ${METRICS_HISTORY_TABLE} (series, at DESC)\n `, []);\n\n // Framework-internal, so the end-user role must not be able to address it.\n //\n // The driver grants `rebase_user` full DML across the schemas a project\n // uses, plus `ALTER DEFAULT PRIVILEGES` so tables created later inherit it —\n // and this one is created later, at boot. Without the revoke, an\n // authenticated request could read and write another deployment's process\n // metrics, and `pnpm rls:check` correctly called that `[critical]\n // rls-disabled` the first time CI saw the table.\n //\n // REVOKE rather than `ENABLE ROW LEVEL SECURITY`, matching every other table\n // in `REBASE_INTERNAL_TABLES`: RLS with no policy denies the same rows but\n // is the weaker statement, because the grant survives and a later policy\n // reopens it. There is no row here any end user should reach, so \"this role\n // has no privilege at all\" is the honest encoding. The owner connection the\n // recorder runs on is unaffected.\n await exec(revokeInternalTableSql(\"rebase\", \"metric_samples\"), []);\n\n // Swept here rather than by a cron, so a deployment that runs no scheduler\n // still stays bounded. A DELETE is the right tool at this size — a few\n // thousand rows a day — and partitioning would be machinery for a table\n // that never gets big.\n await exec(\n `DELETE FROM ${METRICS_HISTORY_TABLE} WHERE at < now() - make_interval(days => $1)`,\n [RETENTION_DAYS]\n );\n}\n\n/**\n * What this process is using right now.\n *\n * `process.cpuUsage()` is cumulative, so a rate needs two readings and the gap\n * between them — which is why the previous one is threaded through rather than\n * held in a module global: a module global is shared by every test in a file\n * and makes the first assertion depend on whatever ran before it.\n */\nexport function sampleSelf(\n previous: { cpu: NodeJS.CpuUsage; at: number } | null,\n now = Date.now(),\n /**\n * Mean event-loop delay over the last window, in milliseconds, or undefined\n * where it cannot be measured. Passed in rather than read here so this\n * function stays pure: the histogram is a stateful handle the recorder owns.\n */\n eventLoopDelayMs?: number\n): { samples: Omit<MetricSample, \"at\">[]; cursor: { cpu: NodeJS.CpuUsage; at: number } } {\n const cpu = process.cpuUsage();\n const memory = process.memoryUsage();\n const samples: Omit<MetricSample, \"at\">[] = [\n { series: \"memory_bytes\", value: memory.rss }\n ];\n\n // Only when it was actually measured. Zero is a real and common reading for\n // an idle process, so a `?? 0` here would be indistinguishable from a\n // healthy one — the same substitution this module rejects everywhere else.\n if (typeof eventLoopDelayMs === \"number\" && Number.isFinite(eventLoopDelayMs)) {\n samples.push({ series: \"event_loop_delay_ms\", value: eventLoopDelayMs });\n }\n\n if (previous) {\n const elapsedMs = now - previous.at;\n if (elapsedMs > 0) {\n // Microseconds of CPU over milliseconds of wall clock, as\n // millicores: 1000m is one core saturated for the whole window.\n const usedMicros = (cpu.user - previous.cpu.user) + (cpu.system - previous.cpu.system);\n samples.push({ series: \"cpu_millicores\", value: (usedMicros / 1000 / elapsedMs) * 1000 });\n }\n }\n\n return { samples, cursor: { cpu, at: now } };\n}\n\n/**\n * Write one tick's samples, for one instance.\n *\n * ## Why `instance` is part of the key\n *\n * A tenant's replicas share one database, and each records its OWN process. Key\n * a row by `(series, minute)` alone and the pods overwrite each other every\n * tick: one pod at 5m and another at 500m leave whichever wrote last, so a\n * scaled-out tenant charts one arbitrary replica and calls it the app. Adding\n * the instance makes each pod its own row, and lets the read decide whether the\n * question is \"the whole deployment\" or \"which pod is hot\".\n *\n * Cardinality stays bounded: rows are series × replicas × minutes, and replicas\n * are capped by the autoscaling ceiling. Six pods is ~43k rows a day and a\n * fortnight of them is well under a million.\n */\nexport async function recordSamples(\n exec: Exec,\n samples: Omit<MetricSample, \"at\">[],\n instance: string,\n at: Date = new Date()\n): Promise<void> {\n if (samples.length === 0) return;\n // Truncated to the minute, so a pod restarting mid-minute overwrites its own\n // earlier row rather than adding a second one for the same instant.\n const bucket = new Date(Math.floor(at.getTime() / 60_000) * 60_000);\n for (const s of samples) {\n if (!Number.isFinite(s.value)) continue;\n await exec(\n `INSERT INTO ${METRICS_HISTORY_TABLE} (at, series, instance, value)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (series, instance, at) DO UPDATE SET value = EXCLUDED.value`,\n [bucket, s.series, instance, s.value]\n );\n }\n}\n\n/**\n * How a series combines across the replicas that reported it.\n *\n * Not one answer for everything, because the right one differs by what the\n * number means. CPU and memory are consumption: the deployment's figure is the\n * sum, and a mean would make scaling out look like it reduced usage. A queue\n * depth or an event-loop delay is a condition each process is independently in,\n * and summing those produces a number no single pod ever experienced.\n */\nconst COMBINE: Record<MetricSeries, \"sum\" | \"avg\"> = {\n cpu_millicores: \"sum\",\n memory_bytes: \"sum\",\n event_loop_delay_ms: \"avg\"\n};\n\nexport interface SeriesPoint {\n at: string;\n /** The deployment's figure, combined per `COMBINE`. */\n value: number;\n /** How many instances reported in this bucket. */\n instances: number;\n}\n\n/**\n * Read one series over a window, oldest first — the order a chart draws in.\n *\n * Combined across instances rather than returned per-pod. A chart of \"this\n * app's CPU\" is the question people ask; \"which pod is hot\" is answered by the\n * live panel, which already lists instances individually and does not need\n * history to do it.\n *\n * `instances` rides along because a sum whose contributor count changed is not\n * comparable with itself: CPU doubling because the app got busy and CPU\n * doubling because it scaled from one replica to two are different events, and\n * a line chart alone cannot tell them apart.\n */\nexport async function readSeries(\n exec: Exec,\n series: MetricSeries,\n sinceMinutes: number\n): Promise<SeriesPoint[]> {\n const combine = COMBINE[series] === \"avg\" ? \"avg\" : \"sum\";\n // The current minute is EXCLUDED, and that is not tidiness.\n //\n // Each replica samples on its own phase — `setInterval` from whenever that\n // pod booted — so at 10:45:20 the bucket for 10:45 holds rows from whichever\n // pods have ticked so far, typically one of three. The chart takes the last\n // point as its headline figure, so a three-replica app displayed one\n // replica's CPU as the deployment's, the line ended in a cliff, and the\n // instance step dropped underneath it — which the chart's own caption\n // explains to the reader as a scale-down that never happened.\n //\n // Every live pod has written bucket M-1 before the clock enters M, so the\n // newest bucket returned is complete and its `instances` is the true\n // contributor count. `date_trunc` rather than arithmetic because it matches\n // the recorder's `floor(t / 60_000) * 60_000` exactly, and is\n // timezone-independent on a timestamptz.\n const result = await exec(\n `SELECT at, ${combine}(value) AS value, count(*) AS instances\n FROM ${METRICS_HISTORY_TABLE}\n WHERE series = $1\n AND at >= now() - make_interval(mins => $2)\n AND at < date_trunc('minute', now())\n GROUP BY at\n ORDER BY at ASC`,\n [series, sinceMinutes]\n );\n\n return sqlRows<RawPoint>(result).map(r => ({\n at: r.at instanceof Date ? r.at.toISOString() : String(r.at),\n value: Number(r.value),\n instances: Number(r.instances ?? 1)\n }));\n}\n\ninterface RawPoint { at: Date | string; value: number; instances?: number | string }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,IAAa,wBAAwB;;AAmBrC,IAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BlC,IAAa,gBAAgB;CACzB;CACA;CACA;AACJ;;;;;;;;AAiBA,eAAsB,qBAAqB,MAA2B;CAClE,MAAM,KAAK;qCACsB,sBAAsB;;;;;;;OAOpD,CAAC,CAAC;CAKL,MAAM,KAAK;;iBAEE,sBAAsB;OAChC,CAAC,CAAC;CAiBL,MAAM,KAAK,uBAAuB,UAAU,gBAAgB,GAAG,CAAC,CAAC;CAMjE,MAAM,KACF,eAAe,sBAAsB,gDACrC,CAAA,EAAe,CACnB;AACJ;;;;;;;;;AAUA,SAAgB,WACZ,UACA,MAAM,KAAK,IAAI,GAMf,kBACqF;CACrF,MAAM,MAAM,QAAQ,SAAS;CAE7B,MAAM,UAAsC,CACxC;EAAE,QAAQ;EAAgB,OAFf,QAAQ,YAEc,CAAA,CAAO;CAAI,CAChD;CAKA,IAAI,OAAO,qBAAqB,YAAY,OAAO,SAAS,gBAAgB,GACxE,QAAQ,KAAK;EAAE,QAAQ;EAAuB,OAAO;CAAiB,CAAC;CAG3E,IAAI,UAAU;EACV,MAAM,YAAY,MAAM,SAAS;EACjC,IAAI,YAAY,GAAG;GAGf,MAAM,aAAc,IAAI,OAAO,SAAS,IAAI,QAAS,IAAI,SAAS,SAAS,IAAI;GAC/E,QAAQ,KAAK;IAAE,QAAQ;IAAkB,OAAQ,aAAa,MAAO,YAAa;GAAK,CAAC;EAC5F;CACJ;CAEA,OAAO;EAAE;EAAS,QAAQ;GAAE;GAAK,IAAI;EAAI;CAAE;AAC/C;;;;;;;;;;;;;;;;;AAkBA,eAAsB,cAClB,MACA,SACA,UACA,qBAAW,IAAI,KAAK,GACP;CACb,IAAI,QAAQ,WAAW,GAAG;CAG1B,MAAM,yBAAS,IAAI,KAAK,KAAK,MAAM,GAAG,QAAQ,IAAI,GAAM,IAAI,GAAM;CAClE,KAAK,MAAM,KAAK,SAAS;EACrB,IAAI,CAAC,OAAO,SAAS,EAAE,KAAK,GAAG;EAC/B,MAAM,KACF,eAAe,sBAAsB;;uFAGrC;GAAC;GAAQ,EAAE;GAAQ;GAAU,EAAE;EAAK,CACxC;CACJ;AACJ;;;;;;;;;;AAWA,IAAM,UAA+C;CACjD,gBAAgB;CAChB,cAAc;CACd,qBAAqB;AACzB;;;;;;;;;;;;;;AAuBA,eAAsB,WAClB,MACA,QACA,cACsB;CA4BtB,OAAO,QAAkB,MAXJ,KACjB,cAjBY,QAAQ,YAAY,QAAQ,QAAQ,MAiB1B;kBACZ,sBAAsB;;;;;4BAMhC,CAAC,QAAQ,YAAY,CACzB,CAE+B,CAAC,CAAC,KAAI,OAAM;EACvC,IAAI,EAAE,cAAc,OAAO,EAAE,GAAG,YAAY,IAAI,OAAO,EAAE,EAAE;EAC3D,OAAO,OAAO,EAAE,KAAK;EACrB,WAAW,OAAO,EAAE,aAAa,CAAC;CACtC,EAAE;AACN"}
package/dist/index.d.ts CHANGED
@@ -87,5 +87,5 @@ export { MetricsRegistry, createMetricsMiddleware, createMetricsRoutes, classify
87
87
  export type { MetricSurface, MetricsHandle } from "./metrics/index.js";
88
88
  export { createContractRoutes } from "./api/contract-routes.js";
89
89
  export type { ContractRoutesConfig } from "./api/contract-routes.js";
90
- export { RUNTIME_HEALTH_PATH, RUNTIME_LIVENESS_PATH, RUNTIME_PROBE_PATHS, TOPOLOGY_ENV_VARS, isTopologyEnvVar, RUNTIME_PRESTOP_DRAIN_SECONDS, RUNTIME_STARTUP_BUDGET_SECONDS, RUNTIME_MIN_TERMINATION_GRACE_SECONDS, RUNTIME_BUNDLE_MOUNT } from "./deploy/pod-contract.js";
90
+ export { RUNTIME_HEALTH_PATH, RUNTIME_LIVENESS_PATH, RUNTIME_PROBE_PATHS, TOPOLOGY_ENV_VARS, isTopologyEnvVar, RUNTIME_PRESTOP_DRAIN_SECONDS, RUNTIME_STARTUP_BUDGET_SECONDS, RUNTIME_MIN_TERMINATION_GRACE_SECONDS, RUNTIME_BUNDLE_MOUNT, RUNTIME_DEFAULT_MAX_BODY_SIZE } from "./deploy/pod-contract.js";
91
91
  export type { TopologyEnvVar } from "./deploy/pod-contract.js";