@camstack/system 1.2.127 → 1.2.129

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.
@@ -116,3 +116,29 @@ export declare const SQLITE_PAGE_CACHE_CEILING_KIB = -262144;
116
116
  export declare function sqliteMmapSizeFor(fileSizeBytes: number): number;
117
117
  /** The `cache_size` to apply for a database of this size, in KiB (negative). */
118
118
  export declare function sqlitePageCacheKibFor(fileSizeBytes: number): number;
119
+ /** Page counters read straight off the connection, in the units SQLite reports. */
120
+ export interface SqlitePageCounters {
121
+ readonly pageCount: number;
122
+ readonly freelistCount: number;
123
+ readonly pageSizeBytes: number;
124
+ }
125
+ /** How much of the file holds rows, and how much is free pages awaiting reuse. */
126
+ export interface SqliteOccupancy {
127
+ readonly liveMb: number;
128
+ readonly freeMb: number;
129
+ readonly freePct: number;
130
+ }
131
+ /**
132
+ * Split a database file into live bytes and free bytes.
133
+ *
134
+ * A file that stopped growing is not the same as a file that is full. SQLite
135
+ * reuses freed pages but never returns them to the filesystem without a
136
+ * `VACUUM`, so a database sitting at its high-water mark looks — from `stat`,
137
+ * from `df`, from any measurement outside the connection — exactly like one
138
+ * genuinely packed with rows. The two call for opposite actions: the first is
139
+ * reclaimed by a VACUUM, the second means retention is not keeping up.
140
+ *
141
+ * `dbSizeMb` on the boot line could not tell them apart. This can, and it costs
142
+ * three header reads on a connection that is already open.
143
+ */
144
+ export declare function sqliteOccupancy(counters: SqlitePageCounters): SqliteOccupancy;
@@ -218,6 +218,23 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
218
218
  * unreadable `stat`, and both callers treat it as "unknown" and fall back to
219
219
  * the floor — never to a smaller setting than the one already argued for.
220
220
  */
221
+ /**
222
+ * One numeric PRAGMA off the open connection. A pragma that does not answer
223
+ * with a number yields 0, which `sqliteOccupancy` reads as "unknown" and
224
+ * reports as an empty file rather than inventing a ratio.
225
+ */
226
+ /**
227
+ * Name the biggest tables, once, a minute after the connection opens.
228
+ *
229
+ * `dbSizeMb` says how big the file is and `freePct` says how much of it is
230
+ * live; neither says WHOSE. Per-addon scoped collections are created on first
231
+ * write and declared nowhere, so this is the only place the real set can be
232
+ * enumerated. `unref` so it can never hold the process open, and
233
+ * `CAMSTACK_SQLITE_STORAGE_REPORT=off` turns it off without a rebuild.
234
+ */
235
+ private scheduleStorageReport;
236
+ private logStorageReport;
237
+ private readPragmaNumber;
221
238
  private measureDbSizeBytes;
222
239
  private getDb;
223
240
  /**
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Which collection IS the database.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * The boot line already says how big the file is (`dbSizeMb`) and how much of
7
+ * it is real (`freePct`). Measured on the live hub 2026-08-25 those read
8
+ * `dbSizeMb=858 liveMb=840 freeMb=18 freePct=2` — so the growth from 153 MB
9
+ * (08-08) is not a high-water mark waiting for a VACUUM, it is live rows. That
10
+ * turned "should we VACUUM" into "which table", and nothing in this system
11
+ * could answer it.
12
+ *
13
+ * Nothing could, structurally: per-addon scoped collections
14
+ * (`<addonId>:<canonical>`) are created on the first write and declared in no
15
+ * source file, so the real set only exists on the running node. A reference
16
+ * page cannot list them and a grep cannot find them.
17
+ *
18
+ * ## Why `dbstat` and not `COUNT(*)`
19
+ *
20
+ * `dbstat` reports the pages a table actually occupies, which is the question —
21
+ * a table with few enormous rows and a table with many small ones are the same
22
+ * problem when the file is the thing that grew. It walks the b-tree structure
23
+ * without decoding row payloads, so it costs a page traversal rather than a
24
+ * full table scan. It is still real work on an 840 MB file, which is why the
25
+ * caller runs it ONCE, well after boot, off the critical path — never on the
26
+ * path a config read waits behind.
27
+ */
28
+ /** One `dbstat` group: a table (or index) and the space it occupies. */
29
+ export interface TableSizeRow {
30
+ readonly name: string;
31
+ readonly bytes: number;
32
+ readonly pages: number;
33
+ }
34
+ /** One line of the report. */
35
+ export interface TableSizeEntry {
36
+ readonly name: string;
37
+ readonly sizeMb: number;
38
+ readonly pages: number;
39
+ /** Share of the whole file, percent. 0 when the file size is unknown. */
40
+ readonly pct: number;
41
+ }
42
+ export interface TableSizeReport {
43
+ readonly tables: readonly TableSizeEntry[];
44
+ /** Megabytes held by everything below the cut. */
45
+ readonly otherMb: number;
46
+ /** How many tables are behind `otherMb`. */
47
+ readonly otherTables: number;
48
+ }
49
+ /**
50
+ * Rank tables by the space they hold and keep the top `limit`.
51
+ *
52
+ * The tail is REPORTED, never dropped: a top-N that silently discards the rest
53
+ * reads as "these N are the database", which is exactly the wrong conclusion
54
+ * when the remainder is a third of the file.
55
+ */
56
+ export declare function summariseTableSizes(rows: readonly TableSizeRow[], totalBytes: number, limit: number): TableSizeReport;
57
+ /**
58
+ * How long after opening the connection the report runs.
59
+ *
60
+ * Not zero and not "on every boot forever at startup": boot is already the
61
+ * busiest window this engine sees (`ensureTable` x9, the retired-key purge, the
62
+ * vector registry), and a page traversal of an 840 MB file placed there would
63
+ * be indistinguishable from the stall this whole subsystem was just fixed for.
64
+ * Sixty seconds puts it after the storm, once per process.
65
+ */
66
+ export declare const STORAGE_REPORT_DELAY_MS = 60000;
67
+ /** Top-N tables named individually; the remainder is reported as a tail. */
68
+ export declare const STORAGE_REPORT_TOP_N = 10;
69
+ /** `dbstat` shape, narrowed without a cast. */
70
+ export declare function toTableSizeRow(value: unknown): TableSizeRow | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.127",
3
+ "version": "1.2.129",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",