@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
package/dist/index.mjs
CHANGED
|
@@ -2144,13 +2144,9 @@ function readInfo(raw) {
|
|
|
2144
2144
|
return {};
|
|
2145
2145
|
}
|
|
2146
2146
|
var integrationCounter = 0;
|
|
2147
|
-
var deviceCounter = 0;
|
|
2148
2147
|
function nextIntegrationId() {
|
|
2149
2148
|
return `int_${String(++integrationCounter).padStart(4, "0")}`;
|
|
2150
2149
|
}
|
|
2151
|
-
function nextDeviceId() {
|
|
2152
|
-
return `dev_${String(++deviceCounter).padStart(4, "0")}`;
|
|
2153
|
-
}
|
|
2154
2150
|
var INTEGRATIONS_COLUMNS = [
|
|
2155
2151
|
{
|
|
2156
2152
|
name: "id",
|
|
@@ -2217,95 +2213,6 @@ var INTEGRATION_SETTINGS_COLUMNS = [
|
|
|
2217
2213
|
defaultValue: "string"
|
|
2218
2214
|
}
|
|
2219
2215
|
];
|
|
2220
|
-
var DEVICES_COLUMNS = [
|
|
2221
|
-
{
|
|
2222
|
-
name: "id",
|
|
2223
|
-
type: "TEXT",
|
|
2224
|
-
primaryKey: true
|
|
2225
|
-
},
|
|
2226
|
-
{
|
|
2227
|
-
name: "integration_id",
|
|
2228
|
-
type: "TEXT",
|
|
2229
|
-
notNull: true
|
|
2230
|
-
},
|
|
2231
|
-
{
|
|
2232
|
-
name: "stable_id",
|
|
2233
|
-
type: "TEXT",
|
|
2234
|
-
notNull: true,
|
|
2235
|
-
unique: true
|
|
2236
|
-
},
|
|
2237
|
-
{
|
|
2238
|
-
name: "type",
|
|
2239
|
-
type: "TEXT",
|
|
2240
|
-
notNull: true,
|
|
2241
|
-
defaultValue: "camera"
|
|
2242
|
-
},
|
|
2243
|
-
{
|
|
2244
|
-
name: "name",
|
|
2245
|
-
type: "TEXT",
|
|
2246
|
-
notNull: true
|
|
2247
|
-
},
|
|
2248
|
-
{
|
|
2249
|
-
name: "enabled",
|
|
2250
|
-
type: "INTEGER",
|
|
2251
|
-
notNull: true,
|
|
2252
|
-
defaultValue: 1
|
|
2253
|
-
},
|
|
2254
|
-
{
|
|
2255
|
-
name: "info",
|
|
2256
|
-
type: "JSON",
|
|
2257
|
-
notNull: true,
|
|
2258
|
-
defaultValue: "{}"
|
|
2259
|
-
},
|
|
2260
|
-
{
|
|
2261
|
-
name: "created_at",
|
|
2262
|
-
type: "INTEGER",
|
|
2263
|
-
notNull: true
|
|
2264
|
-
},
|
|
2265
|
-
{
|
|
2266
|
-
name: "updated_at",
|
|
2267
|
-
type: "INTEGER",
|
|
2268
|
-
notNull: true
|
|
2269
|
-
}
|
|
2270
|
-
];
|
|
2271
|
-
var DEVICES_INDEXES = [
|
|
2272
|
-
{
|
|
2273
|
-
name: "idx_devices_integration",
|
|
2274
|
-
columns: ["integration_id"]
|
|
2275
|
-
},
|
|
2276
|
-
{
|
|
2277
|
-
name: "idx_devices_stable",
|
|
2278
|
-
columns: ["stable_id"],
|
|
2279
|
-
unique: true
|
|
2280
|
-
},
|
|
2281
|
-
{
|
|
2282
|
-
name: "idx_devices_type",
|
|
2283
|
-
columns: ["type"]
|
|
2284
|
-
}
|
|
2285
|
-
];
|
|
2286
|
-
var DEVICE_SETTINGS_COLUMNS = [
|
|
2287
|
-
{
|
|
2288
|
-
name: "device_id",
|
|
2289
|
-
type: "TEXT",
|
|
2290
|
-
notNull: true
|
|
2291
|
-
},
|
|
2292
|
-
{
|
|
2293
|
-
name: "key",
|
|
2294
|
-
type: "TEXT",
|
|
2295
|
-
notNull: true
|
|
2296
|
-
},
|
|
2297
|
-
{
|
|
2298
|
-
name: "value",
|
|
2299
|
-
type: "TEXT",
|
|
2300
|
-
notNull: true
|
|
2301
|
-
},
|
|
2302
|
-
{
|
|
2303
|
-
name: "value_type",
|
|
2304
|
-
type: "TEXT",
|
|
2305
|
-
notNull: true,
|
|
2306
|
-
defaultValue: "string"
|
|
2307
|
-
}
|
|
2308
|
-
];
|
|
2309
2216
|
/**
|
|
2310
2217
|
* @durable class=config owner=integration-registry
|
|
2311
2218
|
* write="`createIntegration` — an operator adds a provider, or an addon declares
|
|
@@ -2324,29 +2231,19 @@ var INTEGRATIONS = "integrations";
|
|
|
2324
2231
|
*/
|
|
2325
2232
|
var INTEGRATION_SETTINGS = "integration_settings";
|
|
2326
2233
|
/**
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
* same physical camera from creating a second device row. An insert that
|
|
2330
|
-
* collides throws rather than duplicating.
|
|
2234
|
+
* The persisted INTEGRATIONS — one row per provider instance an operator (or a
|
|
2235
|
+
* declaring addon) added, plus that instance's settings.
|
|
2331
2236
|
*
|
|
2332
|
-
*
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
*
|
|
2336
|
-
*
|
|
2337
|
-
*
|
|
2338
|
-
*
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
/**
|
|
2342
|
-
* @durable class=config owner=integration-registry
|
|
2343
|
-
* write="`setDeviceSetting` — one row per (device_id, key), deleted and
|
|
2344
|
-
* re-inserted on every write; per-camera operator intent lands here"
|
|
2345
|
-
* retention="cascade only — `deleteDevice` and `deleteIntegration` remove a
|
|
2346
|
-
* device's rows. No sweep; losing the table silently reverts every camera to
|
|
2347
|
-
* its defaults."
|
|
2237
|
+
* It had a device half once: `devices` + `device_settings_kv`, `dev_NNNN` ids,
|
|
2238
|
+
* `createDevice` / `listCameras` / `setDeviceSetting`. It was removed on
|
|
2239
|
+
* 2026-08-19 with both tables ([D183](../../../../../docs/decisions/adr-0183-code-that-is-deleted-takes-its-tables-with-it.md)):
|
|
2240
|
+
* measured on the reference hub it held 0 rows against 974 live devices, no
|
|
2241
|
+
* production caller ever reached it, and its id-space could not be reconciled
|
|
2242
|
+
* with the numeric ids every track, rule and binding in the system uses. The
|
|
2243
|
+
* fleet is `device-manager:devices`. Do not add a device method here — the
|
|
2244
|
+
* device authority is the device-manager, and a second one that agrees with
|
|
2245
|
+
* nothing is how this half came to exist.
|
|
2348
2246
|
*/
|
|
2349
|
-
var DEVICE_SETTINGS = "device_settings_kv";
|
|
2350
2247
|
var IntegrationRegistry = class IntegrationRegistry {
|
|
2351
2248
|
store;
|
|
2352
2249
|
constructor(backend) {
|
|
@@ -2362,15 +2259,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2362
2259
|
collection: INTEGRATION_SETTINGS,
|
|
2363
2260
|
columns: INTEGRATION_SETTINGS_COLUMNS
|
|
2364
2261
|
});
|
|
2365
|
-
await this.store.declareCollection({
|
|
2366
|
-
collection: DEVICES,
|
|
2367
|
-
columns: DEVICES_COLUMNS,
|
|
2368
|
-
indexes: DEVICES_INDEXES
|
|
2369
|
-
});
|
|
2370
|
-
await this.store.declareCollection({
|
|
2371
|
-
collection: DEVICE_SETTINGS,
|
|
2372
|
-
columns: DEVICE_SETTINGS_COLUMNS
|
|
2373
|
-
});
|
|
2374
2262
|
const ints = await this.store.query({
|
|
2375
2263
|
collection: INTEGRATIONS,
|
|
2376
2264
|
filter: {
|
|
@@ -2385,20 +2273,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2385
2273
|
const num = parseInt(ints[0].id.replace("int_", ""), 10);
|
|
2386
2274
|
if (!isNaN(num)) integrationCounter = num;
|
|
2387
2275
|
}
|
|
2388
|
-
const devs = await this.store.query({
|
|
2389
|
-
collection: DEVICES,
|
|
2390
|
-
filter: {
|
|
2391
|
-
orderBy: {
|
|
2392
|
-
field: "id",
|
|
2393
|
-
direction: "desc"
|
|
2394
|
-
},
|
|
2395
|
-
limit: 1
|
|
2396
|
-
}
|
|
2397
|
-
});
|
|
2398
|
-
if (devs[0]) {
|
|
2399
|
-
const num = parseInt(devs[0].id.replace("dev_", ""), 10);
|
|
2400
|
-
if (!isNaN(num)) deviceCounter = num;
|
|
2401
|
-
}
|
|
2402
2276
|
}
|
|
2403
2277
|
async createIntegration(input) {
|
|
2404
2278
|
const id = nextIntegrationId();
|
|
@@ -2482,15 +2356,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2482
2356
|
async deleteIntegration(id) {
|
|
2483
2357
|
const integration = await this.getIntegration(id);
|
|
2484
2358
|
if (integration !== null && IntegrationRegistry.isFixed(integration)) throw new Error(`integration "${id}" is fixed — it belongs to the addon that declared it and cannot be deleted`);
|
|
2485
|
-
const devices = await this.listDevices(id);
|
|
2486
|
-
for (const d of devices) await this.store.deleteWhere({
|
|
2487
|
-
collection: DEVICE_SETTINGS,
|
|
2488
|
-
filter: { where: { device_id: d.id } }
|
|
2489
|
-
});
|
|
2490
|
-
await this.store.deleteWhere({
|
|
2491
|
-
collection: DEVICES,
|
|
2492
|
-
filter: { where: { integration_id: id } }
|
|
2493
|
-
});
|
|
2494
2359
|
await this.store.deleteWhere({
|
|
2495
2360
|
collection: INTEGRATION_SETTINGS,
|
|
2496
2361
|
filter: { where: { integration_id: id } }
|
|
@@ -2535,139 +2400,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2535
2400
|
async setIntegrationSettings(integrationId, settings) {
|
|
2536
2401
|
for (const [key, value] of Object.entries(settings)) await this.setIntegrationSetting(integrationId, key, value);
|
|
2537
2402
|
}
|
|
2538
|
-
async createDevice(input) {
|
|
2539
|
-
const id = nextDeviceId();
|
|
2540
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2541
|
-
await this.store.insert({
|
|
2542
|
-
collection: DEVICES,
|
|
2543
|
-
record: {
|
|
2544
|
-
id,
|
|
2545
|
-
data: {
|
|
2546
|
-
integration_id: input.integrationId,
|
|
2547
|
-
stable_id: input.stableId,
|
|
2548
|
-
type: input.type,
|
|
2549
|
-
name: input.name,
|
|
2550
|
-
enabled: input.enabled !== false ? 1 : 0,
|
|
2551
|
-
info: input.info ?? {},
|
|
2552
|
-
created_at: now,
|
|
2553
|
-
updated_at: now
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
});
|
|
2557
|
-
if (input.settings) await this.setDeviceSettings(id, input.settings);
|
|
2558
|
-
return {
|
|
2559
|
-
id,
|
|
2560
|
-
integrationId: input.integrationId,
|
|
2561
|
-
stableId: input.stableId,
|
|
2562
|
-
type: input.type,
|
|
2563
|
-
name: input.name,
|
|
2564
|
-
enabled: input.enabled !== false,
|
|
2565
|
-
info: input.info ?? {},
|
|
2566
|
-
createdAt: now,
|
|
2567
|
-
updatedAt: now
|
|
2568
|
-
};
|
|
2569
|
-
}
|
|
2570
|
-
async getDevice(id) {
|
|
2571
|
-
const rows = await this.store.query({
|
|
2572
|
-
collection: DEVICES,
|
|
2573
|
-
filter: {
|
|
2574
|
-
where: { id },
|
|
2575
|
-
limit: 1
|
|
2576
|
-
}
|
|
2577
|
-
});
|
|
2578
|
-
return rows[0] ? this.mapDevice(rows[0].data) : null;
|
|
2579
|
-
}
|
|
2580
|
-
async getDeviceByStableId(stableId) {
|
|
2581
|
-
const rows = await this.store.query({
|
|
2582
|
-
collection: DEVICES,
|
|
2583
|
-
filter: {
|
|
2584
|
-
where: { stable_id: stableId },
|
|
2585
|
-
limit: 1
|
|
2586
|
-
}
|
|
2587
|
-
});
|
|
2588
|
-
return rows[0] ? this.mapDevice(rows[0].data) : null;
|
|
2589
|
-
}
|
|
2590
|
-
async listDevices(integrationId) {
|
|
2591
|
-
return (await this.store.query({
|
|
2592
|
-
collection: DEVICES,
|
|
2593
|
-
filter: {
|
|
2594
|
-
...integrationId ? { where: { integration_id: integrationId } } : {},
|
|
2595
|
-
orderBy: {
|
|
2596
|
-
field: "created_at",
|
|
2597
|
-
direction: "asc"
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
})).map((r) => this.mapDevice(r.data));
|
|
2601
|
-
}
|
|
2602
|
-
async listCameras() {
|
|
2603
|
-
return (await this.store.query({
|
|
2604
|
-
collection: DEVICES,
|
|
2605
|
-
filter: {
|
|
2606
|
-
where: { type: "camera" },
|
|
2607
|
-
orderBy: {
|
|
2608
|
-
field: "created_at",
|
|
2609
|
-
direction: "asc"
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
})).map((r) => this.mapDevice(r.data));
|
|
2613
|
-
}
|
|
2614
|
-
async updateDevice(id, updates) {
|
|
2615
|
-
const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
|
|
2616
|
-
if (updates.name !== void 0) updateRow["name"] = updates.name;
|
|
2617
|
-
if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
|
|
2618
|
-
if (updates.info !== void 0) updateRow["info"] = updates.info;
|
|
2619
|
-
await this.store.updateWhere({
|
|
2620
|
-
collection: DEVICES,
|
|
2621
|
-
filter: { where: { id } },
|
|
2622
|
-
data: updateRow
|
|
2623
|
-
});
|
|
2624
|
-
return this.getDevice(id);
|
|
2625
|
-
}
|
|
2626
|
-
async deleteDevice(id) {
|
|
2627
|
-
await this.store.deleteWhere({
|
|
2628
|
-
collection: DEVICE_SETTINGS,
|
|
2629
|
-
filter: { where: { device_id: id } }
|
|
2630
|
-
});
|
|
2631
|
-
await this.store.deleteWhere({
|
|
2632
|
-
collection: DEVICES,
|
|
2633
|
-
filter: { where: { id } }
|
|
2634
|
-
});
|
|
2635
|
-
return true;
|
|
2636
|
-
}
|
|
2637
|
-
async getDeviceSettings(deviceId) {
|
|
2638
|
-
const result = {};
|
|
2639
|
-
const rows = await this.store.query({
|
|
2640
|
-
collection: DEVICE_SETTINGS,
|
|
2641
|
-
filter: { where: { device_id: deviceId } }
|
|
2642
|
-
});
|
|
2643
|
-
for (const row of rows) result[String(row.data["key"])] = deserializeSetting(row.data["value"], String(row.data["value_type"]));
|
|
2644
|
-
return result;
|
|
2645
|
-
}
|
|
2646
|
-
async setDeviceSetting(deviceId, key, value) {
|
|
2647
|
-
const s = serializeSetting(value);
|
|
2648
|
-
await this.store.deleteWhere({
|
|
2649
|
-
collection: DEVICE_SETTINGS,
|
|
2650
|
-
filter: { where: {
|
|
2651
|
-
device_id: deviceId,
|
|
2652
|
-
key
|
|
2653
|
-
} }
|
|
2654
|
-
});
|
|
2655
|
-
await this.store.insert({
|
|
2656
|
-
collection: DEVICE_SETTINGS,
|
|
2657
|
-
record: {
|
|
2658
|
-
id: `${deviceId}:${key}`,
|
|
2659
|
-
data: {
|
|
2660
|
-
device_id: deviceId,
|
|
2661
|
-
key,
|
|
2662
|
-
value: s.value,
|
|
2663
|
-
value_type: s.valueType
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
});
|
|
2667
|
-
}
|
|
2668
|
-
async setDeviceSettings(deviceId, settings) {
|
|
2669
|
-
for (const [key, value] of Object.entries(settings)) await this.setDeviceSetting(deviceId, key, value);
|
|
2670
|
-
}
|
|
2671
2403
|
mapIntegration(row) {
|
|
2672
2404
|
return {
|
|
2673
2405
|
id: String(row["id"]),
|
|
@@ -2679,19 +2411,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2679
2411
|
updatedAt: Number(row["updated_at"])
|
|
2680
2412
|
};
|
|
2681
2413
|
}
|
|
2682
|
-
mapDevice(row) {
|
|
2683
|
-
return {
|
|
2684
|
-
id: String(row["id"]),
|
|
2685
|
-
integrationId: String(row["integration_id"]),
|
|
2686
|
-
stableId: String(row["stable_id"]),
|
|
2687
|
-
type: String(row["type"]),
|
|
2688
|
-
name: String(row["name"]),
|
|
2689
|
-
enabled: row["enabled"] === 1,
|
|
2690
|
-
info: readInfo(row["info"]),
|
|
2691
|
-
createdAt: Number(row["created_at"]),
|
|
2692
|
-
updatedAt: Number(row["updated_at"])
|
|
2693
|
-
};
|
|
2694
|
-
}
|
|
2695
2414
|
};
|
|
2696
2415
|
//#endregion
|
|
2697
2416
|
//#region src/kernel/deps/ensure-native-prebuilds.ts
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { kt as asJsonObject } from "./dist-_oC_QkQA.mjs";
|
|
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 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 = 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 = 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
|
+
export { purgeRetiredSettingsRows as n, retiredKeyStoreOf as r, purgeRetiredSettingsKeys as t };
|