@camstack/system 1.2.108 → 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.
@@ -3810,13 +3810,32 @@ var DeviceMetaStore = class {
3810
3810
  //#endregion
3811
3811
  //#region src/builtins/device-manager/device-row-store.ts
3812
3812
  /**
3813
- * @durable class=registry owner=device-manager
3813
+ * @durable class=ledger owner=device-manager
3814
3814
  * write="one row per device, written by `allocateDeviceId` (identity placeholder),
3815
3815
  * `registerDevice` (full reconcile) and every meta setter (single-column patch);
3816
3816
  * `setMetadata` patches the `metadata` column of the same row"
3817
3817
  * retention="none — a row goes only when the operator removes the device
3818
3818
  * (`removeDevice`), or when an integration is deleted and cascades. Bounded by the
3819
3819
  * fleet an operator configures (974 rows on the reference hub)."
3820
+ *
3821
+ * ### Why `ledger` and not `config`
3822
+ *
3823
+ * This shipped as `class=registry`, a sixth class that does not exist —
3824
+ * `DURABLE_CLASSES` is `ledger | mirror | ephemeral | config | audit`, so
3825
+ * `check-durable-state-declared.ts` has failed since. `config` is the tempting
3826
+ * answer, because a device's `name` and `location` ARE operator intent. It is
3827
+ * the wrong one, and wrong in the dangerous direction: `config`'s rule is
3828
+ * *"already durable everywhere"*, and that is exactly the premise that made the
3829
+ * dead `devices` table look harmless right up to D183.
3830
+ *
3831
+ * Nothing else holds a device's NUMERIC id. `allocateDeviceId` mints it here
3832
+ * and every other store in the system keys on it — `addon-device-settings`,
3833
+ * `device-runtime-state`, every `pipeline-analytics:*` row, every recorder path.
3834
+ * That is `ledger`'s definition verbatim ("a fact only this component ever
3835
+ * held"), and `ledger`'s rule — *must not be pruned against a fallible read* —
3836
+ * is precisely the rule this collection needs (D49): a `getBindings` or an
3837
+ * integration listing that answers empty because it FAILED must never be
3838
+ * allowed to retire a device row.
3820
3839
  */
3821
3840
  var DEVICE_ROWS_COLLECTION = "device-manager:devices";
3822
3841
  /**
@@ -3805,13 +3805,32 @@ var DeviceMetaStore = class {
3805
3805
  //#endregion
3806
3806
  //#region src/builtins/device-manager/device-row-store.ts
3807
3807
  /**
3808
- * @durable class=registry owner=device-manager
3808
+ * @durable class=ledger owner=device-manager
3809
3809
  * write="one row per device, written by `allocateDeviceId` (identity placeholder),
3810
3810
  * `registerDevice` (full reconcile) and every meta setter (single-column patch);
3811
3811
  * `setMetadata` patches the `metadata` column of the same row"
3812
3812
  * retention="none — a row goes only when the operator removes the device
3813
3813
  * (`removeDevice`), or when an integration is deleted and cascades. Bounded by the
3814
3814
  * fleet an operator configures (974 rows on the reference hub)."
3815
+ *
3816
+ * ### Why `ledger` and not `config`
3817
+ *
3818
+ * This shipped as `class=registry`, a sixth class that does not exist —
3819
+ * `DURABLE_CLASSES` is `ledger | mirror | ephemeral | config | audit`, so
3820
+ * `check-durable-state-declared.ts` has failed since. `config` is the tempting
3821
+ * answer, because a device's `name` and `location` ARE operator intent. It is
3822
+ * the wrong one, and wrong in the dangerous direction: `config`'s rule is
3823
+ * *"already durable everywhere"*, and that is exactly the premise that made the
3824
+ * dead `devices` table look harmless right up to D183.
3825
+ *
3826
+ * Nothing else holds a device's NUMERIC id. `allocateDeviceId` mints it here
3827
+ * and every other store in the system keys on it — `addon-device-settings`,
3828
+ * `device-runtime-state`, every `pipeline-analytics:*` row, every recorder path.
3829
+ * That is `ledger`'s definition verbatim ("a fact only this component ever
3830
+ * held"), and `ledger`'s rule — *must not be pruned against a fallible read* —
3831
+ * is precisely the rule this collection needs (D49): a `getBindings` or an
3832
+ * integration listing that answers empty because it FAILED must never be
3833
+ * allowed to retire a device row.
3815
3834
  */
3816
3835
  var DEVICE_ROWS_COLLECTION = "device-manager:devices";
3817
3836
  /**
@@ -2,13 +2,32 @@ import { ChildLayout, CollectionColumn, CollectionIndex, DeviceDisplayOverride,
2
2
  import { RetiredRowStore } from '../sqlite-storage/retired-settings-keys.js';
3
3
  import { PersistedDeviceMeta } from './device-meta-types.js';
4
4
  /**
5
- * @durable class=registry owner=device-manager
5
+ * @durable class=ledger owner=device-manager
6
6
  * write="one row per device, written by `allocateDeviceId` (identity placeholder),
7
7
  * `registerDevice` (full reconcile) and every meta setter (single-column patch);
8
8
  * `setMetadata` patches the `metadata` column of the same row"
9
9
  * retention="none — a row goes only when the operator removes the device
10
10
  * (`removeDevice`), or when an integration is deleted and cascades. Bounded by the
11
11
  * fleet an operator configures (974 rows on the reference hub)."
12
+ *
13
+ * ### Why `ledger` and not `config`
14
+ *
15
+ * This shipped as `class=registry`, a sixth class that does not exist —
16
+ * `DURABLE_CLASSES` is `ledger | mirror | ephemeral | config | audit`, so
17
+ * `check-durable-state-declared.ts` has failed since. `config` is the tempting
18
+ * answer, because a device's `name` and `location` ARE operator intent. It is
19
+ * the wrong one, and wrong in the dangerous direction: `config`'s rule is
20
+ * *"already durable everywhere"*, and that is exactly the premise that made the
21
+ * dead `devices` table look harmless right up to D183.
22
+ *
23
+ * Nothing else holds a device's NUMERIC id. `allocateDeviceId` mints it here
24
+ * and every other store in the system keys on it — `addon-device-settings`,
25
+ * `device-runtime-state`, every `pipeline-analytics:*` row, every recorder path.
26
+ * That is `ledger`'s definition verbatim ("a fact only this component ever
27
+ * held"), and `ledger`'s rule — *must not be pruned against a fallible read* —
28
+ * is precisely the rule this collection needs (D49): a `getBindings` or an
29
+ * integration listing that answers empty because it FAILED must never be
30
+ * allowed to retire a device row.
12
31
  */
13
32
  export declare const DEVICE_ROWS_COLLECTION = "device-manager:devices";
14
33
  /**
@@ -1,4 +1,18 @@
1
- import { CreateDeviceInput, CreateIntegrationInput, IIntegrationRegistry, Integration, ISettingsStoreProvider, PersistedDevice } from '@camstack/types';
1
+ import { CreateIntegrationInput, IIntegrationRegistry, Integration, ISettingsStoreProvider } from '@camstack/types';
2
+ /**
3
+ * The persisted INTEGRATIONS — one row per provider instance an operator (or a
4
+ * declaring addon) added, plus that instance's settings.
5
+ *
6
+ * It had a device half once: `devices` + `device_settings_kv`, `dev_NNNN` ids,
7
+ * `createDevice` / `listCameras` / `setDeviceSetting`. It was removed on
8
+ * 2026-08-19 with both tables ([D183](../../../../../docs/decisions/adr-0183-code-that-is-deleted-takes-its-tables-with-it.md)):
9
+ * measured on the reference hub it held 0 rows against 974 live devices, no
10
+ * production caller ever reached it, and its id-space could not be reconciled
11
+ * with the numeric ids every track, rule and binding in the system uses. The
12
+ * fleet is `device-manager:devices`. Do not add a device method here — the
13
+ * device authority is the device-manager, and a second one that agrees with
14
+ * nothing is how this half came to exist.
15
+ */
2
16
  export declare class IntegrationRegistry implements IIntegrationRegistry {
3
17
  private readonly store;
4
18
  constructor(backend: ISettingsStoreProvider);
@@ -22,16 +36,5 @@ export declare class IntegrationRegistry implements IIntegrationRegistry {
22
36
  getIntegrationSettings(integrationId: string): Promise<Record<string, unknown>>;
23
37
  setIntegrationSetting(integrationId: string, key: string, value: unknown): Promise<void>;
24
38
  setIntegrationSettings(integrationId: string, settings: Record<string, unknown>): Promise<void>;
25
- createDevice(input: CreateDeviceInput): Promise<PersistedDevice>;
26
- getDevice(id: string): Promise<PersistedDevice | null>;
27
- getDeviceByStableId(stableId: string): Promise<PersistedDevice | null>;
28
- listDevices(integrationId?: string): Promise<readonly PersistedDevice[]>;
29
- listCameras(): Promise<readonly PersistedDevice[]>;
30
- updateDevice(id: string, updates: Partial<Pick<PersistedDevice, 'name' | 'enabled' | 'info'>>): Promise<PersistedDevice | null>;
31
- deleteDevice(id: string): Promise<boolean>;
32
- getDeviceSettings(deviceId: string): Promise<Record<string, unknown>>;
33
- setDeviceSetting(deviceId: string, key: string, value: unknown): Promise<void>;
34
- setDeviceSettings(deviceId: string, settings: Record<string, unknown>): Promise<void>;
35
39
  private mapIntegration;
36
- private mapDevice;
37
40
  }
@@ -0,0 +1,52 @@
1
+ import { RetiredKeyLogger } from './retired-settings-keys.js';
2
+ import { DroppedTable, SqliteSettingsBackend } from './sqlite-settings-backend.js';
3
+ /** One retired-table declaration. */
4
+ export interface RetiredCollectionSpec {
5
+ /**
6
+ * The table name as it exists on disk. These tables are NOT namespaced —
7
+ * the retired code declared them bare — so this is the literal name, not a
8
+ * `<namespace>:<collection>` pair to be resolved.
9
+ */
10
+ readonly collection: string;
11
+ /** The code that used to own it — reported, never used to address it. */
12
+ readonly owner: string;
13
+ /** Why it is dead. Cited in the log line, not re-derived at the call site. */
14
+ readonly reason: string;
15
+ }
16
+ /** What one drop destroyed. */
17
+ export interface RetiredCollectionDrop {
18
+ readonly collection: string;
19
+ readonly owner: string;
20
+ /** Rows the table held. Zero is the expected case and is still reported. */
21
+ readonly rows: number;
22
+ }
23
+ /** The engine access the purge needs. Narrow on purpose — trivially fakeable. */
24
+ export interface RetiredCollectionStore {
25
+ /** Drop `collection`, or answer `null` when it is not there. Throws on fault. */
26
+ drop(collection: string): Promise<DroppedTable | null>;
27
+ }
28
+ /**
29
+ * The dead tables.
30
+ *
31
+ * `devices` / `device_settings_kv` — the device half of the
32
+ * integration-registry ([D183](../../../../../docs/decisions/adr-0183.md)).
33
+ * The registry's INTEGRATION half is live (`integrations`,
34
+ * `integration_settings`); its device half never had a production caller and
35
+ * never had a row. The fleet lives in `device-manager:devices`, under numeric
36
+ * ids, while these tables key on `dev_NNNN` — the two id-spaces could not be
37
+ * reconciled even if something had wanted to.
38
+ */
39
+ export declare const RETIRED_COLLECTIONS: readonly RetiredCollectionSpec[];
40
+ /**
41
+ * Adapt the SQLite engine to {@link RetiredCollectionStore}.
42
+ *
43
+ * The DDL itself is the engine's — it owns the handle and the memos a drop has
44
+ * to evict. This module owns only the list and the reporting.
45
+ */
46
+ export declare function retiredCollectionStoreOf(backend: SqliteSettingsBackend): RetiredCollectionStore;
47
+ /**
48
+ * Drop every retired table that is still present. Returns one entry per table
49
+ * actually dropped — empty when the schema is already clean, which is the
50
+ * steady state after the first boot that ran it.
51
+ */
52
+ export declare function purgeRetiredCollections(store: RetiredCollectionStore, logger: RetiredKeyLogger, specs?: readonly RetiredCollectionSpec[]): Promise<readonly RetiredCollectionDrop[]>;
@@ -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;
@@ -85,6 +85,77 @@ var SQLITE_MMAP_SIZE_BYTES = 268435456;
85
85
  */
86
86
  var SQLITE_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
87
87
  //#endregion
88
+ //#region src/builtins/sqlite-storage/retired-collections.ts
89
+ /**
90
+ * The dead tables.
91
+ *
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
+ */
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)"
104
+ }, {
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`"
108
+ }];
109
+ /**
110
+ * Adapt the SQLite engine to {@link RetiredCollectionStore}.
111
+ *
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.
114
+ */
115
+ function retiredCollectionStoreOf(backend) {
116
+ return { async drop(collection) {
117
+ return backend.dropRetiredTable(collection);
118
+ } };
119
+ }
120
+ /**
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.
124
+ */
125
+ async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
126
+ const results = [];
127
+ for (const spec of specs) {
128
+ let dropped;
129
+ try {
130
+ dropped = await store.drop(spec.collection);
131
+ } catch (err) {
132
+ logger.warn("retired collection — drop failed, table left in place", { meta: {
133
+ addon: spec.owner,
134
+ collection: spec.collection,
135
+ error: err instanceof Error ? err.message : String(err)
136
+ } });
137
+ continue;
138
+ }
139
+ if (dropped === null) continue;
140
+ const result = {
141
+ collection: spec.collection,
142
+ owner: spec.owner,
143
+ rows: dropped.rows
144
+ };
145
+ results.push(result);
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: {
152
+ ...result,
153
+ reason: spec.reason
154
+ } });
155
+ }
156
+ return results;
157
+ }
158
+ //#endregion
88
159
  //#region src/builtins/sqlite-storage/filter-compiler.ts
89
160
  /** Thrown by `mutate` mode. Distinct type so a caller can map it to a 400. */
90
161
  var UnsafeFilterError = class extends Error {
@@ -290,6 +361,9 @@ function isImposedBound(source) {
290
361
  }
291
362
  //#endregion
292
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
+ }
293
367
  function parseRowData(raw) {
294
368
  return require_dist.asJsonObject(require_dist.parseJsonUnknown(raw)) ?? {};
295
369
  }
@@ -1017,6 +1091,31 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
1017
1091
  }
1018
1092
  this.structuredTables.set(table, fingerprint);
1019
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
+ }
1020
1119
  async tableInsert(table, row) {
1021
1120
  const keys = Object.keys(row);
1022
1121
  const cols = keys.map((k) => `"${k}"`).join(", ");
@@ -1653,6 +1752,7 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
1653
1752
  } catch (err) {
1654
1753
  this.ctx.logger.warn("Retired-key purge failed", { meta: { error: require_dist.errMsg(err) } });
1655
1754
  }
1755
+ await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
1656
1756
  const vectorIndex = this.createVectorIndex(this.backend);
1657
1757
  await vectorIndex.loadIndexRegistry();
1658
1758
  this.vectorIndex = vectorIndex;
@@ -79,6 +79,77 @@ var SQLITE_MMAP_SIZE_BYTES = 268435456;
79
79
  */
80
80
  var SQLITE_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
81
81
  //#endregion
82
+ //#region src/builtins/sqlite-storage/retired-collections.ts
83
+ /**
84
+ * The dead tables.
85
+ *
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
+ */
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)"
98
+ }, {
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`"
102
+ }];
103
+ /**
104
+ * Adapt the SQLite engine to {@link RetiredCollectionStore}.
105
+ *
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.
108
+ */
109
+ function retiredCollectionStoreOf(backend) {
110
+ return { async drop(collection) {
111
+ return backend.dropRetiredTable(collection);
112
+ } };
113
+ }
114
+ /**
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.
118
+ */
119
+ async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
120
+ const results = [];
121
+ for (const spec of specs) {
122
+ let dropped;
123
+ try {
124
+ dropped = await store.drop(spec.collection);
125
+ } catch (err) {
126
+ logger.warn("retired collection — drop failed, table left in place", { meta: {
127
+ addon: spec.owner,
128
+ collection: spec.collection,
129
+ error: err instanceof Error ? err.message : String(err)
130
+ } });
131
+ continue;
132
+ }
133
+ if (dropped === null) continue;
134
+ const result = {
135
+ collection: spec.collection,
136
+ owner: spec.owner,
137
+ rows: dropped.rows
138
+ };
139
+ results.push(result);
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: {
146
+ ...result,
147
+ reason: spec.reason
148
+ } });
149
+ }
150
+ return results;
151
+ }
152
+ //#endregion
82
153
  //#region src/builtins/sqlite-storage/filter-compiler.ts
