@camstack/system 1.2.317 → 1.2.319

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 (75) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +3 -77
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +3 -77
  11. package/dist/builtins/camera-grid/index.js +1 -1
  12. package/dist/builtins/camera-grid/index.mjs +1 -1
  13. package/dist/builtins/composer/composer-apply-waits.d.ts +21 -0
  14. package/dist/builtins/composer/composer-live-set.d.ts +22 -0
  15. package/dist/builtins/composer/composer-runtime-factory.d.ts +22 -0
  16. package/dist/builtins/composer/composer-seeds.d.ts +22 -0
  17. package/dist/builtins/composer/composer.addon.js +1905 -1809
  18. package/dist/builtins/composer/composer.addon.mjs +1905 -1809
  19. package/dist/builtins/composer/composer.d.ts +3 -15
  20. package/dist/builtins/console-logging/index.js +1 -1
  21. package/dist/builtins/console-logging/index.mjs +1 -1
  22. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  23. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  24. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  25. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  26. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  27. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  28. package/dist/builtins/hub-forwarder/index.js +1 -1
  29. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  30. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  31. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  32. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  33. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  34. package/dist/builtins/local-network/local-network.addon.js +1 -1
  35. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  36. package/dist/builtins/loki-logging/index.js +1 -1
  37. package/dist/builtins/loki-logging/index.mjs +1 -1
  38. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  39. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  40. package/dist/builtins/platform-probe/index.js +1 -1
  41. package/dist/builtins/platform-probe/index.mjs +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  43. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/snapshot/index.js +1 -1
  45. package/dist/builtins/snapshot/index.mjs +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  47. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/shm-lock-canary.d.ts +60 -0
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +1 -0
  50. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  51. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  52. package/dist/builtins/sqlite-storage/wal-checkpoint-policy.d.ts +115 -34
  53. package/dist/builtins/sqlite-storage/wal-checkpointer.d.ts +6 -0
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  55. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  56. package/dist/builtins/system-backup/system-backup.service.d.ts +2 -17
  57. package/dist/builtins/system-config/system-config.addon.js +1 -1
  58. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  59. package/dist/builtins/winston-logging/index.js +1 -1
  60. package/dist/builtins/winston-logging/index.mjs +1 -1
  61. package/dist/{composition-sources-Dep7nBwu.js → composition-sources-BjRPhWtU.js} +1 -1
  62. package/dist/{composition-sources-DVClu006.mjs → composition-sources-Cg2MP-qf.mjs} +1 -1
  63. package/dist/{dist-Dori4pf3.mjs → dist-CQ9vXSUh.mjs} +135 -2
  64. package/dist/{dist-a6p0V-IL.js → dist-Dcy0gVHu.js} +135 -2
  65. package/dist/index.js +1 -1
  66. package/dist/index.mjs +1 -1
  67. package/dist/{retired-settings-keys-DVcKRgzm.mjs → retired-settings-keys-DNIbyfTR.mjs} +1 -1
  68. package/dist/{retired-settings-keys-DC1uHWPZ.js → retired-settings-keys-pzlrLUbA.js} +1 -1
  69. package/dist/wal-checkpoint-policy-BahrTtyD.mjs +186 -0
  70. package/dist/wal-checkpoint-policy-d2z2qPqx.js +233 -0
  71. package/dist/wal-checkpoint-worker.js +63 -57
  72. package/dist/wal-checkpoint-worker.mjs +63 -57
  73. package/package.json +1 -1
  74. package/dist/wal-checkpoint-policy-CAcg63o-.mjs +0 -92
  75. package/dist/wal-checkpoint-policy-CCJZngds.js +0 -127
