@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.
- package/dist/builtins/device-manager/device-aggregation.d.ts +2 -2
- package/dist/builtins/device-manager/device-bindings-store.d.ts +12 -6
- package/dist/builtins/device-manager/device-manager.addon.d.ts +6 -0
- package/dist/builtins/device-manager/device-manager.addon.js +2260 -1888
- package/dist/builtins/device-manager/device-manager.addon.mjs +2260 -1888
- package/dist/builtins/device-manager/device-meta-actions.d.ts +4 -4
- package/dist/builtins/device-manager/device-meta-store.d.ts +18 -43
- package/dist/builtins/device-manager/device-meta-types.d.ts +19 -17
- package/dist/builtins/device-manager/device-row-store.d.ts +248 -0
- package/dist/builtins/sqlite-storage/integration-registry.d.ts +15 -12
- package/dist/builtins/sqlite-storage/retired-collections.d.ts +52 -0
- package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +99 -0
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +22 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +71 -145
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +70 -144
- package/dist/index.js +11 -292
- package/dist/index.mjs +11 -292
- package/dist/retired-settings-keys-Bsjf7HQ-.mjs +278 -0
- package/dist/retired-settings-keys-pNmoHPTg.js +295 -0
- package/package.json +1 -2
|
@@ -20,6 +20,11 @@ interface SettingsUpdateWhereInput {
|
|
|
20
20
|
interface SettingsUpdateWhereResult {
|
|
21
21
|
readonly updated: number;
|
|
22
22
|
}
|
|
23
|
+
/** What {@link SqliteSettingsBackend.dropRetiredTable} destroyed. */
|
|
24
|
+
export interface DroppedTable {
|
|
25
|
+
/** Rows the table held at the moment it was dropped. */
|
|
26
|
+
readonly rows: number;
|
|
27
|
+
}
|
|
23
28
|
/**
|
|
24
29
|
* SQLite implementation of ISettingsBackend.
|
|
25
30
|
*
|
|
@@ -213,6 +218,23 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
|
|
|
213
218
|
/** Serialise per-column values for SQL binding: objects → JSON, booleans → 0/1. */
|
|
214
219
|
private serializeColumnValue;
|
|
215
220
|
private ensureTable;
|
|
221
|
+
/**
|
|
222
|
+
* Drop a table nothing declares any more, reporting what went with it.
|
|
223
|
+
*
|
|
224
|
+
* The DDL lives here because the engine owns the handle AND the two memos
|
|
225
|
+
* that outlive a table: `structuredTables` (schema fingerprint) and
|
|
226
|
+
* `declaredCollections` (the column set `compileFilter` resolves against).
|
|
227
|
+
* Dropping behind their backs would leave a ghost — a later declaration of
|
|
228
|
+
* the same name would be memoised as "already materialised" against a table
|
|
229
|
+
* that no longer exists.
|
|
230
|
+
*
|
|
231
|
+
* WHICH tables are retired, and the logging, are policy: they live in
|
|
232
|
+
* `retired-collections.ts`. This method knows only how to drop one.
|
|
233
|
+
*
|
|
234
|
+
* Answers `null` when the table is not there — the steady state after the
|
|
235
|
+
* first boot that ran the purge, and the case on a fresh install.
|
|
236
|
+
*/
|
|
237
|
+
dropRetiredTable(table: string): DroppedTable | null;
|
|
216
238
|
private tableInsert;
|
|
217
239
|
private tableUpdate;
|
|
218
240
|
private tableDelete;
|
|
@@ -4,6 +4,7 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
6
|
const require_dist = require("../../dist-Ctfhlrtl.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-pNmoHPTg.js");
|
|
7
8
|
let node_crypto = require("node:crypto");
|
|
8
9
|
let node_fs = require("node:fs");
|
|
9
10
|
let node_module = require("node:module");
|
|
@@ -84,174 +85,70 @@ var SQLITE_MMAP_SIZE_BYTES = 268435456;
|
|
|
84
85
|
*/
|
|
85
86
|
var SQLITE_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
|
|
86
87
|
//#endregion
|
|
87
|
-
//#region src/builtins/sqlite-storage/retired-
|
|
88
|
+
//#region src/builtins/sqlite-storage/retired-collections.ts
|
|
88
89
|
/**
|
|
89
|
-
*
|
|
90
|
+
* The dead tables.
|
|
90
91
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* write to a store this node does not own. Only one of those is recoverable.
|
|
92
|
+
* `devices` / `device_settings_kv` — the device half of the
|
|
93
|
+
* integration-registry ([D183](../../../../../docs/decisions/adr-0183.md)).
|
|
94
|
+
* The registry's INTEGRATION half is live (`integrations`,
|
|
95
|
+
* `integration_settings`); its device half never had a production caller and
|
|
96
|
+
* never had a row. The fleet lives in `device-manager:devices`, under numeric
|
|
97
|
+
* ids, while these tables key on `dev_NNNN` — the two id-spaces could not be
|
|
98
|
+
* reconciled even if something had wanted to.
|
|
99
99
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* The dead keys, per row.
|
|
106
|
-
*
|
|
107
|
-
* `detection-pipeline` / `addon-settings` / `root` — every key in this row is
|
|
108
|
-
* dead; the row itself is kept because the addon writes live keys into it
|
|
109
|
-
* (`pipelineTemplates`, `videoPipelineSteps`).
|
|
110
|
-
*
|
|
111
|
-
* - `pipelineSteps` / `pipelineEngine`: the persisted GLOBAL step + engine
|
|
112
|
-
* seed. Removed with `getGlobalSteps` becoming pure — dispatch builds steps
|
|
113
|
-
* from the orchestrator's `agentSettings` plus per-camera overrides, and
|
|
114
|
-
* never consulted the seed. See `addon-pipeline/src/detection-pipeline/provider.ts`.
|
|
115
|
-
* - `engineRuntime` / `engineBackend` / `engineDevice` / `probedBestEngine`:
|
|
116
|
-
* the per-node engine CASCADE, removed with the operator-facing engine
|
|
117
|
-
* election. Each node now runs a device-array of inference pools balanced
|
|
118
|
-
* per session, and the node-default pool derives live from the node's own
|
|
119
|
-
* hardware (`resolveAutoEngine`). See
|
|
120
|
-
* `addon-pipeline/src/detection-pipeline/engine-store-keys.ts`.
|
|
121
|
-
*/
|
|
122
|
-
var RETIRED_SETTINGS_KEYS = [{
|
|
123
|
-
namespace: "detection-pipeline",
|
|
124
|
-
collection: "addon-settings",
|
|
125
|
-
row: "root",
|
|
126
|
-
keys: ["pipelineSteps", "pipelineEngine"],
|
|
127
|
-
perNodeKeys: [
|
|
128
|
-
"engineRuntime",
|
|
129
|
-
"engineBackend",
|
|
130
|
-
"engineDevice",
|
|
131
|
-
"probedBestEngine"
|
|
132
|
-
],
|
|
133
|
-
reason: "global step/engine seed and the per-node engine cascade — both removed; the live per-camera dispatch path never read either"
|
|
100
|
+
var RETIRED_COLLECTIONS = [{
|
|
101
|
+
collection: "devices",
|
|
102
|
+
owner: "integration-registry",
|
|
103
|
+
reason: "the device half of the integration-registry is deleted; the fleet is `device-manager:devices` and always was (0 rows here against 974 live devices)"
|
|
134
104
|
}, {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
keys: [],
|
|
139
|
-
perNodeKeys: [],
|
|
140
|
-
mapValueKeys: ["deviceLinks"],
|
|
141
|
-
reason: "cross-device field wiring, deleted 2026-08-08 — 0 links across 302 devices a month after the UI shipped. There is no reader left: the resolver, the overlay, the cycle guard, the fifth binding kind and the tab are gone"
|
|
105
|
+
collection: "device_settings_kv",
|
|
106
|
+
owner: "integration-registry",
|
|
107
|
+
reason: "per-device settings of a device table that never had a row; the live per-camera store is the device-manager row plus `addon-device-settings`"
|
|
142
108
|
}];
|
|
143
109
|
/**
|
|
144
|
-
* Adapt the
|
|
110
|
+
* Adapt the SQLite engine to {@link RetiredCollectionStore}.
|
|
145
111
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* routes it to the "read failed, row left untouched" branch.
|
|
112
|
+
* The DDL itself is the engine's — it owns the handle and the memos a drop has
|
|
113
|
+
* to evict. This module owns only the list and the reporting.
|
|
149
114
|
*/
|
|
150
|
-
function
|
|
151
|
-
return {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
namespace: spec.namespace,
|
|
155
|
-
collection: spec.collection,
|
|
156
|
-
key: spec.row
|
|
157
|
-
}));
|
|
158
|
-
},
|
|
159
|
-
async write(spec, value) {
|
|
160
|
-
await backend.set({
|
|
161
|
-
namespace: spec.namespace,
|
|
162
|
-
collection: spec.collection,
|
|
163
|
-
key: spec.row,
|
|
164
|
-
value
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
};
|
|
115
|
+
function retiredCollectionStoreOf(backend) {
|
|
116
|
+
return { async drop(collection) {
|
|
117
|
+
return backend.dropRetiredTable(collection);
|
|
118
|
+
} };
|
|
168
119
|
}
|
|
169
120
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
121
|
+
* Drop every retired table that is still present. Returns one entry per table
|
|
122
|
+
* actually dropped — empty when the schema is already clean, which is the
|
|
123
|
+
* steady state after the first boot that ran it.
|
|
172
124
|
*/
|
|
173
|
-
function
|
|
174
|
-
const nestedKeys = spec.mapValueKeys ?? [];
|
|
175
|
-
if (nestedKeys.length === 0) return [];
|
|
176
|
-
const found = [];
|
|
177
|
-
for (const [mapKey, value] of Object.entries(blob)) {
|
|
178
|
-
const inner = require_dist.asJsonObject(value);
|
|
179
|
-
if (inner === null) continue;
|
|
180
|
-
for (const retired of nestedKeys) if (retired in inner) found.push(`${mapKey}.${retired}`);
|
|
181
|
-
}
|
|
182
|
-
return found;
|
|
183
|
-
}
|
|
184
|
-
/** Keys of `blob` that `spec` retires. Pure. */
|
|
185
|
-
function planRetiredKeyPurge(blob, spec) {
|
|
186
|
-
const exact = new Set(spec.keys);
|
|
187
|
-
const found = [];
|
|
188
|
-
for (const key of Object.keys(blob)) {
|
|
189
|
-
if (exact.has(key)) {
|
|
190
|
-
found.push(key);
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
for (const perNode of spec.perNodeKeys) if (key === perNode || key.startsWith(`${perNode}@`)) {
|
|
194
|
-
found.push(key);
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return found;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Delete every retired key present in the declared rows — on the hub.
|
|
202
|
-
*
|
|
203
|
-
* Off the authoritative node this is a no-op that touches nothing and says
|
|
204
|
-
* nothing (see the module header). Returns one entry per row that actually
|
|
205
|
-
* changed — empty when the store is already clean, which is the steady state
|
|
206
|
-
* after the first boot that ran it.
|
|
207
|
-
*/
|
|
208
|
-
async function purgeRetiredSettingsKeys(store, logger, specs = RETIRED_SETTINGS_KEYS, env = process.env) {
|
|
209
|
-
if (!settingsStoreIsAuthoritativeHere(env)) return [];
|
|
125
|
+
async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
|
|
210
126
|
const results = [];
|
|
211
127
|
for (const spec of specs) {
|
|
212
|
-
let
|
|
128
|
+
let dropped;
|
|
213
129
|
try {
|
|
214
|
-
|
|
130
|
+
dropped = await store.drop(spec.collection);
|
|
215
131
|
} catch (err) {
|
|
216
|
-
logger.warn("retired
|
|
217
|
-
addon: spec.
|
|
132
|
+
logger.warn("retired collection — drop failed, table left in place", { meta: {
|
|
133
|
+
addon: spec.owner,
|
|
218
134
|
collection: spec.collection,
|
|
219
|
-
row: spec.row,
|
|
220
135
|
error: err instanceof Error ? err.message : String(err)
|
|
221
136
|
} });
|
|
222
137
|
continue;
|
|
223
138
|
}
|
|
224
|
-
if (
|
|
225
|
-
const doomed = planRetiredKeyPurge(blob, spec);
|
|
226
|
-
const doomedNested = planNestedRetiredKeyPurge(blob, spec);
|
|
227
|
-
if (doomed.length === 0 && doomedNested.length === 0) continue;
|
|
228
|
-
const doomedSet = new Set(doomed);
|
|
229
|
-
const nestedByMapKey = /* @__PURE__ */ new Map();
|
|
230
|
-
for (const path of doomedNested) {
|
|
231
|
-
const cut = path.indexOf(".");
|
|
232
|
-
const mapKey = path.slice(0, cut);
|
|
233
|
-
const inner = path.slice(cut + 1);
|
|
234
|
-
const bucket = nestedByMapKey.get(mapKey) ?? /* @__PURE__ */ new Set();
|
|
235
|
-
bucket.add(inner);
|
|
236
|
-
nestedByMapKey.set(mapKey, bucket);
|
|
237
|
-
}
|
|
238
|
-
const next = Object.fromEntries(Object.entries(blob).filter(([key]) => !doomedSet.has(key)).map(([key, value]) => {
|
|
239
|
-
const retired = nestedByMapKey.get(key);
|
|
240
|
-
if (retired === void 0) return [key, value];
|
|
241
|
-
const inner = require_dist.asJsonObject(value);
|
|
242
|
-
if (inner === null) return [key, value];
|
|
243
|
-
return [key, Object.fromEntries(Object.entries(inner).filter(([k]) => !retired.has(k)))];
|
|
244
|
-
}));
|
|
245
|
-
await store.write(spec, next);
|
|
139
|
+
if (dropped === null) continue;
|
|
246
140
|
const result = {
|
|
247
|
-
addon: spec.namespace,
|
|
248
141
|
collection: spec.collection,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
nested: doomedNested
|
|
142
|
+
owner: spec.owner,
|
|
143
|
+
rows: dropped.rows
|
|
252
144
|
};
|
|
253
145
|
results.push(result);
|
|
254
|
-
|
|
146
|
+
const line = "dropped a retired collection";
|
|
147
|
+
if (dropped.rows > 0) logger.warn(`${line} — it still held rows`, { meta: {
|
|
148
|
+
...result,
|
|
149
|
+
reason: spec.reason
|
|
150
|
+
} });
|
|
151
|
+
else logger.info(line, { meta: {
|
|
255
152
|
...result,
|
|
256
153
|
reason: spec.reason
|
|
257
154
|
} });
|
|
@@ -464,6 +361,9 @@ function isImposedBound(source) {
|
|
|
464
361
|
}
|
|
465
362
|
//#endregion
|
|
466
363
|
//#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
|
|
364
|
+
function isCountRow(raw) {
|
|
365
|
+
return raw !== null && typeof raw === "object" && typeof Reflect.get(raw, "n") === "number";
|
|
366
|
+
}
|
|
467
367
|
function parseRowData(raw) {
|
|
468
368
|
return require_dist.asJsonObject(require_dist.parseJsonUnknown(raw)) ?? {};
|
|
469
369
|
}
|
|
@@ -1191,6 +1091,31 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
1191
1091
|
}
|
|
1192
1092
|
this.structuredTables.set(table, fingerprint);
|
|
1193
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Drop a table nothing declares any more, reporting what went with it.
|
|
1096
|
+
*
|
|
1097
|
+
* The DDL lives here because the engine owns the handle AND the two memos
|
|
1098
|
+
* that outlive a table: `structuredTables` (schema fingerprint) and
|
|
1099
|
+
* `declaredCollections` (the column set `compileFilter` resolves against).
|
|
1100
|
+
* Dropping behind their backs would leave a ghost — a later declaration of
|
|
1101
|
+
* the same name would be memoised as "already materialised" against a table
|
|
1102
|
+
* that no longer exists.
|
|
1103
|
+
*
|
|
1104
|
+
* WHICH tables are retired, and the logging, are policy: they live in
|
|
1105
|
+
* `retired-collections.ts`. This method knows only how to drop one.
|
|
1106
|
+
*
|
|
1107
|
+
* Answers `null` when the table is not there — the steady state after the
|
|
1108
|
+
* first boot that ran the purge, and the case on a fresh install.
|
|
1109
|
+
*/
|
|
1110
|
+
dropRetiredTable(table) {
|
|
1111
|
+
if (this.getDb().prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table) === void 0) return null;
|
|
1112
|
+
const counted = this.getDb().prepare(`SELECT COUNT(*) AS n FROM "${table}"`).get();
|
|
1113
|
+
const rows = isCountRow(counted) ? counted.n : 0;
|
|
1114
|
+
this.getDb().exec(`DROP TABLE "${table}"`);
|
|
1115
|
+
this.structuredTables.delete(table);
|
|
1116
|
+
this.declaredCollections.delete(table);
|
|
1117
|
+
return { rows };
|
|
1118
|
+
}
|
|
1194
1119
|
async tableInsert(table, row) {
|
|
1195
1120
|
const keys = Object.keys(row);
|
|
1196
1121
|
const cols = keys.map((k) => `"${k}"`).join(", ");
|
|
@@ -1823,10 +1748,11 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
|
|
|
1823
1748
|
this.backend = new SqliteSettingsBackend(dbPath, { ...require_dist.RUNTIME_DEFAULTS }, this.ctx.logger.child("Query"));
|
|
1824
1749
|
await this.backend.initialize();
|
|
1825
1750
|
try {
|
|
1826
|
-
await purgeRetiredSettingsKeys(retiredKeyStoreOf(this.backend), this.ctx.logger.child("RetiredKeys"));
|
|
1751
|
+
await require_retired_settings_keys.purgeRetiredSettingsKeys(require_retired_settings_keys.retiredKeyStoreOf(this.backend), this.ctx.logger.child("RetiredKeys"));
|
|
1827
1752
|
} catch (err) {
|
|
1828
1753
|
this.ctx.logger.warn("Retired-key purge failed", { meta: { error: require_dist.errMsg(err) } });
|
|
1829
1754
|
}
|
|
1755
|
+
await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
|
|
1830
1756
|
const vectorIndex = this.createVectorIndex(this.backend);
|
|
1831
1757
|
await vectorIndex.loadIndexRegistry();
|
|
1832
1758
|
this.vectorIndex = vectorIndex;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { A as dataStoreProviderCapability, Bt as parseJsonUnknown, _t as vectorStoreCapability, f as RUNTIME_DEFAULTS, gt as vectorDimFromBase64, j as decodeVectorBase64, kt as asJsonObject, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-_oC_QkQA.mjs";
|
|
2
|
+
import { r as retiredKeyStoreOf, t as purgeRetiredSettingsKeys } from "../../retired-settings-keys-Bsjf7HQ-.mjs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { statSync } from "node:fs";
|
|
@@ -78,174 +79,70 @@ var SQLITE_MMAP_SIZE_BYTES = 268435456;
|
|
|
78
79
|
*/
|
|
79
80
|
var SQLITE_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
|
|
80
81
|
//#endregion
|
|
81
|
-
//#region src/builtins/sqlite-storage/retired-
|
|
82
|
+
//#region src/builtins/sqlite-storage/retired-collections.ts
|
|
82
83
|
/**
|
|
83
|
-
*
|
|
84
|
+
* The dead tables.
|
|
84
85
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* write to a store this node does not own. Only one of those is recoverable.
|
|
86
|
+
* `devices` / `device_settings_kv` — the device half of the
|
|
87
|
+
* integration-registry ([D183](../../../../../docs/decisions/adr-0183.md)).
|
|
88
|
+
* The registry's INTEGRATION half is live (`integrations`,
|
|
89
|
+
* `integration_settings`); its device half never had a production caller and
|
|
90
|
+
* never had a row. The fleet lives in `device-manager:devices`, under numeric
|
|
91
|
+
* ids, while these tables key on `dev_NNNN` — the two id-spaces could not be
|
|
92
|
+
* reconciled even if something had wanted to.
|
|
93
93
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The dead keys, per row.
|
|
100
|
-
*
|
|
101
|
-
* `detection-pipeline` / `addon-settings` / `root` — every key in this row is
|
|
102
|
-
* dead; the row itself is kept because the addon writes live keys into it
|
|
103
|
-
* (`pipelineTemplates`, `videoPipelineSteps`).
|
|
104
|
-
*
|
|
105
|
-
* - `pipelineSteps` / `pipelineEngine`: the persisted GLOBAL step + engine
|
|
106
|
-
* seed. Removed with `getGlobalSteps` becoming pure — dispatch builds steps
|
|
107
|
-
* from the orchestrator's `agentSettings` plus per-camera overrides, and
|
|
108
|
-
* never consulted the seed. See `addon-pipeline/src/detection-pipeline/provider.ts`.
|
|
109
|
-
* - `engineRuntime` / `engineBackend` / `engineDevice` / `probedBestEngine`:
|
|
110
|
-
* the per-node engine CASCADE, removed with the operator-facing engine
|
|
111
|
-
* election. Each node now runs a device-array of inference pools balanced
|
|
112
|
-
* per session, and the node-default pool derives live from the node's own
|
|
113
|
-
* hardware (`resolveAutoEngine`). See
|
|
114
|
-
* `addon-pipeline/src/detection-pipeline/engine-store-keys.ts`.
|
|
115
|
-
*/
|
|
116
|
-
var RETIRED_SETTINGS_KEYS = [{
|
|
117
|
-
namespace: "detection-pipeline",
|
|
118
|
-
collection: "addon-settings",
|
|
119
|
-
row: "root",
|
|
120
|
-
keys: ["pipelineSteps", "pipelineEngine"],
|
|
121
|
-
perNodeKeys: [
|
|
122
|
-
"engineRuntime",
|
|
123
|
-
"engineBackend",
|
|
124
|
-
"engineDevice",
|
|
125
|
-
"probedBestEngine"
|
|
126
|
-
],
|
|
127
|
-
reason: "global step/engine seed and the per-node engine cascade — both removed; the live per-camera dispatch path never read either"
|
|
94
|
+
var RETIRED_COLLECTIONS = [{
|
|
95
|
+
collection: "devices",
|
|
96
|
+
owner: "integration-registry",
|
|
97
|
+
reason: "the device half of the integration-registry is deleted; the fleet is `device-manager:devices` and always was (0 rows here against 974 live devices)"
|
|
128
98
|
}, {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
keys: [],
|
|
133
|
-
perNodeKeys: [],
|
|
134
|
-
mapValueKeys: ["deviceLinks"],
|
|
135
|
-
reason: "cross-device field wiring, deleted 2026-08-08 — 0 links across 302 devices a month after the UI shipped. There is no reader left: the resolver, the overlay, the cycle guard, the fifth binding kind and the tab are gone"
|
|
99
|
+
collection: "device_settings_kv",
|
|
100
|
+
owner: "integration-registry",
|
|
101
|
+
reason: "per-device settings of a device table that never had a row; the live per-camera store is the device-manager row plus `addon-device-settings`"
|
|
136
102
|
}];
|
|
137
103
|
/**
|
|
138
|
-
* Adapt the
|
|
104
|
+
* Adapt the SQLite engine to {@link RetiredCollectionStore}.
|
|
139
105
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* routes it to the "read failed, row left untouched" branch.
|
|
106
|
+
* The DDL itself is the engine's — it owns the handle and the memos a drop has
|
|
107
|
+
* to evict. This module owns only the list and the reporting.
|
|
143
108
|
*/
|
|
144
|
-
function
|
|
145
|
-
return {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
namespace: spec.namespace,
|
|
149
|
-
collection: spec.collection,
|
|
150
|
-
key: spec.row
|
|
151
|
-
}));
|
|
152
|
-
},
|
|
153
|
-
async write(spec, value) {
|
|
154
|
-
await backend.set({
|
|
155
|
-
namespace: spec.namespace,
|
|
156
|
-
collection: spec.collection,
|
|
157
|
-
key: spec.row,
|
|
158
|
-
value
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
};
|
|
109
|
+
function retiredCollectionStoreOf(backend) {
|
|
110
|
+
return { async drop(collection) {
|
|
111
|
+
return backend.dropRetiredTable(collection);
|
|
112
|
+
} };
|
|
162
113
|
}
|
|
163
114
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
115
|
+
* Drop every retired table that is still present. Returns one entry per table
|
|
116
|
+
* actually dropped — empty when the schema is already clean, which is the
|
|
117
|
+
* steady state after the first boot that ran it.
|
|
166
118
|
*/
|
|
167
|
-
function
|
|
168
|
-
const nestedKeys = spec.mapValueKeys ?? [];
|
|
169
|
-
if (nestedKeys.length === 0) return [];
|
|
170
|
-
const found = [];
|
|
171
|
-
for (const [mapKey, value] of Object.entries(blob)) {
|
|
172
|
-
const inner = asJsonObject(value);
|
|
173
|
-
if (inner === null) continue;
|
|
174
|
-
for (const retired of nestedKeys) if (retired in inner) found.push(`${mapKey}.${retired}`);
|
|
175
|
-
}
|
|
176
|
-
return found;
|
|
177
|
-
}
|
|
178
|
-
/** Keys of `blob` that `spec` retires. Pure. */
|
|
179
|
-
function planRetiredKeyPurge(blob, spec) {
|
|
180
|
-
const exact = new Set(spec.keys);
|
|
181
|
-
const found = [];
|
|
182
|
-
for (const key of Object.keys(blob)) {
|
|
183
|
-
if (exact.has(key)) {
|
|
184
|
-
found.push(key);
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
for (const perNode of spec.perNodeKeys) if (key === perNode || key.startsWith(`${perNode}@`)) {
|
|
188
|
-
found.push(key);
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return found;
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Delete every retired key present in the declared rows — on the hub.
|
|
196
|
-
*
|
|
197
|
-
* Off the authoritative node this is a no-op that touches nothing and says
|
|
198
|
-
* nothing (see the module header). Returns one entry per row that actually
|
|
199
|
-
* changed — empty when the store is already clean, which is the steady state
|
|
200
|
-
* after the first boot that ran it.
|
|
201
|
-
*/
|
|
202
|
-
async function purgeRetiredSettingsKeys(store, logger, specs = RETIRED_SETTINGS_KEYS, env = process.env) {
|
|
203
|
-
if (!settingsStoreIsAuthoritativeHere(env)) return [];
|
|
119
|
+
async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
|
|
204
120
|
const results = [];
|
|
205
121
|
for (const spec of specs) {
|
|
206
|
-
let
|
|
122
|
+
let dropped;
|
|
207
123
|
try {
|
|
208
|
-
|
|
124
|
+
dropped = await store.drop(spec.collection);
|
|
209
125
|
} catch (err) {
|
|
210
|
-
logger.warn("retired
|
|
211
|
-
addon: spec.
|
|
126
|
+
logger.warn("retired collection — drop failed, table left in place", { meta: {
|
|
127
|
+
addon: spec.owner,
|
|
212
128
|
collection: spec.collection,
|
|
213
|
-
row: spec.row,
|
|
214
129
|
error: err instanceof Error ? err.message : String(err)
|
|
215
130
|
} });
|
|
216
131
|
continue;
|
|
217
132
|
}
|
|
218
|
-
if (
|
|
219
|
-
const doomed = planRetiredKeyPurge(blob, spec);
|
|
220
|
-
const doomedNested = planNestedRetiredKeyPurge(blob, spec);
|
|
221
|
-
if (doomed.length === 0 && doomedNested.length === 0) continue;
|
|
222
|
-
const doomedSet = new Set(doomed);
|
|
223
|
-
const nestedByMapKey = /* @__PURE__ */ new Map();
|
|
224
|
-
for (const path of doomedNested) {
|
|
225
|
-
const cut = path.indexOf(".");
|
|
226
|
-
const mapKey = path.slice(0, cut);
|
|
227
|
-
const inner = path.slice(cut + 1);
|
|
228
|
-
const bucket = nestedByMapKey.get(mapKey) ?? /* @__PURE__ */ new Set();
|
|
229
|
-
bucket.add(inner);
|
|
230
|
-
nestedByMapKey.set(mapKey, bucket);
|
|
231
|
-
}
|
|
232
|
-
const next = Object.fromEntries(Object.entries(blob).filter(([key]) => !doomedSet.has(key)).map(([key, value]) => {
|
|
233
|
-
const retired = nestedByMapKey.get(key);
|
|
234
|
-
if (retired === void 0) return [key, value];
|
|
235
|
-
const inner = asJsonObject(value);
|
|
236
|
-
if (inner === null) return [key, value];
|
|
237
|
-
return [key, Object.fromEntries(Object.entries(inner).filter(([k]) => !retired.has(k)))];
|
|
238
|
-
}));
|
|
239
|
-
await store.write(spec, next);
|
|
133
|
+
if (dropped === null) continue;
|
|
240
134
|
const result = {
|
|
241
|
-
addon: spec.namespace,
|
|
242
135
|
collection: spec.collection,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
nested: doomedNested
|
|
136
|
+
owner: spec.owner,
|
|
137
|
+
rows: dropped.rows
|
|
246
138
|
};
|
|
247
139
|
results.push(result);
|
|
248
|
-
|
|
140
|
+
const line = "dropped a retired collection";
|
|
141
|
+
if (dropped.rows > 0) logger.warn(`${line} — it still held rows`, { meta: {
|
|
142
|
+
...result,
|
|
143
|
+
reason: spec.reason
|
|
144
|
+
} });
|
|
145
|
+
else logger.info(line, { meta: {
|
|
249
146
|
...result,
|
|
250
147
|
reason: spec.reason
|
|
251
148
|
} });
|
|
@@ -458,6 +355,9 @@ function isImposedBound(source) {
|
|
|
458
355
|
}
|
|
459
356
|
//#endregion
|
|
460
357
|
//#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
|
|
358
|
+
function isCountRow(raw) {
|
|
359
|
+
return raw !== null && typeof raw === "object" && typeof Reflect.get(raw, "n") === "number";
|
|
360
|
+
}
|
|
461
361
|
function parseRowData(raw) {
|
|
462
362
|
return asJsonObject(parseJsonUnknown(raw)) ?? {};
|
|
463
363
|
}
|
|
@@ -1185,6 +1085,31 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
1185
1085
|
}
|
|
1186
1086
|
this.structuredTables.set(table, fingerprint);
|
|
1187
1087
|
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Drop a table nothing declares any more, reporting what went with it.
|
|
1090
|
+
*
|
|
1091
|
+
* The DDL lives here because the engine owns the handle AND the two memos
|
|
1092
|
+
* that outlive a table: `structuredTables` (schema fingerprint) and
|
|
1093
|
+
* `declaredCollections` (the column set `compileFilter` resolves against).
|
|
1094
|
+
* Dropping behind their backs would leave a ghost — a later declaration of
|
|
1095
|
+
* the same name would be memoised as "already materialised" against a table
|
|
1096
|
+
* that no longer exists.
|
|
1097
|
+
*
|
|
1098
|
+
* WHICH tables are retired, and the logging, are policy: they live in
|
|
1099
|
+
* `retired-collections.ts`. This method knows only how to drop one.
|
|
1100
|
+
*
|
|
1101
|
+
* Answers `null` when the table is not there — the steady state after the
|
|
1102
|
+
* first boot that ran the purge, and the case on a fresh install.
|
|
1103
|
+
*/
|
|
1104
|
+
dropRetiredTable(table) {
|
|
1105
|
+
if (this.getDb().prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table) === void 0) return null;
|
|
1106
|
+
const counted = this.getDb().prepare(`SELECT COUNT(*) AS n FROM "${table}"`).get();
|
|
1107
|
+
const rows = isCountRow(counted) ? counted.n : 0;
|
|
1108
|
+
this.getDb().exec(`DROP TABLE "${table}"`);
|
|
1109
|
+
this.structuredTables.delete(table);
|
|
1110
|
+
this.declaredCollections.delete(table);
|
|
1111
|
+
return { rows };
|
|
1112
|
+
}
|
|
1188
1113
|
async tableInsert(table, row) {
|
|
1189
1114
|
const keys = Object.keys(row);
|
|
1190
1115
|
const cols = keys.map((k) => `"${k}"`).join(", ");
|
|
@@ -1821,6 +1746,7 @@ var SqliteSettingsAddon = class extends BaseAddon {
|
|
|
1821
1746
|
} catch (err) {
|
|
1822
1747
|
this.ctx.logger.warn("Retired-key purge failed", { meta: { error: errMsg(err) } });
|
|
1823
1748
|
}
|
|
1749
|
+
await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
|
|
1824
1750
|
const vectorIndex = this.createVectorIndex(this.backend);
|
|
1825
1751
|
await vectorIndex.loadIndexRegistry();
|
|
1826
1752
|
this.vectorIndex = vectorIndex;
|