@camstack/system 1.2.107 → 1.2.109

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.
@@ -0,0 +1,295 @@
1
+ const require_dist = require("./dist-Ctfhlrtl.js");
2
+ //#region src/builtins/sqlite-storage/retired-settings-keys.ts
3
+ /**
4
+ * Is THIS node the one whose settings store is the cluster's authority?
5
+ *
6
+ * Mirrors `resolveNodeRole` (server/backend `node-role.ts`) — unset or `'hub'`
7
+ * means hub, which is the launcher's own default. It is duplicated rather than
8
+ * imported because `@camstack/system` is a dependency OF the backend, not the
9
+ * other way round; the parse is four lines and the env var is the contract.
10
+ *
11
+ * Anything else answers `false`. Skipping where the purge should have run is a
12
+ * missed cleanup the next hub boot fixes; running where it should not have is a
13
+ * write to a store this node does not own. Only one of those is recoverable.
14
+ */
15
+ function settingsStoreIsAuthoritativeHere(env) {
16
+ const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
17
+ return raw === "" || raw === "hub";
18
+ }
19
+ /**
20
+ * The dead keys, per row.
21
+ *
22
+ * `detection-pipeline` / `addon-settings` / `root` — every key in this row is
23
+ * dead; the row itself is kept because the addon writes live keys into it
24
+ * (`pipelineTemplates`, `videoPipelineSteps`).
25
+ *
26
+ * - `pipelineSteps` / `pipelineEngine`: the persisted GLOBAL step + engine
27
+ * seed. Removed with `getGlobalSteps` becoming pure — dispatch builds steps
28
+ * from the orchestrator's `agentSettings` plus per-camera overrides, and
29
+ * never consulted the seed. See `addon-pipeline/src/detection-pipeline/provider.ts`.
30
+ * - `engineRuntime` / `engineBackend` / `engineDevice` / `probedBestEngine`:
31
+ * the per-node engine CASCADE, removed with the operator-facing engine
32
+ * election. Each node now runs a device-array of inference pools balanced
33
+ * per session, and the node-default pool derives live from the node's own
34
+ * hardware (`resolveAutoEngine`). See
35
+ * `addon-pipeline/src/detection-pipeline/engine-store-keys.ts`.
36
+ */
37
+ var RETIRED_SETTINGS_KEYS = [{
38
+ namespace: "detection-pipeline",
39
+ collection: "addon-settings",
40
+ row: "root",
41
+ keys: ["pipelineSteps", "pipelineEngine"],
42
+ perNodeKeys: [
43
+ "engineRuntime",
44
+ "engineBackend",
45
+ "engineDevice",
46
+ "probedBestEngine"
47
+ ],
48
+ reason: "global step/engine seed and the per-node engine cascade — both removed; the live per-camera dispatch path never read either"
49
+ }];
50
+ /**
51
+ * The device-manager's three fleet blobs, replaced by one row per device in
52
+ * `device-manager:devices` (`device-row-store.ts`).
53
+ *
54
+ * 625 KB of the `addon-settings` table's 627 KB, re-read and re-parsed on every
55
+ * projection and rewritten in full on every rename. `deviceIndex` is listed
56
+ * here like the other two even though it was never migrated: `addonId` and
57
+ * `stableId` are columns of every row, so an addon's device list is a query,
58
+ * and the one bit that was not derivable — "has `registerDevice` run?" — is the
59
+ * `registered` column.
60
+ */
61
+ var RETIRED_SETTINGS_ROWS = [
62
+ {
63
+ collection: "addon-settings",
64
+ row: "device-manager.deviceMeta",
65
+ owner: "device-manager",
66
+ evidence: { collection: "device-manager:devices" },
67
+ reason: "the fleet is one row per device in `device-manager:devices` since the flatten; this blob was 467 KB read and JSON.parsed on hub-main per projection"
68
+ },
69
+ {
70
+ collection: "addon-settings",
71
+ row: "device-manager.deviceMetadata",
72
+ owner: "device-manager",
73
+ evidence: { collection: "device-manager:devices" },
74
+ reason: "hardware-identity metadata is the `metadata` column of the device row; the separate blob existed only because a blob write is whole-value"
75
+ },
76
+ {
77
+ collection: "addon-settings",
78
+ row: "device-manager.deviceIndex",
79
+ owner: "device-manager",
80
+ evidence: { collection: "device-manager:devices" },
81
+ reason: "derivable: `addonId`/`stableId` are indexed columns, and index membership is the `registered` column"
82
+ }
83
+ ];
84
+ /**
85
+ * Adapt the settings backend to {@link RetiredKeyStore}.
86
+ *
87
+ * `get` answering a non-object (missing row) is a MISS, not a fault: it maps to
88
+ * `null` and the purge skips the row. A backend throw propagates, which is what
89
+ * routes it to the "read failed, row left untouched" branch.
90
+ */
91
+ function retiredKeyStoreOf(backend) {
92
+ return {
93
+ async read(spec) {
94
+ return require_dist.asJsonObject(await backend.get({
95
+ namespace: spec.namespace,
96
+ collection: spec.collection,
97
+ key: spec.row
98
+ }));
99
+ },
100
+ async write(spec, value) {
101
+ await backend.set({
102
+ namespace: spec.namespace,
103
+ collection: spec.collection,
104
+ key: spec.row,
105
+ value
106
+ });
107
+ }
108
+ };
109
+ }
110
+ /**
111
+ * Nested `<mapKey>.<retiredKey>` paths that `spec` retires inside a map-shaped
112
+ * row. Pure. Values that are not plain objects are skipped, never rewritten.
113
+ */
114
+ function planNestedRetiredKeyPurge(blob, spec) {
115
+ const nestedKeys = spec.mapValueKeys ?? [];
116
+ if (nestedKeys.length === 0) return [];
117
+ const found = [];
118
+ for (const [mapKey, value] of Object.entries(blob)) {
119
+ const inner = require_dist.asJsonObject(value);
120
+ if (inner === null) continue;
121
+ for (const retired of nestedKeys) if (retired in inner) found.push(`${mapKey}.${retired}`);
122
+ }
123
+ return found;
124
+ }
125
+ /** Keys of `blob` that `spec` retires. Pure. */
126
+ function planRetiredKeyPurge(blob, spec) {
127
+ const exact = new Set(spec.keys);
128
+ const found = [];
129
+ for (const key of Object.keys(blob)) {
130
+ if (exact.has(key)) {
131
+ found.push(key);
132
+ continue;
133
+ }
134
+ for (const perNode of spec.perNodeKeys) if (key === perNode || key.startsWith(`${perNode}@`)) {
135
+ found.push(key);
136
+ break;
137
+ }
138
+ }
139
+ return found;
140
+ }
141
+ /**
142
+ * Delete every retired key present in the declared rows — on the hub.
143
+ *
144
+ * Off the authoritative node this is a no-op that touches nothing and says
145
+ * nothing (see the module header). Returns one entry per row that actually
146
+ * changed — empty when the store is already clean, which is the steady state
147
+ * after the first boot that ran it.
148
+ */
149
+ async function purgeRetiredSettingsKeys(store, logger, specs = RETIRED_SETTINGS_KEYS, env = process.env) {
150
+ if (!settingsStoreIsAuthoritativeHere(env)) return [];
151
+ const results = [];
152
+ for (const spec of specs) {
153
+ let blob;
154
+ try {
155
+ blob = await store.read(spec);
156
+ } catch (err) {
157
+ logger.warn("retired settings keys — read failed, row left untouched", { meta: {
158
+ addon: spec.namespace,
159
+ collection: spec.collection,
160
+ row: spec.row,
161
+ error: err instanceof Error ? err.message : String(err)
162
+ } });
163
+ continue;
164
+ }
165
+ if (blob === null) continue;
166
+ const doomed = planRetiredKeyPurge(blob, spec);
167
+ const doomedNested = planNestedRetiredKeyPurge(blob, spec);
168
+ if (doomed.length === 0 && doomedNested.length === 0) continue;
169
+ const doomedSet = new Set(doomed);
170
+ const nestedByMapKey = /* @__PURE__ */ new Map();
171
+ for (const path of doomedNested) {
172
+ const cut = path.indexOf(".");
173
+ const mapKey = path.slice(0, cut);
174
+ const inner = path.slice(cut + 1);
175
+ const bucket = nestedByMapKey.get(mapKey) ?? /* @__PURE__ */ new Set();
176
+ bucket.add(inner);
177
+ nestedByMapKey.set(mapKey, bucket);
178
+ }
179
+ const next = Object.fromEntries(Object.entries(blob).filter(([key]) => !doomedSet.has(key)).map(([key, value]) => {
180
+ const retired = nestedByMapKey.get(key);
181
+ if (retired === void 0) return [key, value];
182
+ const inner = require_dist.asJsonObject(value);
183
+ if (inner === null) return [key, value];
184
+ return [key, Object.fromEntries(Object.entries(inner).filter(([k]) => !retired.has(k)))];
185
+ }));
186
+ await store.write(spec, next);
187
+ const result = {
188
+ addon: spec.namespace,
189
+ collection: spec.collection,
190
+ row: spec.row,
191
+ keys: doomed,
192
+ nested: doomedNested
193
+ };
194
+ results.push(result);
195
+ logger.info(`purged ${doomed.length + doomedNested.length} retired settings keys`, { meta: {
196
+ ...result,
197
+ reason: spec.reason
198
+ } });
199
+ }
200
+ return results;
201
+ }
202
+ /**
203
+ * Delete every retired ROW whose successor is populated — on the hub.
204
+ *
205
+ * Off the authoritative node this is a no-op that touches nothing and says
206
+ * nothing, same gate as {@link purgeRetiredSettingsKeys}.
207
+ *
208
+ * **The evidence check is the whole safety of this function.** A retired row is
209
+ * the only copy of its data until the migration has written the successor
210
+ * collection, and the migration is an OFFLINE step an operator runs — it is not
211
+ * part of boot. So a hub that boots the new code before being migrated must
212
+ * find its blobs intact, not deleted: the purge SKIPS, loudly, and the next
213
+ * boot after the migration cleans up. An evidence read that FAILS is treated
214
+ * exactly like an empty one — a fault is never permission to delete.
215
+ */
216
+ async function purgeRetiredSettingsRows(store, logger, specs = RETIRED_SETTINGS_ROWS, env = process.env) {
217
+ if (!settingsStoreIsAuthoritativeHere(env)) return [];
218
+ const results = [];
219
+ /** Evidence counts, memoised per collection — the specs share successors. */
220
+ const evidence = /* @__PURE__ */ new Map();
221
+ for (const spec of specs) {
222
+ let present;
223
+ try {
224
+ present = await store.hasRow(spec);
225
+ } catch (err) {
226
+ logger.warn("retired settings row — read failed, row left untouched", { meta: {
227
+ addon: spec.owner,
228
+ collection: spec.collection,
229
+ row: spec.row,
230
+ error: errText(err)
231
+ } });
232
+ continue;
233
+ }
234
+ if (!present) continue;
235
+ const successor = spec.evidence.collection;
236
+ let count = evidence.get(successor);
237
+ if (count === void 0) {
238
+ try {
239
+ count = await store.countRows(successor);
240
+ } catch (err) {
241
+ logger.warn("retired settings row — successor unreadable, row left untouched", { meta: {
242
+ addon: spec.owner,
243
+ row: spec.row,
244
+ successor,
245
+ error: errText(err)
246
+ } });
247
+ count = 0;
248
+ }
249
+ evidence.set(successor, count);
250
+ }
251
+ if (count === 0) {
252
+ logger.warn("retired settings row kept — its successor is empty, migration has not run", { meta: {
253
+ addon: spec.owner,
254
+ collection: spec.collection,
255
+ row: spec.row,
256
+ successor
257
+ } });
258
+ continue;
259
+ }
260
+ await store.deleteRow(spec);
261
+ const result = {
262
+ owner: spec.owner,
263
+ collection: spec.collection,
264
+ row: spec.row
265
+ };
266
+ results.push(result);
267
+ logger.info("purged a retired settings row", { meta: {
268
+ ...result,
269
+ reason: spec.reason
270
+ } });
271
+ }
272
+ return results;
273
+ }
274
+ function errText(err) {
275
+ return err instanceof Error ? err.message : String(err);
276
+ }
277
+ //#endregion
278
+ Object.defineProperty(exports, "purgeRetiredSettingsKeys", {
279
+ enumerable: true,
280
+ get: function() {
281
+ return purgeRetiredSettingsKeys;
282
+ }
283
+ });
284
+ Object.defineProperty(exports, "purgeRetiredSettingsRows", {
285
+ enumerable: true,
286
+ get: function() {
287
+ return purgeRetiredSettingsRows;
288
+ }
289
+ });
290
+ Object.defineProperty(exports, "retiredKeyStoreOf", {
291
+ enumerable: true,
292
+ get: function() {
293
+ return retiredKeyStoreOf;
294
+ }
295
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.107",
3
+ "version": "1.2.109",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",
@@ -349,7 +349,6 @@
349
349
  },
350
350
  "dependencies": {
351
351
  "@camstack/sdk": "*",
352
- "@camstack/shm-ring": "*",
353
352
  "@camstack/types": "*",
354
353
  "@msgpack/msgpack": "^3.1.3",
355
354
  "@trpc/client": "^11.16.0",