@@ -0,0 +1,233 @@
1
+ //#region src/builtins/sqlite-storage/wal-checkpoint-policy.ts
2
+ /**
3
+ * When the checkpoint worker folds the WAL — the judgement, pure.
4
+ *
5
+ * ## Why a worker checkpoints at all
6
+ *
7
+ * `hub/sqlite-settings` is one JS thread with a synchronous binding, and every
8
+ * configuration read in the cluster queues behind whatever statement it is
9
+ * inside. With `journal_mode = WAL` and the build default `synchronous =
10
+ * NORMAL`, an ordinary commit is an append to the WAL with NO fsync. The
11
+ * fsyncs — the only thing in a one-row write that can take seconds — happen
12
+ * at the CHECKPOINT: the WAL is synced before the pages are copied, the
13
+ * database file after. And SQLite runs the auto-checkpoint (1000 pages)
14
+ * INSIDE the commit of whichever writer crosses the threshold.
15
+ *
16
+ * Measured on the hub, 2026-09-11 10:45–10:55 CEST, from outside the process
17
+ * (`/proc/<pid>/syscall` at 10 Hz + the wal-index header at 1 Hz, D452): the
18
+ * WAL refilled at ~54 frames/s, the auto-checkpoint fired every 12–25 s (42
19
+ * in 9.5 min), and every D-state run on the settings thread that sat in a
20
+ * syscall sat in `fsync` (74), each within ±2 s of a checkpoint event —
21
+ * 12.6 s of the 569 s window, all of it inside writers' commits. In the two
22
+ * saturated bursts earlier that morning (08:35–08:40, 09:30–09:39) the same
23
+ * fsync took 1–10 s, once per checkpoint, and the profiler filed each as a
24
+ * slow `set`. `synchronous` was never the lever: it was already NORMAL, and
25
+ * `sqlite-pragmas.ts` had said so since 2026-08-08.
26
+ *
27
+ * So the checkpoint runs on a worker thread, over its own connection. The
28
+ * settings thread's commits stay appends. What stays on the settings thread
29
+ * is the WAL-header sync SQLite performs when a WAL is reused after a
30
+ * completed checkpoint — one small fsync per cycle — and that residual is
31
+ * named here so nobody reads "no fsync on the settings thread" into this.
32
+ *
33
+ * ## The rule this file reproduces
34
+ *
35
+ * The auto-checkpoint's own rule: fold when 1000 frames are pending. The
36
+ * point of moving the checkpoint is to change WHO waits on the disk, not how
37
+ * often the disk is asked to sync — a worker that folded every second would
38
+ * multiply the fsync count on a drive the media plane already saturates.
39
+ *
40
+ * ## How the worker knows how much is pending — and how it must NOT learn it
41
+ *
42
+ * Until 2026-09-29 the worker read the wal-index header straight out of the
43
+ * `-shm` file, once a second, with `openSync` / `readSync` / `closeSync`. That
44
+ * `closeSync` was the SIGBUS of D468 (corrected by D679): on POSIX, closing
45
+ * ANY descriptor of a file drops EVERY fcntl lock the process holds on that
46
+ * file — including the shared lock on `-shm` byte 128 (the "DMS" lock) that
47
+ * SQLite takes for the life of a connection and that tells every OTHER
48
+ * process "a live connection is using this wal-index, do not rebuild it". With
49
+ * the lock gone, the next process to open the database believed it was
50
+ * first, truncated and re-initialised the `-shm` under the runner's mappings,
51
+ * and the runner's next write past a 32 KiB wal-index region hit a page
52
+ * beyond end-of-file.
53
+ *
54
+ * So the worker never touches a SQLite file except through its connection.
55
+ * What it knows about the WAL it learns from the checkpoint it just ran:
56
+ * `PRAGMA wal_checkpoint(PASSIVE)` answers `log` (frames in the WAL) and
57
+ * `checkpointed` (frames now backfilled). Two consecutive answers give the
58
+ * frames appended between them, hence a rate, hence WHEN the next 1000 frames
59
+ * will have accumulated. The next checkpoint is scheduled for then —
60
+ * {@link nextCheckpointPlan} — clamped between the worker's poll interval and
61
+ * {@link WAL_CHECKPOINT_MAX_DELAY_MS}. A PASSIVE with nothing to fold performs
62
+ * no fsync (the copy and both syncs sit behind `nBackfill < mxFrame` in
63
+ * `wal.c`), so the clamp costs a quiet database nothing, and it bounds how
64
+ * long a burst that follows a quiet spell goes unseen.
65
+ */
66
+ /** The engine's own `wal_autocheckpoint` default, in frames. Reproduced, not changed. */
67
+ var WAL_CHECKPOINT_THRESHOLD_FRAMES = 1e3;
68
+ /**
69
+ * Longest the worker waits between two checkpoints, in ms, whatever the
70
+ * measured rate says. It is the bound on how late a burst after a steady or
71
+ * quiet spell is noticed, and it is sized against the engine's own fallback
72
+ * (`SQLITE_WAL_AUTOCHECKPOINT_BOUND_PAGES` = 10 000 frames), which runs INSIDE
73
+ * a settings-thread commit — the stall D452 moved off that thread. No burst
74
+ * rate has been measured on the hub (D452's 54 frames/s is the steady rate),
75
+ * so the design point is 1 000 frames/s, ~18× steady: 8 s of it is 8 000
76
+ * frames, under the fallback, so the worker looks before the engine has to.
77
+ * A pass that finds more than {@link WAL_CHECKPOINT_LARGE_PASS_FRAMES} is
78
+ * reported so this number can be tuned from the field.
79
+ *
80
+ * The cost, stated against D452: at the steady 54 frames/s the threshold
81
+ * would fold every ~18.5 s; this clamp folds every 8 s, ~2.3× the fsync pairs
82
+ * (≈ 432 frames per fold instead of 1 000). They are paid on the WORKER, a
83
+ * thread nobody waits on — the settings thread's commits stay appends. A pass
84
+ * with nothing pending does no fsync at all.
85
+ */
86
+ var WAL_CHECKPOINT_MAX_DELAY_MS = 8e3;
87
+ /**
88
+ * A pass that folded more than this many appended frames is reported (WARN,
89
+ * sampled: the first of each summary window, the rest counted). Half the
90
+ * engine's fallback bound: a gap that let this much accumulate was within 2×
91
+ * of handing the checkpoint back to the settings thread.
92
+ */
93
+ var WAL_CHECKPOINT_LARGE_PASS_FRAMES = 5e3;
94
+ /** Shortest gap between two checkpoints, in ms — the worker's poll interval. */
95
+ var WAL_CHECKPOINT_POLL_MS = 1e3;
96
+ /**
97
+ * A checkpoint that took this long is reported at WARN with its numbers.
98
+ * Matched to `SQLITE_SLOW_CALL_MS`: it is the same "the disk held us" event
99
+ * the profiler used to file as a slow `set`, now on the thread that paid it.
100
+ */
101
+ var WAL_CHECKPOINT_SLOW_MS = 1e3;
102
+ /**
103
+ * Frames appended to the WAL between two checkpoints.
104
+ *
105
+ * When the previous checkpoint backfilled EVERYTHING, the next writer restarts
106
+ * the WAL from frame 1, so every frame now in it is new. When it did not (a
107
+ * reader pinned a snapshot), the WAL kept growing and the difference is what
108
+ * was added — unless it shrank, which only a restart can do.
109
+ *
110
+ * An IDLE WAL is the exception to "complete means restarted": the WAL restarts
111
+ * only at the next write, so a fully checkpointed WAL nobody writes answers
112
+ * the same `log == checkpointed` pass after pass. Identical answers after a
113
+ * complete checkpoint are therefore 0 appended, not the whole WAL again —
114
+ * otherwise an idle node would be scheduled at the poll interval forever and
115
+ * every summary would count the same frames once per pass. (A restart that
116
+ * appended exactly as many frames as the previous cycle is read as idle too;
117
+ * it is scheduled at the max delay, which is the bounded case anyway.)
118
+ *
119
+ * The one remaining ambiguity — a complete checkpoint followed by writes the
120
+ * writer could NOT restart the WAL for — counts old frames as new, which
121
+ * over-estimates the rate and checkpoints EARLY: the safe direction. `-1`
122
+ * (not a WAL database, or a busy pass) reads as 0.
123
+ */
124
+ function framesAppendedBetween(previous, current) {
125
+ const prevWal = Math.max(0, previous.walFrames);
126
+ const prevDone = Math.max(0, previous.checkpointedFrames);
127
+ const curWal = Math.max(0, current.walFrames);
128
+ const curDone = Math.max(0, current.checkpointedFrames);
129
+ if (prevDone >= prevWal) {
130
+ if (curWal === prevWal && curDone === prevDone) return 0;
131
+ return curWal;
132
+ }
133
+ return curWal >= prevWal ? curWal - prevWal : curWal;
134
+ }
135
+ /**
136
+ * What the worker "saw" before its first checkpoint: nothing, at the moment it
137
+ * started. Against it, the first checkpoint counts every frame then in the WAL
138
+ * as appended since the start — which over-estimates the rate when the WAL
139
+ * held older frames, and so schedules the second checkpoint EARLY: the safe
140
+ * direction, and the second one measures the real rate.
141
+ */
142
+ function workerStartObservation(at) {
143
+ return {
144
+ at,
145
+ walFrames: 0,
146
+ checkpointedFrames: 0
147
+ };
148
+ }
149
+ /**
150
+ * When to run the next checkpoint: the moment the measured rate says
151
+ * {@link WAL_CHECKPOINT_THRESHOLD_FRAMES} more frames will be pending,
152
+ * clamped to `[minDelayMs, maxDelayMs]`. Nothing appended since the last one
153
+ * is `maxDelayMs`.
154
+ */
155
+ function nextCheckpointPlan(input) {
156
+ const { previous, current, minDelayMs, maxDelayMs } = input;
157
+ const appendedFrames = framesAppendedBetween(previous, current);
158
+ const elapsedMs = Math.max(1, current.at - previous.at);
159
+ if (appendedFrames === 0) return {
160
+ appendedFrames,
161
+ nextDelayMs: maxDelayMs
162
+ };
163
+ const untilThresholdMs = WAL_CHECKPOINT_THRESHOLD_FRAMES / (appendedFrames / elapsedMs);
164
+ return {
165
+ appendedFrames,
166
+ nextDelayMs: Math.round(Math.min(maxDelayMs, Math.max(minDelayMs, untilThresholdMs)))
167
+ };
168
+ }
169
+ function isRecord(value) {
170
+ return typeof value === "object" && value !== null;
171
+ }
172
+ /** Narrow a message off the worker port without a cast. */
173
+ function isWorkerMessage(value) {
174
+ if (!isRecord(value)) return false;
175
+ switch (value["kind"]) {
176
+ case "ready": return true;
177
+ case "failed": return typeof value["error"] === "string";
178
+ case "checkpointed": return (value["reason"] === "first" || value["reason"] === "scheduled") && typeof value["appendedFrames"] === "number" && typeof value["nextDelayMs"] === "number" && typeof value["walFrames"] === "number" && typeof value["checkpointedFrames"] === "number" && typeof value["busy"] === "boolean" && typeof value["ms"] === "number";
179
+ default: return false;
180
+ }
181
+ }
182
+ function isWorkerStop(value) {
183
+ return isRecord(value) && value["kind"] === "stop";
184
+ }
185
+ //#endregion
186
+ Object.defineProperty(exports, "WAL_CHECKPOINT_LARGE_PASS_FRAMES", {
187
+ enumerable: true,
188
+ get: function() {
189
+ return WAL_CHECKPOINT_LARGE_PASS_FRAMES;
190
+ }
191
+ });
192
+ Object.defineProperty(exports, "WAL_CHECKPOINT_MAX_DELAY_MS", {
193
+ enumerable: true,
194
+ get: function() {
195
+ return WAL_CHECKPOINT_MAX_DELAY_MS;
196
+ }
197
+ });
198
+ Object.defineProperty(exports, "WAL_CHECKPOINT_POLL_MS", {
199
+ enumerable: true,
200
+ get: function() {
201
+ return WAL_CHECKPOINT_POLL_MS;
202
+ }
203
+ });
204
+ Object.defineProperty(exports, "WAL_CHECKPOINT_SLOW_MS", {
205
+ enumerable: true,
206
+ get: function() {
207
+ return WAL_CHECKPOINT_SLOW_MS;
208
+ }
209
+ });
210
+ Object.defineProperty(exports, "isWorkerMessage", {
211
+ enumerable: true,
212
+ get: function() {
213
+ return isWorkerMessage;
214
+ }
215
+ });
216
+ Object.defineProperty(exports, "isWorkerStop", {
217
+ enumerable: true,
218
+ get: function() {
219
+ return isWorkerStop;
220
+ }
221
+ });
222
+ Object.defineProperty(exports, "nextCheckpointPlan", {
223
+ enumerable: true,
224
+ get: function() {
225
+ return nextCheckpointPlan;
226
+ }
227
+ });
228
+ Object.defineProperty(exports, "workerStartObservation", {
229
+ enumerable: true,
230
+ get: function() {
231
+ return workerStartObservation;
232
+ }
233
+ });
@@ -1,6 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_wal_checkpoint_policy = require("./wal-checkpoint-policy-CCJZngds.js");
3
- let node_fs = require("node:fs");
2
+ const require_wal_checkpoint_policy = require("./wal-checkpoint-policy-d2z2qPqx.js");
4
3
  let better_sqlite3 = require("better-sqlite3");
