@camstack/system 1.2.424 → 1.2.426
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.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-databases.d.ts +51 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +19 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +268 -17
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +268 -17
- package/dist/builtins/backup-orchestrator/manifest-store.d.ts +23 -3
- package/dist/builtins/backup-orchestrator/run-queue.d.ts +3 -0
- package/dist/builtins/backup-orchestrator/schedule-store.d.ts +7 -1
- package/dist/builtins/backup-orchestrator/seed-marker-store.d.ts +34 -0
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +3 -3
- package/dist/builtins/keychain/index.mjs +3 -3
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
- package/dist/builtins/local-network/address-book.d.ts +21 -0
- package/dist/builtins/local-network/local-network.addon.d.ts +6 -0
- package/dist/builtins/local-network/local-network.addon.js +48 -7
- package/dist/builtins/local-network/local-network.addon.mjs +48 -7
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/database-snapshot-provider.d.ts +18 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +49 -7
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +49 -7
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-backup/system-backup.service.d.ts +48 -15
- package/dist/builtins/system-config/system-config.addon.js +2 -2
- package/dist/builtins/system-config/system-config.addon.mjs +2 -2
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BWNs-ZeH.js → composition-sources-60tuHQ9N.js} +1 -1
- package/dist/{composition-sources-tRG7nKWK.mjs → composition-sources-gxVpghXh.mjs} +1 -1
- package/dist/{constants-DHhhVjkt.mjs → constants-B6YTf0v2.mjs} +1 -1
- package/dist/{constants-BBWZlnpQ.js → constants-CO5zpJQY.js} +1 -1
- package/dist/{dist-B-NmfvVC.mjs → dist-CGz9u2HD.mjs} +869 -800
- package/dist/{dist-CiUKtVkX.js → dist-K3WFop9Q.js} +869 -800
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/index.js +272 -198
- package/dist/index.mjs +272 -198
- package/dist/{jwt-signing-key-nwRpRgf_.js → jwt-signing-key-BdDy9ZJN.js} +1 -1
- package/dist/{jwt-signing-key-CUpweD22.mjs → jwt-signing-key-C_w5ielw.mjs} +1 -1
- package/dist/kernel/moleculer/process-service.d.ts +1 -110
- package/dist/kernel/moleculer/runner-heap-flags.d.ts +228 -0
- package/dist/{restore-archive-DA-aJtg7.js → restore-archive-BJp4Llea.js} +156 -65
- package/dist/{restore-archive-CFmPVlWQ.mjs → restore-archive-CZyUM8Qf.mjs} +151 -66
- package/dist/{retired-settings-keys-D62kN_hT.js → retired-settings-keys-Dr2AyhsD.js} +1 -1
- package/dist/{retired-settings-keys-DCGEI4Vi.mjs → retired-settings-keys-Dut6QWh6.mjs} +1 -1
- package/dist/{wal-maintenance-BlqNqaQ3.mjs → wal-maintenance-BZhzoxqA.mjs} +0 -0
- package/dist/{wal-maintenance-BzdCQGjH.js → wal-maintenance-Bzm3O2Nj.js} +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { AddonHeapProfile } from "@camstack/types";
|
|
2
|
+
/** One addon hosted inside a runner subprocess. */
|
|
3
|
+
export interface RunnerAddonSpec {
|
|
4
|
+
readonly addonId: string;
|
|
5
|
+
readonly addonDir: string;
|
|
6
|
+
}
|
|
7
|
+
/** The memory declarations one addon's manifest makes about its own runner. */
|
|
8
|
+
export interface AddonHeapDeclaration {
|
|
9
|
+
readonly profile: AddonHeapProfile | undefined;
|
|
10
|
+
/** `execution.maxOldSpaceMb`, when declared. `0` means "this addon opts out of a ceiling". */
|
|
11
|
+
readonly maxOldSpaceMb: number | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* `execution.rssBudgetMb`, when declared — the whole-process footprint the addon's role
|
|
14
|
+
* considers legitimate. `undefined` means the addon declares no budget, which is NOT a
|
|
15
|
+
* request for a default: a runner where nobody declares one reports itself UNWATCHED.
|
|
16
|
+
*/
|
|
17
|
+
readonly rssBudgetMb: number | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* `execution.semiSpaceMb`, when declared and inside {@link SEMI_SPACE_MB_RANGE} — the
|
|
20
|
+
* young-generation semi-space the addon measured its runner needs. See `AddonExecution`.
|
|
21
|
+
*/
|
|
22
|
+
readonly semiSpaceMb: number | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* `execution.initialOldSpaceMb`, when declared and inside {@link INITIAL_OLD_SPACE_MB_RANGE}
|
|
25
|
+
* — the old-space headroom the runner starts with. See `AddonExecution`.
|
|
26
|
+
*/
|
|
27
|
+
readonly initialOldSpaceMb: number | undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Inclusive MB bounds a manifest value must sit in to be a declaration at all. */
|
|
30
|
+
export interface ManifestMbRange {
|
|
31
|
+
readonly min: number;
|
|
32
|
+
readonly max: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `execution.semiSpaceMb` is accepted in `[1, 256]` MB. V8 itself clamps the flag to its
|
|
36
|
+
* own limits (the 64-bit default is 64 MB after Node 24's scavenger change, a 256 MB
|
|
37
|
+
* semi-space is three times anything measured useful here); below 1 MB the flag is
|
|
38
|
+
* meaningless. Outside the range the declaration is IGNORED (the runner keeps its class
|
|
39
|
+
* default) rather than clamped: a clamped number silently becomes a different setting from
|
|
40
|
+
* the one in the file, and the spawn log would then say something the manifest does not.
|
|
41
|
+
*/
|
|
42
|
+
export declare const SEMI_SPACE_MB_RANGE: ManifestMbRange;
|
|
43
|
+
/**
|
|
44
|
+
* `execution.initialOldSpaceMb` is accepted in `[16, 65536]` MB. Below 16 MB V8's default
|
|
45
|
+
* already starts larger; 64 GB clears any runner in this fleet. The accepted value is then
|
|
46
|
+
* clamped at spawn to the runner's old-space CEILING (`runnerHeapFlags`) — V8 refuses an
|
|
47
|
+
* initial size above `--max-old-space-size`, and the ceiling is the one number that belongs
|
|
48
|
+
* to the runner as a whole.
|
|
49
|
+
*/
|
|
50
|
+
export declare const INITIAL_OLD_SPACE_MB_RANGE: ManifestMbRange;
|
|
51
|
+
export declare function readAddonHeapDeclaration(spec: RunnerAddonSpec): AddonHeapDeclaration;
|
|
52
|
+
/**
|
|
53
|
+
* Drop the cached heap declaration for each addon so the NEXT `readAddonHeapDeclaration`
|
|
54
|
+
* re-reads the manifest from disk. Called on operator-driven spawns / restarts
|
|
55
|
+
* (a deploy may have changed `execution.heapProfile` / `execution.maxOldSpaceMb`); the
|
|
56
|
+
* crash-respawn fast path deliberately does NOT invalidate — a crashing runner
|
|
57
|
+
* has the same on-disk manifest, so re-reading it is pure overhead.
|
|
58
|
+
*/
|
|
59
|
+
export declare function invalidateHeapProfiles(addons: readonly RunnerAddonSpec[]): void;
|
|
60
|
+
/**
|
|
61
|
+
* A runner is heavy if ANY co-located addon declares `heapProfile: 'heavy'` in its manifest.
|
|
62
|
+
* Everything else (the un-annotated default) is a light control-plane runner. No hard-coded names.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isHeavyRunner(addons: readonly RunnerAddonSpec[]): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Default old-space ceiling for a HEAVY runner, in MB.
|
|
67
|
+
*
|
|
68
|
+
* ## Why a heavy runner needs a ceiling at all
|
|
69
|
+
*
|
|
70
|
+
* `heapProfile: 'heavy'` used to mean "V8 defaults", i.e. no `--max-old-space-size` — which on this
|
|
71
|
+
* host is V8's own memory-derived default (multiple GB). Nothing then bounds a burst, and V8 does
|
|
72
|
+
* not hand committed pages back (the long argument for that is in `kernel/heap-watch.ts`), so a
|
|
73
|
+
* one-off burst becomes the runner's permanent RSS.
|
|
74
|
+
*
|
|
75
|
+
* Measured on the live hub, 2026-08-17: the `recorder` runner reaches **2122 MB RSS** and stays
|
|
76
|
+
* there. The same addon on two agent nodes carrying the same code and a smaller archive sits at
|
|
77
|
+
* **58 MB**.
|
|
78
|
+
*
|
|
79
|
+
* ⚠ This paragraph used to attribute that ~2 GB to "walk garbage" from the deferred archive walk,
|
|
80
|
+
* and to argue from a ~21 MB `RecordingIndex`. Both were wrong, and the error cost a session on
|
|
81
|
+
* 2026-08-27. The deferred walk had NO CALLER and never ran (it has since been deleted). And the
|
|
82
|
+
* index is not 21 MB: a 2026-08-27 profile of the live recorder read `heapUsed = 1606 MB` against
|
|
83
|
+
* `external = 8 MB`, with an `smaps` census putting 1510 MB in V8's 256 KB page ranges against
|
|
84
|
+
* 54 MB of native heap — LIVE JavaScript retainers, not high-water garbage. The retained set is
|
|
85
|
+
* ~7.1 M `SegmentRow`s plus their path strings and Map entries (13 cameras x 2 profiles x 8 640
|
|
86
|
+
* segments/day x ~30 days), reseeded at boot by `SegmentHourLedger.hydrateIndex`.
|
|
87
|
+
*
|
|
88
|
+
* The lesson for anyone sizing this flag: a heap number needs a RETAINER, and "garbage the GC has
|
|
89
|
+
* no reason to collect" is the explanation to reach for last, not first.
|
|
90
|
+
*
|
|
91
|
+
* ## Why 1024
|
|
92
|
+
*
|
|
93
|
+
* From the same live topology snapshot, every heavy runner's FULL RSS — which is a strict
|
|
94
|
+
* over-estimate of its old space, since it also contains native allocations, `external` and
|
|
95
|
+
* `arrayBuffers`, none of which `--max-old-space-size` bounds:
|
|
96
|
+
*
|
|
97
|
+
* detection 471 MB · provider-reolink 507 MB · pipeline-orchestrator 238 MB · ml 218 MB ·
|
|
98
|
+
* snapshot 185 MB · decoder-nodeav 174 MB · audio-analyzer 170 MB
|
|
99
|
+
*
|
|
100
|
+
* 1024 MB clears the largest of those by ~2x while cutting the recorder's observed ratchet by
|
|
101
|
+
* more than half. It is a CEILING, not a target: a runner that fits today keeps behaving exactly
|
|
102
|
+
* as it does now, because V8 only collects harder as it approaches the limit.
|
|
103
|
+
*
|
|
104
|
+
* A runner whose working set genuinely does not fit declares its own number in its manifest
|
|
105
|
+
* (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
|
|
106
|
+
* `recorder` (3072 MB; peak 2341 MB heapTotal) is the one shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
|
|
107
|
+
* `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
|
|
108
|
+
* space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
|
|
109
|
+
* from RSS.
|
|
110
|
+
*
|
|
111
|
+
* ## What this flag CANNOT do
|
|
112
|
+
*
|
|
113
|
+
* It bounds old space and nothing else — not `external`, not `arrayBuffers`, not what a native
|
|
114
|
+
* addon mallocs. `hub/pipeline-analytics` reached 7334 MB RSS with this flag set to 1024 the
|
|
115
|
+
* whole time, because 96% of it was native. That half is bounded in `runner-native-allocator.ts`.
|
|
116
|
+
* Reaching for a bigger number here when RSS is the complaint is the standard wrong move.
|
|
117
|
+
*/
|
|
118
|
+
export declare const HEAVY_MAX_OLD_MB_DEFAULT = 1024;
|
|
119
|
+
/**
|
|
120
|
+
* V8 platform worker threads for a LIGHT runner (`--v8-pool-size`).
|
|
121
|
+
*
|
|
122
|
+
* ## What this bounds
|
|
123
|
+
*
|
|
124
|
+
* Node gives every process a V8 platform thread pool — background compilation,
|
|
125
|
+
* parallel marking and sweeping — defaulted to 4. It is not the libuv pool
|
|
126
|
+
* (`UV_THREADPOOL_SIZE`, which serves `fs`/`dns`/`zlib`/`crypto`) and it is
|
|
127
|
+
* deliberately not touched here; see below.
|
|
128
|
+
*
|
|
129
|
+
* ## Why it is worth a flag
|
|
130
|
+
*
|
|
131
|
+
* A container cgroup counts THREADS, not processes, and this cluster forks a
|
|
132
|
+
* runner per addon (D2). Measured on the live hub 2026-09-18 (20 cores,
|
|
133
|
+
* `pids.max=2048`), after 22 fork refusals in 45 minutes earlier that day:
|
|
134
|
+
*
|
|
135
|
+
* pids.current 991 · 47 node processes holding 469 threads
|
|
136
|
+
* of those, 29 LIGHT runners holding 274 (mean 9.4 threads each)
|
|
137
|
+
*
|
|
138
|
+
* A light runner is a control-plane addon with a 2 MB young generation. It does
|
|
139
|
+
* not generate the background GC work four marking threads exist for, and it
|
|
140
|
+
* pays 4 threads of the cgroup's pid budget for them anyway. Two returns ~58
|
|
141
|
+
* threads — ~6% of the container's live thread count — for background GC that
|
|
142
|
+
* is, on these heaps, a few milliseconds a cycle.
|
|
143
|
+
*
|
|
144
|
+
* ## Why TWO and never ZERO
|
|
145
|
+
*
|
|
146
|
+
* `--v8-pool-size=0` does not disable the pool: it tells V8 to size it from the
|
|
147
|
+
* detected parallelism. Measured locally 2026-09-18 on a node process holding
|
|
148
|
+
* the libuv pool open:
|
|
149
|
+
*
|
|
150
|
+
* default 11 threads
|
|
151
|
+
* --v8-pool-size=2 9 threads
|
|
152
|
+
* --v8-pool-size=0 18 threads
|
|
153
|
+
*
|
|
154
|
+
* So the flag is only ever passed with a value of at least 1 — the same trap as
|
|
155
|
+
* `MALLOC_ARENA_MAX=0` in `runner-native-allocator.ts`.
|
|
156
|
+
*
|
|
157
|
+
* ## What is deliberately NOT bounded
|
|
158
|
+
*
|
|
159
|
+
* HEAVY runners keep V8's default: they hold real heaps (the recorder declares
|
|
160
|
+
* 3072 MB) where parallel marking is the difference between a pause and a stall.
|
|
161
|
+
*
|
|
162
|
+
* `UV_THREADPOOL_SIZE` is left alone on every runner. Its 4 threads are lazily
|
|
163
|
+
* spawned, and the measurement above says most light runners have spawned all
|
|
164
|
+
* four — i.e. they really do run concurrent `fs` work. Halving that would trade
|
|
165
|
+
* ~58 more threads for serialized filesystem I/O in runners whose I/O profile
|
|
166
|
+
* has not been measured, on a media system. A thread saving is not worth a
|
|
167
|
+
* media gap.
|
|
168
|
+
*/
|
|
169
|
+
export declare const LIGHT_V8_POOL_SIZE_DEFAULT = 2;
|
|
170
|
+
/** Light runners' young-generation semi-space, in MB. */
|
|
171
|
+
export declare const LIGHT_SEMI_SPACE_MB = 2;
|
|
172
|
+
/**
|
|
173
|
+
* The runner's old-space ceiling in MB, or 0 for "uncapped".
|
|
174
|
+
*
|
|
175
|
+
* The runner takes the MAXIMUM ceiling any co-located addon declares — co-location must never
|
|
176
|
+
* silence the hungriest member (D2/D29: a runner is respawned as a whole, so an OOM caused by
|
|
177
|
+
* under-sizing one member kills every addon in the group). An addon declaring `0` therefore lifts
|
|
178
|
+
* the cap for the whole runner, which is the intended escape hatch.
|
|
179
|
+
*/
|
|
180
|
+
export declare function runnerMaxOldSpaceMb(addons: readonly RunnerAddonSpec[], heavy: boolean): number;
|
|
181
|
+
/**
|
|
182
|
+
* The runner's declared RSS budget in MB, or `undefined` when NOBODY declared one.
|
|
183
|
+
*
|
|
184
|
+
* Same rule as the old-space ceiling, for the same reason (D2/D29): the runner takes the MAXIMUM
|
|
185
|
+
* any co-located addon declares, because a runner is respawned as a whole and co-location must
|
|
186
|
+
* never silence the hungriest member. It would be strictly worse here than for a heap ceiling —
|
|
187
|
+
* an under-sized budget does not kill anything, it just cries wolf, and a watchdog that cries
|
|
188
|
+
* wolf is a watchdog that gets muted.
|
|
189
|
+
*
|
|
190
|
+
* `undefined` propagates as an ABSENT env var, and the child then announces itself unwatched.
|
|
191
|
+
* There is deliberately no class default: see `AddonExecution.rssBudgetMb`.
|
|
192
|
+
*/
|
|
193
|
+
export declare function runnerRssBudgetMb(addons: readonly RunnerAddonSpec[]): number | undefined;
|
|
194
|
+
/**
|
|
195
|
+
* Per-placement V8 flags (Block D). LIGHT runners cap their young generation
|
|
196
|
+
* (`--max-semi-space-size`) to reclaim V8's over-reservation — a measured ~15 MB/runner win — bound
|
|
197
|
+
* V8's platform thread pool (`--v8-pool-size`, see {@link LIGHT_V8_POOL_SIZE_DEFAULT}: a measured
|
|
198
|
+
* ~58 threads across the hub's 29 light runners, against a cgroup `pids.max` this host has already
|
|
199
|
+
* hit) and carry no old-space cap unless a manifest declares one. HEAVY
|
|
200
|
+
* runners keep V8's default young generation but ALWAYS carry an old-space ceiling; see
|
|
201
|
+
* {@link HEAVY_MAX_OLD_MB_DEFAULT} for why, and how a runner declares its own.
|
|
202
|
+
*
|
|
203
|
+
* The per-addon overrides are the manifest's declared `maxOldSpaceMb` (`0` = uncapped),
|
|
204
|
+
* `semiSpaceMb` (`--max-semi-space-size`, replacing the class default) and
|
|
205
|
+
* `initialOldSpaceMb` (`--initial-old-space-size`, clamped to the ceiling) — each the MAXIMUM
|
|
206
|
+
* across a runner's co-located addons. Shipped for `stream-broker` alone (8 / 512): with the
|
|
207
|
+
* heavy class default the hub's broker ran ~166 major GCs a minute and not one scavenge, at
|
|
208
|
+
* 45–60 % of a core. The environment overrides and the `CAMSTACK_RUNNER_HEAP_TUNING=off` kill
|
|
209
|
+
* switch were removed on 2026-10-06 — nothing had set them since the rollout. The flags must
|
|
210
|
+
* be on the spawn command line: `v8.setFlagsFromString` at runtime changes neither space.
|
|
211
|
+
*/
|
|
212
|
+
export declare function runnerHeapFlags(addons: readonly RunnerAddonSpec[]): string[];
|
|
213
|
+
/**
|
|
214
|
+
* The runner's declared young-generation semi-space in MB, or `undefined` when no
|
|
215
|
+
* co-located addon declares one (→ the class default applies).
|
|
216
|
+
*
|
|
217
|
+
* The MAXIMUM across the group, like every other declaration here (D2/D29): the
|
|
218
|
+
* member that measured its churn away must not be handed a smaller young
|
|
219
|
+
* generation because a quieter co-tenant declared one.
|
|
220
|
+
*/
|
|
221
|
+
export declare function runnerSemiSpaceMb(addons: readonly RunnerAddonSpec[]): number | undefined;
|
|
222
|
+
/**
|
|
223
|
+
* The runner's declared initial old space in MB, or `undefined` when nobody declares
|
|
224
|
+
* one (→ V8 grows it from the live set). MAXIMUM across the group, same rule and same
|
|
225
|
+
* reason as {@link runnerSemiSpaceMb}; the clamp to the runner's ceiling happens in
|
|
226
|
+
* {@link runnerHeapFlags}, where the ceiling is known.
|
|
227
|
+
*/
|
|
228
|
+
export declare function runnerInitialOldSpaceMb(addons: readonly RunnerAddonSpec[]): number | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_dist = require("./dist-
|
|
2
|
+
const require_dist = require("./dist-K3WFop9Q.js");
|
|
3
3
|
let node_crypto = require("node:crypto");
|
|
4
4
|
let buffer = require("buffer");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -3508,6 +3508,11 @@ function isSqlitePath(relPath) {
|
|
|
3508
3508
|
const db = databaseOfSidecar(relPath) ?? relPath;
|
|
3509
3509
|
return SQLITE_DATABASE_SUFFIXES.some((suffix) => db.endsWith(suffix));
|
|
3510
3510
|
}
|
|
3511
|
+
/**
|
|
3512
|
+
* The databases a run carries when it states none: the minimum (D382). A
|
|
3513
|
+
* schedule, a manual run and `createArchive` all fall back to this one value.
|
|
3514
|
+
*/
|
|
3515
|
+
var DEFAULT_BACKUP_DATABASES = ["settings"];
|
|
3511
3516
|
/** Path of the manifest file embedded inside every archive. */
|
|
3512
3517
|
var ARCHIVE_MANIFEST_NAME = ".camstack-backup-manifest.json";
|
|
3513
3518
|
var DEFAULT_STAT_CACHE_TTL_MS = 6e4;
|
|
@@ -3596,6 +3601,8 @@ var SystemBackupService = class {
|
|
|
3596
3601
|
throwIfCancelled(opts.signal);
|
|
3597
3602
|
const recipient = opts.encryptTo !== void 0 ? parseBackupPublicKey(opts.encryptTo) : null;
|
|
3598
3603
|
const requested = opts.locations ?? DEFAULT_BACKUP_LOCATIONS;
|
|
3604
|
+
const selection = new Set(opts.databases ?? DEFAULT_BACKUP_DATABASES);
|
|
3605
|
+
if (!selection.has("settings")) return this.createSnapshotsOnlyArchive(opts, selection, recipient);
|
|
3599
3606
|
const present = [];
|
|
3600
3607
|
for (const loc of requested) {
|
|
3601
3608
|
const abs = node_path.isAbsolute(loc) ? loc : node_path.join(this.dataDir, loc);
|
|
@@ -3613,7 +3620,12 @@ var SystemBackupService = class {
|
|
|
3613
3620
|
else if (!isExcludedFromBackup(node_path.relative(this.dataDir, abs))) walked.push(toArchiveEntry(abs, this.dataDir, stat));
|
|
3614
3621
|
}
|
|
3615
3622
|
const walkedPaths = new Set(walked.map((e) => e.path));
|
|
3616
|
-
const
|
|
3623
|
+
const ownedById = walked.some((e) => isSqlitePath(e.path)) ? await this.ownedDatabasesInScope(opts.owner, walkedPaths) : /* @__PURE__ */ new Map();
|
|
3624
|
+
const owned = new Set(ownedById.keys());
|
|
3625
|
+
const selected = /* @__PURE__ */ new Map();
|
|
3626
|
+
const omitted = /* @__PURE__ */ new Set();
|
|
3627
|
+
for (const [rel, databaseId] of ownedById) if (selection.has(databaseId)) selected.set(rel, databaseId);
|
|
3628
|
+
else omitted.add(databaseId);
|
|
3617
3629
|
const unowned = /* @__PURE__ */ new Set();
|
|
3618
3630
|
for (const e of walked) {
|
|
3619
3631
|
const db = databaseOfSidecar(e.path);
|
|
@@ -3627,7 +3639,7 @@ var SystemBackupService = class {
|
|
|
3627
3639
|
} });
|
|
3628
3640
|
throw new BackupSnapshotRefusedError(`live SQLite database ${databases.join(", ")} has no owner that can snapshot it — a copy of its files would be torn; remove its stale -wal/-shm/-journal if nothing has it open, or leave its location out of the backup`);
|
|
3629
3641
|
}
|
|
3630
|
-
const
|
|
3642
|
+
const snapshots = selected.size > 0 && opts.owner ? await this.takeOwnerSnapshots(opts.owner, selected, opts.signal) : [];
|
|
3631
3643
|
try {
|
|
3632
3644
|
const liveFiles = /* @__PURE__ */ new Set();
|
|
3633
3645
|
for (const db of owned) {
|
|
@@ -3635,7 +3647,7 @@ var SystemBackupService = class {
|
|
|
3635
3647
|
for (const suffix of SQLITE_SIDECAR_SUFFIXES) liveFiles.add(db + suffix);
|
|
3636
3648
|
}
|
|
3637
3649
|
const entries = walked.filter((e) => !liveFiles.has(e.path));
|
|
3638
|
-
|
|
3650
|
+
for (const snapshot of snapshots) entries.push(snapshot.entry);
|
|
3639
3651
|
const manifest = {
|
|
3640
3652
|
archiveVersion: 1,
|
|
3641
3653
|
createdAt: Date.now(),
|
|
@@ -3644,15 +3656,20 @@ var SystemBackupService = class {
|
|
|
3644
3656
|
entries,
|
|
3645
3657
|
totalBytes: entries.reduce((acc, e) => acc + e.sizeBytes, 0),
|
|
3646
3658
|
totalFiles: entries.filter((e) => e.kind === "file").length,
|
|
3647
|
-
...
|
|
3659
|
+
...snapshots.length > 0 ? { sqliteSnapshots: snapshots.map((snapshot) => snapshot.archived) } : {},
|
|
3660
|
+
...omitted.size > 0 ? { omittedDatabases: [...omitted].toSorted() } : {}
|
|
3648
3661
|
};
|
|
3649
|
-
|
|
3662
|
+
if (omitted.size > 0) this.logger.info("createArchive: owned databases left out of this run", { meta: {
|
|
3663
|
+
omitted: [...omitted].toSorted(),
|
|
3664
|
+
carried: [...selected.values()]
|
|
3665
|
+
} });
|
|
3666
|
+
const carriedSnapshots = new Set(snapshots.map((snapshot) => snapshot.archived.snapshotPath));
|
|
3650
3667
|
return await this.writeArchive({
|
|
3651
3668
|
archivePath: opts.archivePath,
|
|
3652
3669
|
present,
|
|
3653
3670
|
manifest,
|
|
3654
3671
|
excluded: (rel) => {
|
|
3655
|
-
if (rel
|
|
3672
|
+
if (carriedSnapshots.has(rel)) return false;
|
|
3656
3673
|
return liveFiles.has(rel) || isExcludedFromBackup(rel);
|
|
3657
3674
|
},
|
|
3658
3675
|
...opts.signal ? { signal: opts.signal } : {},
|
|
@@ -3666,7 +3683,62 @@ var SystemBackupService = class {
|
|
|
3666
3683
|
}
|
|
3667
3684
|
});
|
|
3668
3685
|
} finally {
|
|
3669
|
-
|
|
3686
|
+
for (const snapshot of snapshots) await this.removeSnapshot(snapshot.absPath);
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
/**
|
|
3690
|
+
* An archive of the selected databases' owner snapshots and the manifest —
|
|
3691
|
+
* no data-root walk, no location, no settings (D852). Refused when the owner
|
|
3692
|
+
* has none of the selected files: an archive of nothing must reach no
|
|
3693
|
+
* destination, because its upload prunes an older archive that has them.
|
|
3694
|
+
*/
|
|
3695
|
+
async createSnapshotsOnlyArchive(opts, selection, recipient) {
|
|
3696
|
+
if (opts.owner === void 0) throw new BackupSnapshotRefusedError("a snapshots-only archive needs the database owner, and none was given");
|
|
3697
|
+
node_fs.mkdirSync(node_path.dirname(opts.archivePath), { recursive: true });
|
|
3698
|
+
const ownedById = await this.ownedDatabasesInScope(opts.owner, null);
|
|
3699
|
+
const selected = /* @__PURE__ */ new Map();
|
|
3700
|
+
for (const [rel, databaseId] of ownedById) if (selection.has(databaseId)) selected.set(rel, databaseId);
|
|
3701
|
+
if (selected.size === 0) {
|
|
3702
|
+
this.logger.warn("createArchive: the owner has none of the selected databases — no archive built", { meta: {
|
|
3703
|
+
requested: [...selection].toSorted(),
|
|
3704
|
+
archivePath: opts.archivePath
|
|
3705
|
+
} });
|
|
3706
|
+
throw new BackupSnapshotRefusedError(`the owner has none of the selected databases (${[...selection].toSorted().join(", ")})`);
|
|
3707
|
+
}
|
|
3708
|
+
const snapshots = await this.takeOwnerSnapshots(opts.owner, selected, opts.signal);
|
|
3709
|
+
try {
|
|
3710
|
+
const entries = snapshots.map((snapshot) => snapshot.entry);
|
|
3711
|
+
const manifest = {
|
|
3712
|
+
archiveVersion: 1,
|
|
3713
|
+
createdAt: Date.now(),
|
|
3714
|
+
dataDir: this.dataDir,
|
|
3715
|
+
locations: [],
|
|
3716
|
+
entries,
|
|
3717
|
+
totalBytes: entries.reduce((acc, e) => acc + e.sizeBytes, 0),
|
|
3718
|
+
totalFiles: entries.length,
|
|
3719
|
+
sqliteSnapshots: snapshots.map((snapshot) => snapshot.archived)
|
|
3720
|
+
};
|
|
3721
|
+
const carried = snapshots.map((snapshot) => snapshot.archived.snapshotPath);
|
|
3722
|
+
const carriedSet = new Set(carried);
|
|
3723
|
+
const result = await this.writeArchive({
|
|
3724
|
+
archivePath: opts.archivePath,
|
|
3725
|
+
present: [],
|
|
3726
|
+
tarRoots: carried,
|
|
3727
|
+
manifest,
|
|
3728
|
+
excluded: (rel) => rel !== ".camstack-backup-manifest.json" && !carriedSet.has(rel),
|
|
3729
|
+
...opts.signal ? { signal: opts.signal } : {},
|
|
3730
|
+
...opts.onArchiveBytes ? { onArchiveBytes: opts.onArchiveBytes } : {},
|
|
3731
|
+
...opts.encryptTo !== void 0 ? { encryptTo: opts.encryptTo } : {}
|
|
3732
|
+
});
|
|
3733
|
+
return recipient === null ? result : {
|
|
3734
|
+
...result,
|
|
3735
|
+
encryption: {
|
|
3736
|
+
scheme: BACKUP_ENCRYPTION_SCHEME,
|
|
3737
|
+
keyId: recipient.keyId
|
|
3738
|
+
}
|
|
3739
|
+
};
|
|
3740
|
+
} finally {
|
|
3741
|
+
for (const snapshot of snapshots) await this.removeSnapshot(snapshot.absPath);
|
|
3670
3742
|
}
|
|
3671
3743
|
}
|
|
3672
3744
|
/**
|
|
@@ -3700,7 +3772,7 @@ var SystemBackupService = class {
|
|
|
3700
3772
|
if (!excludePrefix) return true;
|
|
3701
3773
|
return norm !== excludePrefix && !norm.startsWith(`${excludePrefix}/`);
|
|
3702
3774
|
}
|
|
3703
|
-
}, [ARCHIVE_MANIFEST_NAME, ...present]);
|
|
3775
|
+
}, [ARCHIVE_MANIFEST_NAME, ...opts.tarRoots ?? present]);
|
|
3704
3776
|
const sink = node_fs.createWriteStream(opts.archivePath, { mode: 384 });
|
|
3705
3777
|
try {
|
|
3706
3778
|
const streamOpts = opts.signal ? { signal: opts.signal } : {};
|
|
@@ -3817,12 +3889,13 @@ var SystemBackupService = class {
|
|
|
3817
3889
|
if (node_fs.existsSync(markerPath)) node_fs.rmSync(markerPath, { force: true });
|
|
3818
3890
|
}
|
|
3819
3891
|
/**
|
|
3820
|
-
* The databases the owner declares, archive-relative, that this walk found
|
|
3821
|
-
* One outside this data root is not in scope. An owner
|
|
3822
|
-
* stops the archive: without the answer, nothing says
|
|
3892
|
+
* The databases the owner declares, archive-relative, that this walk found,
|
|
3893
|
+
* with the id of each. One outside this data root is not in scope. An owner
|
|
3894
|
+
* that cannot answer stops the archive: without the answer, nothing says
|
|
3895
|
+
* which file is live.
|
|
3823
3896
|
*/
|
|
3824
3897
|
async ownedDatabasesInScope(owner, walked) {
|
|
3825
|
-
if (owner === void 0) return /* @__PURE__ */ new
|
|
3898
|
+
if (owner === void 0) return /* @__PURE__ */ new Map();
|
|
3826
3899
|
let declared;
|
|
3827
3900
|
try {
|
|
3828
3901
|
declared = await owner.listDatabases();
|
|
@@ -3831,22 +3904,23 @@ var SystemBackupService = class {
|
|
|
3831
3904
|
this.logger.error("createArchive: the database owner did not say what it owns — no archive built", { meta: { error: reason } });
|
|
3832
3905
|
throw new BackupSnapshotRefusedError(`the database owner could not be asked: ${reason}`);
|
|
3833
3906
|
}
|
|
3834
|
-
const inScope = /* @__PURE__ */ new
|
|
3835
|
-
for (const
|
|
3836
|
-
const rel = this.archiveRelative(
|
|
3837
|
-
if (rel !== null && walked.has(rel)) inScope.
|
|
3907
|
+
const inScope = /* @__PURE__ */ new Map();
|
|
3908
|
+
for (const db of declared) {
|
|
3909
|
+
const rel = this.archiveRelative(db.livePath);
|
|
3910
|
+
if (rel !== null && (walked === null || walked.has(rel))) inScope.set(rel, db.databaseId);
|
|
3838
3911
|
}
|
|
3839
3912
|
return inScope;
|
|
3840
3913
|
}
|
|
3841
3914
|
/**
|
|
3842
|
-
* Ask the owner for
|
|
3843
|
-
*
|
|
3844
|
-
*
|
|
3845
|
-
*
|
|
3846
|
-
* database
|
|
3915
|
+
* Ask the owner for one verified snapshot per SELECTED database, one after
|
|
3916
|
+
* the other, and place each in this archive. A snapshot must sit on THIS data
|
|
3917
|
+
* root — the call is pinned to this node, so a path elsewhere means it
|
|
3918
|
+
* reached another host, and that file is not ours to read — beside the very
|
|
3919
|
+
* database it was asked for. Anything else is refused, and the run builds
|
|
3920
|
+
* nothing: the copies already taken are removed.
|
|
3847
3921
|
*
|
|
3848
|
-
* Only ever DELETED here: `<
|
|
3849
|
-
* a name this process derives itself. A path the owner ANSWERS is never
|
|
3922
|
+
* Only ever DELETED here: `<selected db>.backup-snapshot` under this data
|
|
3923
|
+
* root, a name this process derives itself. A path the owner ANSWERS is never
|
|
3850
3924
|
* deleted on its word — answering the live database as the snapshot would
|
|
3851
3925
|
* otherwise delete the live database. A file at any other path is left to
|
|
3852
3926
|
* the owner's own sweep.
|
|
@@ -3854,62 +3928,73 @@ var SystemBackupService = class {
|
|
|
3854
3928
|
* A cancel is honoured while the owner is still copying; the copy it may
|
|
3855
3929
|
* finish afterwards is removed when that call settles.
|
|
3856
3930
|
*/
|
|
3857
|
-
async
|
|
3931
|
+
async takeOwnerSnapshots(owner, selected, signal) {
|
|
3858
3932
|
const removeOwnedSnapshots = async () => {
|
|
3859
|
-
for (const db of
|
|
3933
|
+
for (const db of selected.keys()) await this.removeSnapshot(node_path.join(this.dataDir, db + require_dist.DATABASE_SNAPSHOT_SUFFIX));
|
|
3860
3934
|
};
|
|
3935
|
+
const taken = [];
|
|
3936
|
+
try {
|
|
3937
|
+
for (const [expectedLive, databaseId] of selected) {
|
|
3938
|
+
throwIfCancelled(signal);
|
|
3939
|
+
taken.push(await this.takeOneSnapshot(owner, databaseId, expectedLive, signal, selected, removeOwnedSnapshots));
|
|
3940
|
+
}
|
|
3941
|
+
} catch (err) {
|
|
3942
|
+
await removeOwnedSnapshots();
|
|
3943
|
+
throw err;
|
|
3944
|
+
}
|
|
3945
|
+
return taken;
|
|
3946
|
+
}
|
|
3947
|
+
async takeOneSnapshot(owner, databaseId, expectedLive, signal, selected, removeOwnedSnapshots) {
|
|
3861
3948
|
let result;
|
|
3862
3949
|
try {
|
|
3863
|
-
result = await raceAbort(owner.writeSnapshot(), signal, () => {
|
|
3864
|
-
this.logger.info("createArchive: run cancelled while the owner copied — its copy is removed when it lands", { meta: {
|
|
3950
|
+
result = await raceAbort(owner.writeSnapshot(databaseId), signal, () => {
|
|
3951
|
+
this.logger.info("createArchive: run cancelled while the owner copied — its copy is removed when it lands", { meta: {
|
|
3952
|
+
databases: [...selected.values()],
|
|
3953
|
+
database: databaseId
|
|
3954
|
+
} });
|
|
3865
3955
|
return removeOwnedSnapshots();
|
|
3866
3956
|
});
|
|
3867
3957
|
} catch (err) {
|
|
3868
3958
|
if (err instanceof BackupCancelledError) throw err;
|
|
3869
3959
|
const reason = err instanceof Error ? err.message : String(err);
|
|
3870
3960
|
this.logger.error("createArchive: the owner could not snapshot its live database — no archive built", { meta: {
|
|
3871
|
-
databases: [...
|
|
3961
|
+
databases: [...selected.values()],
|
|
3962
|
+
database: databaseId,
|
|
3872
3963
|
error: reason
|
|
3873
3964
|
} });
|
|
3874
|
-
|
|
3875
|
-
throw new BackupSnapshotRefusedError(reason);
|
|
3965
|
+
throw new BackupSnapshotRefusedError(`${databaseId}: ${reason}`);
|
|
3876
3966
|
}
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
dataDir: this.dataDir
|
|
3884
|
-
} });
|
|
3885
|
-
throw new BackupSnapshotRefusedError(`snapshot ${result.snapshotPath} is not under this archive's data root ${this.dataDir}`);
|
|
3886
|
-
}
|
|
3887
|
-
if (snapshotPath !== livePath + ".backup-snapshot") throw new BackupSnapshotRefusedError(`snapshot ${snapshotPath} does not sit beside the database ${livePath} it copies`);
|
|
3888
|
-
const uncovered = [...owned].filter((db) => db !== livePath);
|
|
3889
|
-
if (!owned.has(livePath) || uncovered.length > 0) throw new BackupSnapshotRefusedError(`snapshot copies ${livePath}, but the owned database(s) in this archive are ${[...owned].join(", ")}`);
|
|
3890
|
-
const absPath = node_path.join(this.dataDir, snapshotPath);
|
|
3891
|
-
const stat = await node_fs_promises.stat(absPath);
|
|
3892
|
-
this.logger.info("createArchive: carrying the owner snapshot in place of the live database", { meta: {
|
|
3893
|
-
database: livePath,
|
|
3894
|
-
snapshot: snapshotPath,
|
|
3895
|
-
sizeBytes: stat.size,
|
|
3896
|
-
tables: result.tables.length,
|
|
3897
|
-
copyMs: result.copyMs,
|
|
3898
|
-
verifyMs: result.verifyMs
|
|
3967
|
+
const livePath = this.archiveRelative(result.livePath);
|
|
3968
|
+
const snapshotPath = this.archiveRelative(result.snapshotPath);
|
|
3969
|
+
if (livePath === null || snapshotPath === null) {
|
|
3970
|
+
this.logger.warn("createArchive: the owner answered a snapshot outside this data root — not touched, left to its sweep", { meta: {
|
|
3971
|
+
snapshot: result.snapshotPath,
|
|
3972
|
+
dataDir: this.dataDir
|
|
3899
3973
|
} });
|
|
3900
|
-
|
|
3901
|
-
absPath,
|
|
3902
|
-
archived: {
|
|
3903
|
-
...result,
|
|
3904
|
-
livePath,
|
|
3905
|
-
snapshotPath
|
|
3906
|
-
},
|
|
3907
|
-
entry: toArchiveEntry(absPath, this.dataDir, stat)
|
|
3908
|
-
};
|
|
3909
|
-
} catch (err) {
|
|
3910
|
-
await removeOwnedSnapshots();
|
|
3911
|
-
throw err;
|
|
3974
|
+
throw new BackupSnapshotRefusedError(`snapshot ${result.snapshotPath} is not under this archive's data root ${this.dataDir}`);
|
|
3912
3975
|
}
|
|
3976
|
+
if (snapshotPath !== livePath + ".backup-snapshot") throw new BackupSnapshotRefusedError(`snapshot ${snapshotPath} does not sit beside the database ${livePath} it copies`);
|
|
3977
|
+
if (livePath !== expectedLive || result.databaseId !== databaseId) throw new BackupSnapshotRefusedError(`snapshot copies ${livePath} (${result.databaseId}), but ${databaseId} (${expectedLive}) was asked for`);
|
|
3978
|
+
const absPath = node_path.join(this.dataDir, snapshotPath);
|
|
3979
|
+
const stat = await node_fs_promises.stat(absPath);
|
|
3980
|
+
this.logger.info("createArchive: carrying the owner snapshot in place of the live database", { meta: {
|
|
3981
|
+
database: livePath,
|
|
3982
|
+
databaseId,
|
|
3983
|
+
snapshot: snapshotPath,
|
|
3984
|
+
sizeBytes: stat.size,
|
|
3985
|
+
tables: result.tables.length,
|
|
3986
|
+
copyMs: result.copyMs,
|
|
3987
|
+
verifyMs: result.verifyMs
|
|
3988
|
+
} });
|
|
3989
|
+
return {
|
|
3990
|
+
absPath,
|
|
3991
|
+
archived: {
|
|
3992
|
+
...result,
|
|
3993
|
+
livePath,
|
|
3994
|
+
snapshotPath
|
|
3995
|
+
},
|
|
3996
|
+
entry: toArchiveEntry(absPath, this.dataDir, stat)
|
|
3997
|
+
};
|
|
3913
3998
|
}
|
|
3914
3999
|
async removeSnapshot(absPath) {
|
|
3915
4000
|
try {
|
|
@@ -4089,6 +4174,12 @@ Object.defineProperty(exports, "BackupSnapshotRefusedError", {
|
|
|
4089
4174
|
return BackupSnapshotRefusedError;
|
|
4090
4175
|
}
|
|
4091
4176
|
});
|
|
4177
|
+
Object.defineProperty(exports, "DEFAULT_BACKUP_DATABASES", {
|
|
4178
|
+
enumerable: true,
|
|
4179
|
+
get: function() {
|
|
4180
|
+
return DEFAULT_BACKUP_DATABASES;
|
|
4181
|
+
}
|
|
4182
|
+
});
|
|
4092
4183
|
Object.defineProperty(exports, "ENCRYPTED_BACKUP_SUFFIX", {
|
|
4093
4184
|
enumerable: true,
|
|
4094
4185
|
get: function() {
|