83
154
  /** Thrown by `mutate` mode. Distinct type so a caller can map it to a 400. */
84
155
  var UnsafeFilterError = class extends Error {
@@ -284,6 +355,9 @@ function isImposedBound(source) {
284
355
  }
285
356
  //#endregion
286
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
+ }
287
361
  function parseRowData(raw) {
288
362
  return asJsonObject(parseJsonUnknown(raw)) ?? {};
289
363
  }
@@ -1011,6 +1085,31 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
1011
1085
  }
1012
1086
  this.structuredTables.set(table, fingerprint);
1013
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
+ }
1014
1113
  async tableInsert(table, row) {
1015
1114
  const keys = Object.keys(row);
1016
1115
  const cols = keys.map((k) => `"${k}"`).join(", ");
@@ -1647,6 +1746,7 @@ var SqliteSettingsAddon = class extends BaseAddon {
1647
1746
  } catch (err) {
1648
1747
  this.ctx.logger.warn("Retired-key purge failed", { meta: { error: errMsg(err) } });
1649
1748
  }
1749
+ await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
1650
1750
  const vectorIndex = this.createVectorIndex(this.backend);
1651
1751
  await vectorIndex.loadIndexRegistry();
1652
1752
  this.vectorIndex = vectorIndex;
package/dist/index.js CHANGED
@@ -2151,13 +2151,9 @@ function readInfo(raw) {
2151
2151
  return {};
2152
2152
  }
2153
2153
  var integrationCounter = 0;
2154
- var deviceCounter = 0;
2155
2154
  function nextIntegrationId() {
2156
2155
  return `int_${String(++integrationCounter).padStart(4, "0")}`;
2157
2156
  }
2158
- function nextDeviceId() {
2159
- return `dev_${String(++deviceCounter).padStart(4, "0")}`;
2160
- }
2161
2157
  var INTEGRATIONS_COLUMNS = [
2162
2158
  {
2163
2159
  name: "id",
@@ -2224,95 +2220,6 @@ var INTEGRATION_SETTINGS_COLUMNS = [
2224
2220
  defaultValue: "string"
2225
2221
  }
2226
2222
  ];
2227
- var DEVICES_COLUMNS = [
2228
- {
2229
- name: "id",
2230
- type: "TEXT",
2231
- primaryKey: true
2232
- },
2233
- {
2234
- name: "integration_id",
2235
- type: "TEXT",
2236
- notNull: true
2237
- },
2238
- {
2239
- name: "stable_id",
2240
- type: "TEXT",
2241
- notNull: true,
2242
- unique: true
2243
- },
2244
- {
2245
- name: "type",
2246
- type: "TEXT",
2247
- notNull: true,
2248
- defaultValue: "camera"
2249
- },
2250
- {
2251
- name: "name",
2252
- type: "TEXT",
2253
- notNull: true
2254
- },
2255
- {
2256
- name: "enabled",
2257
- type: "INTEGER",
2258
- notNull: true,
2259
- defaultValue: 1
2260
- },
2261
- {
2262
- name: "info",
2263
- type: "JSON",
2264
- notNull: true,
2265
- defaultValue: "{}"
2266
- },
2267
- {
2268
- name: "created_at",
2269
- type: "INTEGER",
2270
- notNull: true
2271
- },
2272
- {
2273
- name: "updated_at",
2274
- type: "INTEGER",
2275
- notNull: true
2276
- }
2277
- ];
2278
- var DEVICES_INDEXES = [
2279
- {
2280
- name: "idx_devices_integration",
2281
- columns: ["integration_id"]
2282
- },
2283
- {
2284
- name: "idx_devices_stable",
2285
- columns: ["stable_id"],
2286
- unique: true
2287
- },
2288
- {
2289
- name: "idx_devices_type",
2290
- columns: ["type"]
2291
- }
2292
- ];
2293
- var DEVICE_SETTINGS_COLUMNS = [
2294
- {
2295
- name: "device_id",
2296
- type: "TEXT",
2297
- notNull: true
2298
- },
2299
- {
2300
- name: "key",
2301
- type: "TEXT",
2302
- notNull: true
2303
- },
2304
- {
2305
- name: "value",
2306
- type: "TEXT",
2307
- notNull: true
2308
- },
2309
- {
2310
- name: "value_type",
2311
- type: "TEXT",
2312
- notNull: true,
2313
- defaultValue: "string"
2314
- }
2315
- ];
2316
2223
  /**
2317
2224
  * @durable class=config owner=integration-registry
2318
2225
  * write="`createIntegration` — an operator adds a provider, or an addon declares
@@ -2331,29 +2238,19 @@ var INTEGRATIONS = "integrations";
2331
2238
  */
2332
2239
  var INTEGRATION_SETTINGS = "integration_settings";
2333
2240
  /**
2334
- * `stable_id` carries a UNIQUE column constraint AND a UNIQUE index
2335
- * (`idx_devices_stable`): that pair is what stops a provider re-announcing the
2336
- * same physical camera from creating a second device row. An insert that
2337
- * collides throws rather than duplicating.
2241
+ * The persisted INTEGRATIONS one row per provider instance an operator (or a
2242
+ * declaring addon) added, plus that instance's settings.
2338
2243
  *
2339
- * @durable class=config owner=integration-registry
2340
- * write="`createDevice` a provider adopts a device under a stable id;
2341
- * `updateDevice` rewrites name/enabled/info"
2342
- * retention="`deleteDevice`, or the cascade in `deleteIntegration`. Nothing ages
2343
- * out. The `dev_NNNN` counter resumes from the highest surviving row at boot,
2344
- * so wiping the table restarts numbering and re-adopted cameras come back
2345
- * under DIFFERENT ids than every stored track and rule refers to."
2346
- */
2347
- var DEVICES = "devices";
2348
- /**
2349
- * @durable class=config owner=integration-registry
2350
- * write="`setDeviceSetting` — one row per (device_id, key), deleted and
2351
- * re-inserted on every write; per-camera operator intent lands here"
2352
- * retention="cascade only — `deleteDevice` and `deleteIntegration` remove a
2353
- * device's rows. No sweep; losing the table silently reverts every camera to
2354
- * its defaults."
2244
+ * It had a device half once: `devices` + `device_settings_kv`, `dev_NNNN` ids,
2245
+ * `createDevice` / `listCameras` / `setDeviceSetting`. It was removed on
2246
+ * 2026-08-19 with both tables ([D183](../../../../../docs/decisions/adr-0183-code-that-is-deleted-takes-its-tables-with-it.md)):
2247
+ * measured on the reference hub it held 0 rows against 974 live devices, no
2248
+ * production caller ever reached it, and its id-space could not be reconciled
2249
+ * with the numeric ids every track, rule and binding in the system uses. The
2250
+ * fleet is `device-manager:devices`. Do not add a device method here — the
2251
+ * device authority is the device-manager, and a second one that agrees with
2252
+ * nothing is how this half came to exist.
2355
2253
  */
2356
- var DEVICE_SETTINGS = "device_settings_kv";
2357
2254
  var IntegrationRegistry = class IntegrationRegistry {
2358
2255
  store;
2359
2256
  constructor(backend) {
@@ -2369,15 +2266,6 @@ var IntegrationRegistry = class IntegrationRegistry {
2369
2266
  collection: INTEGRATION_SETTINGS,
2370
2267
  columns: INTEGRATION_SETTINGS_COLUMNS
2371
2268
  });
2372
- await this.store.declareCollection({
2373
- collection: DEVICES,
2374
- columns: DEVICES_COLUMNS,
2375
- indexes: DEVICES_INDEXES
2376
- });
2377
- await this.store.declareCollection({
2378
- collection: DEVICE_SETTINGS,
2379
- columns: DEVICE_SETTINGS_COLUMNS
2380
- });
2381
2269
  const ints = await this.store.query({
2382
2270
  collection: INTEGRATIONS,
2383
2271
  filter: {
@@ -2392,20 +2280,6 @@ var IntegrationRegistry = class IntegrationRegistry {
2392
2280
  const num = parseInt(ints[0].id.replace("int_", ""), 10);
2393
2281
  if (!isNaN(num)) integrationCounter = num;
2394
2282
  }
2395
- const devs = await this.store.query({
2396
- collection: DEVICES,
2397
- filter: {
2398
- orderBy: {
2399
- field: "id",
2400
- direction: "desc"
2401
- },
2402
- limit: 1
2403
- }
2404
- });
2405
- if (devs[0]) {
2406
- const num = parseInt(devs[0].id.replace("dev_", ""), 10);
2407
- if (!isNaN(num)) deviceCounter = num;
2408
- }
2409
2283
  }
2410
2284
  async createIntegration(input) {
2411
2285
  const id = nextIntegrationId();
@@ -2489,15 +2363,6 @@ var IntegrationRegistry = class IntegrationRegistry {
2489
2363
  async deleteIntegration(id) {
2490
2364
  const integration = await this.getIntegration(id);
2491
2365
  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`);
2492
- const devices = await this.listDevices(id);
2493
- for (const d of devices) await this.store.deleteWhere({
2494
- collection: DEVICE_SETTINGS,
2495
- filter: { where: { device_id: d.id } }
2496
- });
2497
- await this.store.deleteWhere({
2498
- collection: DEVICES,
2499
- filter: { where: { integration_id: id } }
2500
- });
2501
2366
  await this.store.deleteWhere({
2502
2367
  collection: INTEGRATION_SETTINGS,
2503
2368
  filter: { where: { integration_id: id } }
@@ -2542,139 +2407,6 @@ var IntegrationRegistry = class IntegrationRegistry {
2542
2407
  async setIntegrationSettings(integrationId, settings) {
2543
2408
  for (const [key, value] of Object.entries(settings)) await this.setIntegrationSetting(integrationId, key, value);
2544
2409
  }
2545
- async createDevice(input) {
2546
- const id = nextDeviceId();
2547
- const now = Math.floor(Date.now() / 1e3);
2548
- await this.store.insert({
2549
- collection: DEVICES,
2550
- record: {
2551
- id,
2552
- data: {
2553
- integration_id: input.integrationId,
2554
- stable_id: input.stableId,
2555
- type: input.type,
2556
- name: input.name,
2557
- enabled: input.enabled !== false ? 1 : 0,
2558
- info: input.info ?? {},
2559
- created_at: now,
2560
- updated_at: now
2561
- }
2562
- }
2563
- });
2564
- if (input.settings) await this.setDeviceSettings(id, input.settings);
2565
- return {
2566
- id,
2567
- integrationId: input.integrationId,
2568
- stableId: input.stableId,
2569
- type: input.type,
2570
- name: input.name,
2571
- enabled: input.enabled !== false,
2572
- info: input.info ?? {},
2573
- createdAt: now,
2574
- updatedAt: now
2575
- };
2576
- }
2577
- async getDevice(id) {
2578
- const rows = await this.store.query({
2579
- collection: DEVICES,
2580
- filter: {
2581
- where: { id },
2582
- limit: 1
2583
- }
2584
- });
2585
- return rows[0] ? this.mapDevice(rows[0].data) : null;
2586
- }
2587
- async getDeviceByStableId(stableId) {
2588
- const rows = await this.store.query({
2589
- collection: DEVICES,
2590
- filter: {
2591
- where: { stable_id: stableId },
2592
- limit: 1
2593
- }
2594
- });
2595
- return rows[0] ? this.mapDevice(rows[0].data) : null;
2596
- }
2597
- async listDevices(integrationId) {
2598
- return (await this.store.query({
2599
- collection: DEVICES,
2600
- filter: {
2601
- ...integrationId ? { where: { integration_id: integrationId } } : {},
2602
- orderBy: {
2603
- field: "created_at",
2604
- direction: "asc"
2605
- }
2606
- }
2607
- })).map((r) => this.mapDevice(r.data));
2608
- }
2609
- async listCameras() {
2610
- return (await this.store.query({
2611
- collection: DEVICES,
2612
- filter: {
2613
- where: { type: "camera" },
2614
- orderBy: {
2615
- field: "created_at",
2616
- direction: "asc"
2617
- }
2618
- }
2619
- })).map((r) => this.mapDevice(r.data));
2620
- }
2621
- async updateDevice(id, updates) {
2622
- const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
2623
- if (updates.name !== void 0) updateRow["name"] = updates.name;
2624
- if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
2625
- if (updates.info !== void 0) updateRow["info"] = updates.info;
2626
- await this.store.updateWhere({
2627
- collection: DEVICES,
2628
- filter: { where: { id } },
2629
- data: updateRow
2630
- });
2631
- return this.getDevice(id);
2632
- }
2633
- async deleteDevice(id) {
2634
- await this.store.deleteWhere({
2635
- collection: DEVICE_SETTINGS,
2636
- filter: { where: { device_id: id } }
2637
- });
2638
- await this.store.deleteWhere({
2639
- collection: DEVICES,
2640
- filter: { where: { id } }
2641
- });
2642
- return true;
2643
- }
2644
- async getDeviceSettings(deviceId) {
2645
- const result = {};
2646
- const rows = await this.store.query({
2647
- collection: DEVICE_SETTINGS,
2648
- filter: { where: { device_id: deviceId } }
2649
- });
2650
- for (const row of rows) result[String(row.data["key"])] = deserializeSetting(row.data["value"], String(row.data["value_type"]));
2651
- return result;
2652
- }
2653
- async setDeviceSetting(deviceId, key, value) {
2654
- const s = serializeSetting(value);
2655
- await this.store.deleteWhere({
2656
- collection: DEVICE_SETTINGS,
2657
- filter: { where: {
2658
- device_id: deviceId,
2659
- key
2660
- } }
2661
- });
2662
- await this.store.insert({
2663
- collection: DEVICE_SETTINGS,
2664
- record: {
2665
- id: `${deviceId}:${key}`,
2666
- data: {
2667
- device_id: deviceId,
2668
- key,
2669
- value: s.value,
2670
- value_type: s.valueType
2671
- }
2672
- }
2673
- });
2674
- }
2675
- async setDeviceSettings(deviceId, settings) {
2676
- for (const [key, value] of Object.entries(settings)) await this.setDeviceSetting(deviceId, key, value);
2677
- }
2678
2410
  mapIntegration(row) {
2679
2411
  return {
2680
2412
  id: String(row["id"]),
@@ -2686,19 +2418,6 @@ var IntegrationRegistry = class IntegrationRegistry {
2686
2418
  updatedAt: Number(row["updated_at"])
2687
2419
  };
2688
2420
  }
2689
- mapDevice(row) {
2690
- return {
2691
- id: String(row["id"]),
2692
- integrationId: String(row["integration_id"]),
2693
- stableId: String(row["stable_id"]),
2694
- type: String(row["type"]),
2695
- name: String(row["name"]),
2696
- enabled: row["enabled"] === 1,
2697
- info: readInfo(row["info"]),
2698
- createdAt: Number(row["created_at"]),
2699
- updatedAt: Number(row["updated_at"])
2700
- };
2701
- }
2702
2421
  };
2703
2422
  //#endregion
2704
2423
  //#region src/kernel/deps/ensure-native-prebuilds.ts
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
- * `stable_id` carries a UNIQUE column constraint AND a UNIQUE index
2328
- * (`idx_devices_stable`): that pair is what stops a provider re-announcing the
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
- * @durable class=config owner=integration-registry
2333
- * write="`createDevice` a provider adopts a device under a stable id;
2334
- * `updateDevice` rewrites name/enabled/info"
2335
- * retention="`deleteDevice`, or the cascade in `deleteIntegration`. Nothing ages
2336
- * out. The `dev_NNNN` counter resumes from the highest surviving row at boot,
2337
- * so wiping the table restarts numbering and re-adopted cameras come back
2338
- * under DIFFERENT ids than every stored track and rule refers to."
2339
- */
2340
- var DEVICES = "devices";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.108",
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",