5
4
  better_sqlite3 = require_chunk.__toESM(better_sqlite3);
6
5
  let node_worker_threads = require("node:worker_threads");
@@ -22,9 +21,15 @@ let node_worker_threads = require("node:worker_threads");
22
21
  * writers and readers are never blocked by this thread. (The idle-gated
23
22
  * TRUNCATE in `wal-maintenance.ts` is a different job — reclaiming a
24
23
  * sidecar that outgrew its limit — and stays where it is.)
25
- * - The wal-index header is read as bytes (`-shm`, 100 bytes, no lock) so the
26
- * poll costs nothing; when it cannot be read the policy falls back to a
27
- * fixed interval, so nothing here can turn "unreadable" into "nothing to do".
24
+ * - It NEVER touches a SQLite file except through that connection — no
25
+ * `open`/`read`/`close` of the `.db`, `-wal` or `-shm`. Until 2026-09-29 it
26
+ * read the wal-index header out of `-shm` once a second, and each
27
+ * `closeSync` dropped every fcntl lock this PROCESS held on the file —
28
+ * SQLite's DMS lock included — which let the nightly backup's connection
29
+ * rebuild the `-shm` under the runner's mappings: the SIGBUS of D468,
30
+ * diagnosed in D679. `scripts/check-sqlite-files-not-opened-by-fs.ts`
31
+ * refuses the pattern. The schedule is derived from the checkpoints'
32
+ * own answers instead (`nextCheckpointPlan`).
28
33
  * - Every checkpoint is reported to the parent with its numbers. The parent
29
34
  * decides what is worth a line.
30
35
  *
@@ -48,31 +53,8 @@ function readCheckpointRow(value) {
48
53
  }
49
54
  function isWorkerData(value) {
50
55
  if (typeof value !== "object" || value === null) return false;
51
- if (!("dbPath" in value) || !("pollMs" in value)) return false;
52
- return typeof value.dbPath === "string" && typeof value.pollMs === "number";
53
- }
54
- /** The two wal-index counters, or nulls when the sidecar cannot be read. */
55
- function readWalIndex(shmPath) {
56
- let fd = null;
57
- try {
58
- fd = (0, node_fs.openSync)(shmPath, "r");
59
- const buffer = Buffer.alloc(100);
60
- if ((0, node_fs.readSync)(fd, buffer, 0, 100, 0) < 100) return {
61
- mxFrame: null,
62
- nBackfill: null
63
- };
64
- return require_wal_checkpoint_policy.parseWalIndexHeader(buffer) ?? {
65
- mxFrame: null,
66
- nBackfill: null
67
- };
68
- } catch {
69
- return {
70
- mxFrame: null,
71
- nBackfill: null
72
- };
73
- } finally {
74
- if (fd !== null) (0, node_fs.closeSync)(fd);
75
- }
56
+ if (!("dbPath" in value) || !("pollMs" in value) || !("maxDelayMs" in value)) return false;
57
+ return typeof value.dbPath === "string" && typeof value.pollMs === "number" && typeof value.maxDelayMs === "number";
76
58
  }
77
59
  function post(message) {
78
60
  node_worker_threads.parentPort?.postMessage(message);
@@ -81,45 +63,69 @@ function run() {
81
63
  const port = node_worker_threads.parentPort;
82
64
  if (port === null) throw new Error("wal-checkpoint-worker: no parent port");
83
65
  if (!isWorkerData(node_worker_threads.workerData)) throw new Error("wal-checkpoint-worker: bad workerData");
84
- const { dbPath, pollMs } = node_worker_threads.workerData;
85
- const shmPath = `${dbPath}-shm`;
66
+ const { dbPath, pollMs, maxDelayMs } = node_worker_threads.workerData;
86
67
  const db = new better_sqlite3.default(dbPath);
87
68
  db.pragma("cache_size = -2048");
88
69
  db.pragma("mmap_size = 0");
89
- let lastRunAt = Date.now();
70
+ let previous = require_wal_checkpoint_policy.workerStartObservation(Date.now());
71
+ let first = true;
90
72
  let timer = null;
91
73
  let stopping = false;
92
74
  const tick = () => {
93
75
  if (stopping) return;
94
- const decision = require_wal_checkpoint_policy.decideCheckpoint({
95
- ...readWalIndex(shmPath),
96
- sinceLastRunMs: Date.now() - lastRunAt
97
- });
98
- if (decision.run) {
99
- const startedAt = Date.now();
100
- let raw;
101
- try {
102
- raw = db.pragma("wal_checkpoint(PASSIVE)");
103
- } catch (err) {
104
- post({
105
- kind: "failed",
106
- error: err instanceof Error ? err.message : String(err)
107
- });
108
- raw = null;
109
- }
110
- lastRunAt = Date.now();
111
- const row = readCheckpointRow(raw);
112
- if (row !== null) post({
76
+ const startedAt = Date.now();
77
+ let raw = null;
78
+ try {
79
+ raw = db.pragma("wal_checkpoint(PASSIVE)");
80
+ } catch (err) {
81
+ post({
82
+ kind: "failed",
83
+ error: err instanceof Error ? err.message : String(err)
84
+ });
85
+ }
86
+ const row = readCheckpointRow(raw);
87
+ if (row === null) {
88
+ timer = setTimeout(tick, maxDelayMs);
89
+ return;
90
+ }
91
+ if (row.busy !== 0) {
92
+ post({
113
93
  kind: "checkpointed",
114
- reason: decision.reason,
115
- pendingFrames: decision.pendingFrames,
94
+ reason: first ? "first" : "scheduled",
95
+ appendedFrames: 0,
96
+ nextDelayMs: pollMs,
116
97
  walFrames: row.log,
117
98
  checkpointedFrames: row.checkpointed,
118
- busy: row.busy !== 0,
119
- ms: lastRunAt - startedAt
99
+ busy: true,
100
+ ms: Date.now() - startedAt
120
101
  });
102
+ timer = setTimeout(tick, pollMs);
103
+ return;
121
104
  }
122
- timer = setTimeout(tick, pollMs);
105
+ const current = {
106
+ at: Date.now(),
107
+ walFrames: row.log,
108
+ checkpointedFrames: row.checkpointed
109
+ };
110
+ const plan = require_wal_checkpoint_policy.nextCheckpointPlan({
111
+ previous,
112
+ current,
113
+ minDelayMs: pollMs,
114
+ maxDelayMs
115
+ });
116
+ post({
117
+ kind: "checkpointed",
118
+ reason: first ? "first" : "scheduled",
119
+ appendedFrames: plan.appendedFrames,
120
+ nextDelayMs: plan.nextDelayMs,
121
+ walFrames: row.log,
122
+ checkpointedFrames: row.checkpointed,
123
+ busy: row.busy !== 0,
124
+ ms: current.at - startedAt
125
+ });
126
+ previous = current;
127
+ first = false;
128
+ timer = setTimeout(tick, plan.nextDelayMs);
123
129
  };
124
130
  port.on("message", (message) => {
125
131
  if (!require_wal_checkpoint_policy.isWorkerStop(message)) return;
@@ -1,5 +1,4 @@
1
- import { a as isWorkerStop, o as parseWalIndexHeader, r as decideCheckpoint } from "./wal-checkpoint-policy-CAcg63o-.mjs";
2
- import { closeSync, openSync, readSync } from "node:fs";
1
+ import { c as workerStartObservation, o as isWorkerStop, s as nextCheckpointPlan } from "./wal-checkpoint-policy-BahrTtyD.mjs";
3
2
  import Database from "better-sqlite3";
4
3
  import { parentPort, workerData } from "node:worker_threads";
5
4
  //#region src/builtins/sqlite-storage/wal-checkpoint-worker.ts
@@ -20,9 +19,15 @@ import { parentPort, workerData } from "node:worker_threads";
20
19
  * writers and readers are never blocked by this thread. (The idle-gated
21
20
  * TRUNCATE in `wal-maintenance.ts` is a different job — reclaiming a
22
21
  * sidecar that outgrew its limit — and stays where it is.)
23
- * - The wal-index header is read as bytes (`-shm`, 100 bytes, no lock) so the
24
- * poll costs nothing; when it cannot be read the policy falls back to a
25
- * fixed interval, so nothing here can turn "unreadable" into "nothing to do".
22
+ * - It NEVER touches a SQLite file except through that connection — no
23
+ * `open`/`read`/`close` of the `.db`, `-wal` or `-shm`. Until 2026-09-29 it
24
+ * read the wal-index header out of `-shm` once a second, and each
25
+ * `closeSync` dropped every fcntl lock this PROCESS held on the file —
26
+ * SQLite's DMS lock included — which let the nightly backup's connection
27
+ * rebuild the `-shm` under the runner's mappings: the SIGBUS of D468,
28
+ * diagnosed in D679. `scripts/check-sqlite-files-not-opened-by-fs.ts`
29
+ * refuses the pattern. The schedule is derived from the checkpoints'
30
+ * own answers instead (`nextCheckpointPlan`).
26
31
  * - Every checkpoint is reported to the parent with its numbers. The parent
27
32
  * decides what is worth a line.
28
33
  *
@@ -46,31 +51,8 @@ function readCheckpointRow(value) {
46
51
  }
47
52
  function isWorkerData(value) {
48
53
  if (typeof value !== "object" || value === null) return false;
49
- if (!("dbPath" in value) || !("pollMs" in value)) return false;
50
- return typeof value.dbPath === "string" && typeof value.pollMs === "number";
51
- }
52
- /** The two wal-index counters, or nulls when the sidecar cannot be read. */
53
- function readWalIndex(shmPath) {
54
- let fd = null;
55
- try {
56
- fd = openSync(shmPath, "r");
57
- const buffer = Buffer.alloc(100);
58
- if (readSync(fd, buffer, 0, 100, 0) < 100) return {
59
- mxFrame: null,
60
- nBackfill: null
61
- };
62
- return parseWalIndexHeader(buffer) ?? {
63
- mxFrame: null,
64
- nBackfill: null
65
- };
66
- } catch {
67
- return {
68
- mxFrame: null,
69
- nBackfill: null
70
- };
71
- } finally {
72
- if (fd !== null) closeSync(fd);
73
- }
54
+ if (!("dbPath" in value) || !("pollMs" in value) || !("maxDelayMs" in value)) return false;
55
+ return typeof value.dbPath === "string" && typeof value.pollMs === "number" && typeof value.maxDelayMs === "number";
74
56
  }
75
57
  function post(message) {
76
58
  parentPort?.postMessage(message);
@@ -79,45 +61,69 @@ function run() {
79
61
  const port = parentPort;
80
62
  if (port === null) throw new Error("wal-checkpoint-worker: no parent port");
81
63
  if (!isWorkerData(workerData)) throw new Error("wal-checkpoint-worker: bad workerData");
82
- const { dbPath, pollMs } = workerData;
83
- const shmPath = `${dbPath}-shm`;
64
+ const { dbPath, pollMs, maxDelayMs } = workerData;
84
65
  const db = new Database(dbPath);
85
66
  db.pragma("cache_size = -2048");
86
67
  db.pragma("mmap_size = 0");
87
- let lastRunAt = Date.now();
68
+ let previous = workerStartObservation(Date.now());
69
+ let first = true;
88
70
  let timer = null;
89
71
  let stopping = false;
90
72
  const tick = () => {
91
73
  if (stopping) return;
92
- const decision = decideCheckpoint({
93
- ...readWalIndex(shmPath),
94
- sinceLastRunMs: Date.now() - lastRunAt
95
- });
96
- if (decision.run) {
97
- const startedAt = Date.now();
98
- let raw;
99
- try {
100
- raw = db.pragma("wal_checkpoint(PASSIVE)");
101
- } catch (err) {
102
- post({
103
- kind: "failed",
104
- error: err instanceof Error ? err.message : String(err)
105
- });
106
- raw = null;
107
- }
108
- lastRunAt = Date.now();
109
- const row = readCheckpointRow(raw);
110
- if (row !== null) post({
74
+ const startedAt = Date.now();
75
+ let raw = null;
76
+ try {
77
+ raw = db.pragma("wal_checkpoint(PASSIVE)");
78
+ } catch (err) {
79
+ post({
80
+ kind: "failed",
81
+ error: err instanceof Error ? err.message : String(err)
82
+ });
83
+ }
84
+ const row = readCheckpointRow(raw);
85
+ if (row === null) {
86
+ timer = setTimeout(tick, maxDelayMs);
87
+ return;
88
+ }
89
+ if (row.busy !== 0) {
90
+ post({
111
91
  kind: "checkpointed",
112
- reason: decision.reason,
113
- pendingFrames: decision.pendingFrames,
92
+ reason: first ? "first" : "scheduled",
93
+ appendedFrames: 0,
94
+ nextDelayMs: pollMs,
114
95
  walFrames: row.log,
115
96
  checkpointedFrames: row.checkpointed,
116
- busy: row.busy !== 0,
117
- ms: lastRunAt - startedAt
97
+ busy: true,
98
+ ms: Date.now() - startedAt
118
99
  });
100
+ timer = setTimeout(tick, pollMs);
101
+ return;
119
102
  }
120
- timer = setTimeout(tick, pollMs);
103
+ const current = {
104
+ at: Date.now(),
105
+ walFrames: row.log,
106
+ checkpointedFrames: row.checkpointed
107
+ };
108
+ const plan = nextCheckpointPlan({
109
+ previous,
110
+ current,
111
+ minDelayMs: pollMs,
112
+ maxDelayMs
113
+ });
114
+ post({
115
+ kind: "checkpointed",
116
+ reason: first ? "first" : "scheduled",
117
+ appendedFrames: plan.appendedFrames,
118
+ nextDelayMs: plan.nextDelayMs,
119
+ walFrames: row.log,
120
+ checkpointedFrames: row.checkpointed,
121
+ busy: row.busy !== 0,
122
+ ms: current.at - startedAt
123
+ });
124
+ previous = current;
125
+ first = false;
126
+ timer = setTimeout(tick, plan.nextDelayMs);
121
127
  };
122
128
  port.on("message", (message) => {
123
129
  if (!isWorkerStop(message)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.317",
3
+ "version": "1.2.319",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",
@@ -1,92 +0,0 @@
1
- /**
2
- * Longest the worker lets pending frames sit before folding them, in ms. A
3
- * quiet database (a developer machine, an agent with little to persist) still
4
- * gets its WAL folded and its pages made durable within a minute.
5
- */
6
- var WAL_CHECKPOINT_MAX_INTERVAL_MS = 6e4;
7
- /** How often the worker looks at the wal-index, in ms. A 136-byte read. */
8
- var WAL_CHECKPOINT_POLL_MS = 1e3;
9
- /**
10
- * A checkpoint that took this long is reported at WARN with its numbers.
11
- * Matched to `SQLITE_SLOW_CALL_MS`: it is the same "the disk held us" event
12
- * the profiler used to file as a slow `set`, now on the thread that paid it.
13
- */
14
- var WAL_CHECKPOINT_SLOW_MS = 1e3;
15
- /** Should the worker run `wal_checkpoint(PASSIVE)` now? */
16
- function decideCheckpoint(input) {
17
- const intervalDue = input.sinceLastRunMs >= WAL_CHECKPOINT_MAX_INTERVAL_MS;
18
- if (input.mxFrame === null || input.nBackfill === null) return intervalDue ? {
19
- run: true,
20
- reason: "unknown-interval",
21
- pendingFrames: null
22
- } : {
23
- run: false,
24
- reason: "unknown-below-interval",
25
- pendingFrames: null
26
- };
27
- const pendingFrames = Math.max(0, input.mxFrame - input.nBackfill);
28
- if (pendingFrames === 0) return {
29
- run: false,
30
- reason: "nothing-pending",
31
- pendingFrames
32
- };
33
- if (pendingFrames >= 1e3) return {
34
- run: true,
35
- reason: "threshold",
36
- pendingFrames
37
- };
38
- if (intervalDue) return {
39
- run: true,
40
- reason: "interval",
41
- pendingFrames
42
- };
43
- return {
44
- run: false,
45
- reason: "below-threshold",
46
- pendingFrames
47
- };
48
- }
49
- /**
50
- * Byte offsets in the `-shm` file, from `wal.c` ("WAL-index file format"):
51
- * two copies of the 48-byte `WalIndexHdr` (copy 2 at 48), then `WalCkptInfo`
52
- * at 96 whose first field is `nBackfill`. `mxFrame` is at byte 16 of the
53
- * header. Both are u32 in the writer's native byte order — little-endian on
54
- * every x86-64 and arm64 node this ships to.
55
- */
56
- var WAL_INDEX_MXFRAME_OFFSET = 16;
57
- var WAL_INDEX_HDR_SIZE = 48;
58
- var WAL_INDEX_NBACKFILL_OFFSET = 96;
59
- /**
60
- * `mxFrame` and `nBackfill` from the first {@link WAL_INDEX_HEADER_READ_BYTES}
61
- * of the `-shm` file, or `null` when the buffer is short or the two header
62
- * copies disagree (a writer was updating them — SQLite's own readers retry on
63
- * exactly that comparison; this caller simply waits for the next poll).
64
- */
65
- function parseWalIndexHeader(buffer) {
66
- if (buffer.length < 100) return null;
67
- const copy1 = buffer.subarray(0, WAL_INDEX_HDR_SIZE);
68
- const copy2 = buffer.subarray(WAL_INDEX_HDR_SIZE, WAL_INDEX_HDR_SIZE * 2);
69
- if (!copy1.equals(copy2)) return null;
70
- return {
71
- mxFrame: buffer.readUInt32LE(WAL_INDEX_MXFRAME_OFFSET),
72
- nBackfill: buffer.readUInt32LE(WAL_INDEX_NBACKFILL_OFFSET)
73
- };
74
- }
75
- function isRecord(value) {
76
- return typeof value === "object" && value !== null;
77
- }
78
- /** Narrow a message off the worker port without a cast. */
79
- function isWorkerMessage(value) {
80
- if (!isRecord(value)) return false;
81
- switch (value["kind"]) {
82
- case "ready": return true;
83
- case "failed": return typeof value["error"] === "string";
84
- case "checkpointed": return typeof value["reason"] === "string" && (typeof value["pendingFrames"] === "number" || value["pendingFrames"] === null) && typeof value["walFrames"] === "number" && typeof value["checkpointedFrames"] === "number" && typeof value["busy"] === "boolean" && typeof value["ms"] === "number";
85
- default: return false;
86
- }
87
- }
88
- function isWorkerStop(value) {
89
- return isRecord(value) && value["kind"] === "stop";
90
- }
91
- //#endregion
92
- export { isWorkerStop as a, isWorkerMessage as i, WAL_CHECKPOINT_SLOW_MS as n, parseWalIndexHeader as o, decideCheckpoint as r, WAL_CHECKPOINT_POLL_MS as t };