@camstack/system 1.2.134 → 1.2.136

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.
Files changed (54) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +117 -9
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +117 -9
  15. package/dist/builtins/device-manager/device-row-store.d.ts +50 -0
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  18. package/dist/builtins/hub-forwarder/index.js +1 -1
  19. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  24. package/dist/builtins/local-network/local-network.addon.js +1 -1
  25. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  26. package/dist/builtins/loki-logging/index.js +1 -1
  27. package/dist/builtins/loki-logging/index.mjs +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  30. package/dist/builtins/platform-probe/index.js +1 -1
  31. package/dist/builtins/platform-probe/index.mjs +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  34. package/dist/builtins/snapshot/index.js +1 -1
  35. package/dist/builtins/snapshot/index.mjs +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +39 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/system-config/system-config.addon.js +1 -1
  44. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  45. package/dist/builtins/winston-logging/index.js +1 -1
  46. package/dist/builtins/winston-logging/index.mjs +1 -1
  47. package/dist/{dist-BUEU7B1V.mjs → dist-Bh25kEUH.mjs} +62 -32
  48. package/dist/{dist-DqcaE-k1.js → dist-D-R0SuP-.js} +62 -32
  49. package/dist/index.js +18 -1
  50. package/dist/index.mjs +18 -1
  51. package/dist/kernel/config-manager.d.ts +4 -0
  52. package/dist/{retired-settings-keys-BhPleRL7.mjs → retired-settings-keys-BZbOXqch.mjs} +1 -1
  53. package/dist/{retired-settings-keys-KZnmtfsh.js → retired-settings-keys-CqD6rSQx.js} +1 -1
  54. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, v as addonPagesCapability, vt as errMsg, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { Gt as EventCategory, v as addonPagesCapability, vt as errMsg, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, b as addonWidgetsCapability, vt as errMsg, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { Gt as EventCategory, b as addonWidgetsCapability, vt as errMsg, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, Mt as createEvent, n as AlertSchema, vt as errMsg, x as alertsCapability, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { Gt as EventCategory, Mt as createEvent, n as AlertSchema, vt as errMsg, x as alertsCapability, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { C as backupCapability, Gt as EventCategory, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { C as backupCapability, Gt as EventCategory, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-DqcaE-k1.js");
3
+ const require_dist = require("../../dist-D-R0SuP-.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { D as coreBlockAddonId, O as coreBlockIdFromAddonId, c as CoreBlockSchema, k as coreBlocksCapability, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, vt as errMsg, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { D as coreBlockAddonId, O as coreBlockIdFromAddonId, c as CoreBlockSchema, k as coreBlocksCapability, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, vt as errMsg, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-KZnmtfsh.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-CqD6rSQx.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -1280,6 +1280,32 @@ async function listPersistedByAddon(pctx, input) {
1280
1280
  parentDeviceId: m.parentDeviceId
1281
1281
  }));
1282
1282
  }
1283
+ /**
1284
+ * Every requested device's persisted config, in as few store round trips as the
1285
+ * settings view allows.
1286
+ *
1287
+ * One call when the view can batch (the in-process door, which keys
1288
+ * `addon-device-settings` by `<addonId>:<deviceId>.` and answers a whole addon
1289
+ * with one indexed range scan). Otherwise the per-device form, unchanged — a
1290
+ * fallback and not a silent downgrade: `readDeviceStoreBatch` is optional
1291
+ * precisely because a forked addon's view has nothing to batch, and refusing
1292
+ * would break the only implementation that cannot comply.
1293
+ *
1294
+ * A read that THROWS yields `{}` for that device rather than failing the whole
1295
+ * listing: one unreadable settings row must not make the fleet unlistable.
1296
+ */
1297
+ async function readDeviceConfigs(pctx, deviceIds) {
1298
+ if (deviceIds.length === 0) return /* @__PURE__ */ new Map();
1299
+ const batch = pctx.settings.readDeviceStoreBatch;
1300
+ if (batch !== void 0) return batch.call(pctx.settings, deviceIds);
1301
+ const out = /* @__PURE__ */ new Map();
1302
+ for (const id of deviceIds) try {
1303
+ out.set(id, await pctx.settings.readDeviceStore(id));
1304
+ } catch {
1305
+ out.set(id, {});
1306
+ }
1307
+ return out;
1308
+ }
1283
1309
  async function listAll(pctx, input) {
1284
1310
  const ownerFilter = Reflect.get(input, "addonId");
1285
1311
  if (ownerFilter !== void 0 && (typeof ownerFilter !== "string" || ownerFilter.length === 0)) throw new Error(`deviceManager.listAll: addonId must be a non-empty string or be omitted — got ${JSON.stringify(ownerFilter)}. An owner filter that is present but empty is never widened to every device. On an addon context the id is \`ctx.id\`; there is no \`ctx.addonId\`.`);
@@ -1289,7 +1315,10 @@ async function listAll(pctx, input) {
1289
1315
  const idFilter = input.deviceIds === void 0 ? null : new Set(input.deviceIds);
1290
1316
  const results = [];
1291
1317
  const seen = /* @__PURE__ */ new Set();
1292
- const fleet = addonId ? await pctx.metaStore.rows.listByAddon(addonId) : await pctx.metaStore.rows.listAll();
1318
+ const fleet = addonId ? await pctx.metaStore.rows.listByAddon(addonId) : await pctx.metaStore.rows.listFleet({
1319
+ ...input.deviceIds !== void 0 ? { deviceIds: input.deviceIds } : {},
1320
+ slim
1321
+ });
1293
1322
  const rowById = /* @__PURE__ */ new Map();
1294
1323
  for (const row of fleet) rowById.set(row.meta.id, row);
1295
1324
  if (pctx.registry) {
@@ -1311,6 +1340,7 @@ async function listAll(pctx, input) {
1311
1340
  } : info);
1312
1341
  }
1313
1342
  }
1343
+ const configs = await readDeviceConfigs(pctx, slim ? [] : fleet.filter((row) => !seen.has(String(row.meta.id))).filter((row) => idFilter === null || idFilter.has(row.meta.id)).filter((row) => !camerasOnly || row.meta.type === require_dist.DeviceType.Camera).map((row) => row.meta.id));
1314
1344
  for (const row of fleet) {
1315
1345
  const m = row.meta;
1316
1346
  const aid = m.addonId;
@@ -1319,7 +1349,7 @@ async function listAll(pctx, input) {
1319
1349
  if (idFilter !== null && !idFilter.has(m.id)) continue;
1320
1350
  const persistedType = m.type;
1321
1351
  if (camerasOnly && persistedType !== require_dist.DeviceType.Camera) continue;
1322
- const persistedConfig = slim ? {} : await pctx.settings.readDeviceStore(m.id);
1352
+ const persistedConfig = configs.get(m.id) ?? {};
1323
1353
  const metadata = slim ? null : row.metadata;
1324
1354
  results.push({
1325
1355
  id: m.id,
@@ -1848,6 +1878,26 @@ function resolveTrackedEventDeviceIds(config, linkedIds) {
1848
1878
  return linkedIds.filter((id) => picked.has(id));
1849
1879
  }
1850
1880
  /**
1881
+ * The fleet listing every function here needs, and the ONLY projection any of
1882
+ * them may ask for.
1883
+ *
1884
+ * Linked-device policy reads five fields off a candidate — `id`,
1885
+ * `parentDeviceId`, `location`, `name`, `features` — and `slim` carries all
1886
+ * five. What `slim` drops is `config`, and `config` is not a column: `listAll`
1887
+ * materialises it with ONE `readDeviceStore` per device, sequentially, on
1888
+ * device-manager's event loop. This module asked for the full projection three
1889
+ * times, so every linked-devices read swept the whole fleet's settings rows to
1890
+ * use none of them.
1891
+ *
1892
+ * Measured on the live hub 2026-08-25: `query addon-device-settings` ran
1893
+ * 36 814 times a minute against a 967-device fleet — ~38 full-fleet sweeps a
1894
+ * minute, ~31 of them the `pipeline-analytics` reverse index rebuilding itself
1895
+ * one camera at a time through `getLinkedDevices`. The per-camera `config`
1896
+ * this query DOES need is the separate `readDeviceStore(deviceId)` below, and
1897
+ * it is one row, not the fleet.
1898
+ */
1899
+ var LINK_CANDIDATE_PROJECTION = { projection: "slim" };
1900
+ /**
1851
1901
  * Resolve ONE camera's linked set against an ALREADY-FETCHED fleet.
1852
1902
  *
1853
1903
  * The fleet is a parameter, not a fetch, because it is the entire cost of this
@@ -1884,7 +1934,7 @@ function resolveFor(deviceId, all, byId, blob) {
1884
1934
  /** Cap implementation of `getLinkedDevices`. */
1885
1935
  async function getLinkedDevices(pctx, input) {
1886
1936
  const { deviceId } = input;
1887
- const [all, blob] = await Promise.all([listAll(pctx, {}), pctx.settings.readDeviceStore(deviceId)]);
1937
+ const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
1888
1938
  return resolveFor(deviceId, all, new Map(all.map((d) => [d.id, d])), blob);
1889
1939
  }
1890
1940
  /**
@@ -1903,7 +1953,7 @@ async function getLinkedDevices(pctx, input) {
1903
1953
  async function getLinkedDevicesBatch(pctx, input) {
1904
1954
  const deviceIds = [...new Set(input.deviceIds)];
1905
1955
  if (deviceIds.length === 0) return [];
1906
- const [all, blobs] = await Promise.all([listAll(pctx, {}), Promise.all(deviceIds.map((id) => pctx.settings.readDeviceStore(id).catch((err) => {
1956
+ const [all, blobs] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), Promise.all(deviceIds.map((id) => pctx.settings.readDeviceStore(id).catch((err) => {
1907
1957
  pctx.host.ctx.logger.warn("getLinkedDevicesBatch: settings read failed — children only", {
1908
1958
  tags: { deviceId: id },
1909
1959
  meta: { error: err instanceof Error ? err.message : String(err) }
@@ -1925,7 +1975,7 @@ async function getLinkedDevicesBatch(pctx, input) {
1925
1975
  async function buildLinkedDevicesContribution(pctx, deviceId) {
1926
1976
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
1927
1977
  if (!persisted || persisted.meta.type !== require_dist.DeviceType.Camera) return null;
1928
- const [all, blob] = await Promise.all([listAll(pctx, {}), pctx.settings.readDeviceStore(deviceId)]);
1978
+ const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
1929
1979
  const config = parseLinkedDevicesConfig(blob);
1930
1980
  const options = all.filter((d) => d.id !== deviceId).map((d) => ({
1931
1981
  value: String(d.id),
@@ -4004,6 +4054,36 @@ var DEVICE_ROWS_COLUMNS = [
4004
4054
  type: "JSON"
4005
4055
  }
4006
4056
  ];
4057
+ /**
4058
+ * The columns a SLIM fleet read selects: every declared column except the
4059
+ * primary key (always returned) and `metadata`.
4060
+ *
4061
+ * ## Why this is a DENYLIST and must stay one
4062
+ *
4063
+ * `decodeDeviceRow` returns `null` when `deviceId`, `addonId`, `stableId`,
4064
+ * `type` or `name` is missing, and `list()` then SKIPS the row with a warn. A
4065
+ * hand-written allowlist that forgot one of the five would not make the fleet
4066
+ * slower — it would make devices vanish from every listing, quietly, and the
4067
+ * only trace would be a log line nobody is reading at the time. Deriving the
4068
+ * list by subtraction means a column added tomorrow is carried by default and
4069
+ * the only way to lose one is to name it here.
4070
+ *
4071
+ * `metadata` is the one slim provably discards: `device-queries.ts#listAll`
4072
+ * sets `metadata: null` on every slim row. It is also the largest JSON column
4073
+ * on the row, which is why this is worth doing at all.
4074
+ *
4075
+ * ## What is NOT here, and will not be
4076
+ *
4077
+ * There is no `where: { type: 'camera' }` companion to this. `isCameraDevice`
4078
+ * is a method-presence check (`getStreamSources`), not the persisted `type`, so
4079
+ * a doorbell with a camera answers YES while its row says `doorbell`. Filtering
4080
+ * the rows by type would strip exactly those devices of their row — and
4081
+ * `toDeviceInfo` reads `name` / `location` / `disabled` from the ROW precisely
4082
+ * because the live device's copies are stale by construction. The fleet would
4083
+ * keep working and start reporting pre-rename names on doorbells. The
4084
+ * `isCamera` filter stays in JS.
4085
+ */
4086
+ var DEVICE_ROWS_SLIM_COLUMNS = DEVICE_ROWS_COLUMNS.map((c) => c.name).filter((name) => name !== "id" && name !== "metadata");
4007
4087
  var DEVICE_ROWS_INDEXES = [
4008
4088
  {
4009
4089
  name: "idx_dm_devices_addon_stable",
@@ -4222,6 +4302,33 @@ var DeviceRowStore = class {
4222
4302
  });
4223
4303
  }
4224
4304
  /**
4305
+ * The fleet, read as narrowly as the question allows.
4306
+ *
4307
+ * `listAll` is a full-table `SELECT … ORDER BY "deviceId"` — ~1 017 rows a
4308
+ * call on the live hub, 14–54 calls a minute — and its callers then filtered
4309
+ * the result in JS. The two filters that CAN be pushed down are here:
4310
+ *
4311
+ * - `deviceIds` becomes a `whereIn`. The viewer's linked-devices panel asks
4312
+ * about ~8 named ids; it was answered by reading the whole ledger. An
4313
+ * EMPTY set reads nothing at all rather than degrading to "everything" —
4314
+ * a request for no devices is a request, not an omission.
4315
+ * - `slim` becomes a column projection, {@link DEVICE_ROWS_SLIM_COLUMNS}.
4316
+ *
4317
+ * `isCamera` is deliberately NOT one of them — see that constant's note and
4318
+ * `device-row-store-filtered.spec.ts`.
4319
+ */
4320
+ async listFleet(opts) {
4321
+ if (opts.deviceIds !== void 0 && opts.deviceIds.length === 0) return [];
4322
+ return this.list({
4323
+ ...opts.deviceIds !== void 0 ? { whereIn: { deviceId: [...new Set(opts.deviceIds)] } } : {},
4324
+ orderBy: {
4325
+ field: "deviceId",
4326
+ direction: "asc"
4327
+ },
4328
+ limit: DEVICE_ROWS_FLEET_LIMIT
4329
+ }, opts.slim === true ? DEVICE_ROWS_SLIM_COLUMNS : void 0);
4330
+ }
4331
+ /**
4225
4332
  * The device an addon knows as `stableId`, or `null`.
4226
4333
  *
4227
4334
  * `(addonId, stableId)` is the addon-facing identity — unique by
@@ -4292,11 +4399,12 @@ var DeviceRowStore = class {
4292
4399
  await this.declare();
4293
4400
  return this.backend.count({ collection: DEVICE_ROWS_COLLECTION });
4294
4401
  }
4295
- async list(filter) {
4402
+ async list(filter, columns) {
4296
4403
  await this.declare();
4297
4404
  const records = await this.backend.query({
4298
4405
  collection: DEVICE_ROWS_COLLECTION,
4299
- filter
4406
+ filter,
4407
+ ...columns === void 0 ? {} : { columns }
4300
4408
  });
4301
4409
  const out = [];
4302
4410
  for (const record of records) {
@@ -1,5 +1,5 @@
1
- import { Ct as DeviceFeature, E as buildStreamParamsConfigSchema, F as deviceStatusCapability, Gt as EventCategory, Lt as isDeviceConfigCap, N as deviceManagerCapability, Ot as WELL_KNOWN_TAB_MAP, P as deviceStateCapability, R as enumerateItemArrayFields, Tt as DeviceType, Wt as sleep, a as CAP_NAMES_WITH_STATUS, at as runtimeStatePolicyFor, et as normalizeUnit, nt as parseStreamParamsFormPatch, p as STREAM_PROFILE_META, t as ALL_CAPABILITY_DEFINITIONS, u as DeviceStatusSchema, vt as errMsg, wt as DeviceRole, yt as BaseAddon, z as enumerateSchemaFields } from "../../dist-BUEU7B1V.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-BhPleRL7.mjs";
1
+ import { Ct as DeviceFeature, E as buildStreamParamsConfigSchema, F as deviceStatusCapability, Gt as EventCategory, Lt as isDeviceConfigCap, N as deviceManagerCapability, Ot as WELL_KNOWN_TAB_MAP, P as deviceStateCapability, R as enumerateItemArrayFields, Tt as DeviceType, Wt as sleep, a as CAP_NAMES_WITH_STATUS, at as runtimeStatePolicyFor, et as normalizeUnit, nt as parseStreamParamsFormPatch, p as STREAM_PROFILE_META, t as ALL_CAPABILITY_DEFINITIONS, u as DeviceStatusSchema, vt as errMsg, wt as DeviceRole, yt as BaseAddon, z as enumerateSchemaFields } from "../../dist-Bh25kEUH.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-BZbOXqch.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -1275,6 +1275,32 @@ async function listPersistedByAddon(pctx, input) {
1275
1275
  parentDeviceId: m.parentDeviceId
1276
1276
  }));
1277
1277
  }
1278
+ /**
1279
+ * Every requested device's persisted config, in as few store round trips as the
1280
+ * settings view allows.
1281
+ *
1282
+ * One call when the view can batch (the in-process door, which keys
1283
+ * `addon-device-settings` by `<addonId>:<deviceId>.` and answers a whole addon
1284
+ * with one indexed range scan). Otherwise the per-device form, unchanged — a
1285
+ * fallback and not a silent downgrade: `readDeviceStoreBatch` is optional
1286
+ * precisely because a forked addon's view has nothing to batch, and refusing
1287
+ * would break the only implementation that cannot comply.
1288
+ *
1289
+ * A read that THROWS yields `{}` for that device rather than failing the whole
1290
+ * listing: one unreadable settings row must not make the fleet unlistable.
1291
+ */
1292
+ async function readDeviceConfigs(pctx, deviceIds) {
1293
+ if (deviceIds.length === 0) return /* @__PURE__ */ new Map();
1294
+ const batch = pctx.settings.readDeviceStoreBatch;
1295
+ if (batch !== void 0) return batch.call(pctx.settings, deviceIds);
1296
+ const out = /* @__PURE__ */ new Map();
1297
+ for (const id of deviceIds) try {
1298
+ out.set(id, await pctx.settings.readDeviceStore(id));
1299
+ } catch {
1300
+ out.set(id, {});
1301
+ }
1302
+ return out;
1303
+ }
1278
1304
  async function listAll(pctx, input) {
1279
1305
  const ownerFilter = Reflect.get(input, "addonId");
1280
1306
  if (ownerFilter !== void 0 && (typeof ownerFilter !== "string" || ownerFilter.length === 0)) throw new Error(`deviceManager.listAll: addonId must be a non-empty string or be omitted — got ${JSON.stringify(ownerFilter)}. An owner filter that is present but empty is never widened to every device. On an addon context the id is \`ctx.id\`; there is no \`ctx.addonId\`.`);
@@ -1284,7 +1310,10 @@ async function listAll(pctx, input) {
1284
1310
  const idFilter = input.deviceIds === void 0 ? null : new Set(input.deviceIds);
1285
1311
  const results = [];
1286
1312
  const seen = /* @__PURE__ */ new Set();
1287
- const fleet = addonId ? await pctx.metaStore.rows.listByAddon(addonId) : await pctx.metaStore.rows.listAll();
1313
+ const fleet = addonId ? await pctx.metaStore.rows.listByAddon(addonId) : await pctx.metaStore.rows.listFleet({
1314
+ ...input.deviceIds !== void 0 ? { deviceIds: input.deviceIds } : {},
1315
+ slim
1316
+ });
1288
1317
  const rowById = /* @__PURE__ */ new Map();
1289
1318
  for (const row of fleet) rowById.set(row.meta.id, row);
1290
1319
  if (pctx.registry) {
@@ -1306,6 +1335,7 @@ async function listAll(pctx, input) {
1306
1335
  } : info);
1307
1336
  }
1308
1337
  }
1338
+ const configs = await readDeviceConfigs(pctx, slim ? [] : fleet.filter((row) => !seen.has(String(row.meta.id))).filter((row) => idFilter === null || idFilter.has(row.meta.id)).filter((row) => !camerasOnly || row.meta.type === DeviceType.Camera).map((row) => row.meta.id));
1309
1339
  for (const row of fleet) {
1310
1340
  const m = row.meta;
1311
1341
  const aid = m.addonId;
@@ -1314,7 +1344,7 @@ async function listAll(pctx, input) {
1314
1344
  if (idFilter !== null && !idFilter.has(m.id)) continue;
1315
1345
  const persistedType = m.type;
1316
1346
  if (camerasOnly && persistedType !== DeviceType.Camera) continue;
1317
- const persistedConfig = slim ? {} : await pctx.settings.readDeviceStore(m.id);
1347
+ const persistedConfig = configs.get(m.id) ?? {};
1318
1348
  const metadata = slim ? null : row.metadata;
1319
1349
  results.push({
1320
1350
  id: m.id,
@@ -1843,6 +1873,26 @@ function resolveTrackedEventDeviceIds(config, linkedIds) {
1843
1873
  return linkedIds.filter((id) => picked.has(id));
1844
1874
  }
1845
1875
  /**
1876
+ * The fleet listing every function here needs, and the ONLY projection any of
1877
+ * them may ask for.
1878
+ *
1879
+ * Linked-device policy reads five fields off a candidate — `id`,
1880
+ * `parentDeviceId`, `location`, `name`, `features` — and `slim` carries all
1881
+ * five. What `slim` drops is `config`, and `config` is not a column: `listAll`
1882
+ * materialises it with ONE `readDeviceStore` per device, sequentially, on
1883
+ * device-manager's event loop. This module asked for the full projection three
1884
+ * times, so every linked-devices read swept the whole fleet's settings rows to
1885
+ * use none of them.
1886
+ *
1887
+ * Measured on the live hub 2026-08-25: `query addon-device-settings` ran
1888
+ * 36 814 times a minute against a 967-device fleet — ~38 full-fleet sweeps a
1889
+ * minute, ~31 of them the `pipeline-analytics` reverse index rebuilding itself
1890
+ * one camera at a time through `getLinkedDevices`. The per-camera `config`
1891
+ * this query DOES need is the separate `readDeviceStore(deviceId)` below, and
1892
+ * it is one row, not the fleet.
1893
+ */
1894
+ var LINK_CANDIDATE_PROJECTION = { projection: "slim" };
1895
+ /**
1846
1896
  * Resolve ONE camera's linked set against an ALREADY-FETCHED fleet.
1847
1897
  *
1848
1898
  * The fleet is a parameter, not a fetch, because it is the entire cost of this
@@ -1879,7 +1929,7 @@ function resolveFor(deviceId, all, byId, blob) {
1879
1929
  /** Cap implementation of `getLinkedDevices`. */
1880
1930
  async function getLinkedDevices(pctx, input) {
1881
1931
  const { deviceId } = input;
1882
- const [all, blob] = await Promise.all([listAll(pctx, {}), pctx.settings.readDeviceStore(deviceId)]);
1932
+ const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
1883
1933
  return resolveFor(deviceId, all, new Map(all.map((d) => [d.id, d])), blob);
1884
1934
  }
1885
1935
  /**
@@ -1898,7 +1948,7 @@ async function getLinkedDevices(pctx, input) {
1898
1948
  async function getLinkedDevicesBatch(pctx, input) {
1899
1949
  const deviceIds = [...new Set(input.deviceIds)];
1900
1950
  if (deviceIds.length === 0) return [];
1901
- const [all, blobs] = await Promise.all([listAll(pctx, {}), Promise.all(deviceIds.map((id) => pctx.settings.readDeviceStore(id).catch((err) => {
1951
+ const [all, blobs] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), Promise.all(deviceIds.map((id) => pctx.settings.readDeviceStore(id).catch((err) => {
1902
1952
  pctx.host.ctx.logger.warn("getLinkedDevicesBatch: settings read failed — children only", {
1903
1953
  tags: { deviceId: id },
1904
1954
  meta: { error: err instanceof Error ? err.message : String(err) }
@@ -1920,7 +1970,7 @@ async function getLinkedDevicesBatch(pctx, input) {
1920
1970
  async function buildLinkedDevicesContribution(pctx, deviceId) {
1921
1971
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
1922
1972
  if (!persisted || persisted.meta.type !== DeviceType.Camera) return null;
1923
- const [all, blob] = await Promise.all([listAll(pctx, {}), pctx.settings.readDeviceStore(deviceId)]);
1973
+ const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
1924
1974
  const config = parseLinkedDevicesConfig(blob);
1925
1975
  const options = all.filter((d) => d.id !== deviceId).map((d) => ({
1926
1976
  value: String(d.id),
@@ -3999,6 +4049,36 @@ var DEVICE_ROWS_COLUMNS = [
3999
4049
  type: "JSON"
4000
4050
  }
4001
4051
  ];
4052
+ /**
4053
+ * The columns a SLIM fleet read selects: every declared column except the
4054
+ * primary key (always returned) and `metadata`.
4055
+ *
4056
+ * ## Why this is a DENYLIST and must stay one
4057
+ *
4058
+ * `decodeDeviceRow` returns `null` when `deviceId`, `addonId`, `stableId`,
4059
+ * `type` or `name` is missing, and `list()` then SKIPS the row with a warn. A
4060
+ * hand-written allowlist that forgot one of the five would not make the fleet
4061
+ * slower — it would make devices vanish from every listing, quietly, and the
4062
+ * only trace would be a log line nobody is reading at the time. Deriving the
4063
+ * list by subtraction means a column added tomorrow is carried by default and
4064
+ * the only way to lose one is to name it here.
4065
+ *
4066
+ * `metadata` is the one slim provably discards: `device-queries.ts#listAll`
4067
+ * sets `metadata: null` on every slim row. It is also the largest JSON column
4068
+ * on the row, which is why this is worth doing at all.
4069
+ *
4070
+ * ## What is NOT here, and will not be
4071
+ *
4072
+ * There is no `where: { type: 'camera' }` companion to this. `isCameraDevice`
4073
+ * is a method-presence check (`getStreamSources`), not the persisted `type`, so
4074
+ * a doorbell with a camera answers YES while its row says `doorbell`. Filtering
4075
+ * the rows by type would strip exactly those devices of their row — and
4076
+ * `toDeviceInfo` reads `name` / `location` / `disabled` from the ROW precisely
4077
+ * because the live device's copies are stale by construction. The fleet would
4078
+ * keep working and start reporting pre-rename names on doorbells. The
4079
+ * `isCamera` filter stays in JS.
4080
+ */
4081
+ var DEVICE_ROWS_SLIM_COLUMNS = DEVICE_ROWS_COLUMNS.map((c) => c.name).filter((name) => name !== "id" && name !== "metadata");
4002
4082
  var DEVICE_ROWS_INDEXES = [
4003
4083
  {
4004
4084
  name: "idx_dm_devices_addon_stable",
@@ -4217,6 +4297,33 @@ var DeviceRowStore = class {
4217
4297
  });
4218
4298
  }
4219
4299
  /**
4300
+ * The fleet, read as narrowly as the question allows.
4301
+ *
4302
+ * `listAll` is a full-table `SELECT … ORDER BY "deviceId"` — ~1 017 rows a
4303
+ * call on the live hub, 14–54 calls a minute — and its callers then filtered
4304
+ * the result in JS. The two filters that CAN be pushed down are here:
4305
+ *
4306
+ * - `deviceIds` becomes a `whereIn`. The viewer's linked-devices panel asks
4307
+ * about ~8 named ids; it was answered by reading the whole ledger. An
4308
+ * EMPTY set reads nothing at all rather than degrading to "everything" —
4309
+ * a request for no devices is a request, not an omission.
4310
+ * - `slim` becomes a column projection, {@link DEVICE_ROWS_SLIM_COLUMNS}.
4311
+ *
4312
+ * `isCamera` is deliberately NOT one of them — see that constant's note and
4313
+ * `device-row-store-filtered.spec.ts`.
4314
+ */
4315
+ async listFleet(opts) {
4316
+ if (opts.deviceIds !== void 0 && opts.deviceIds.length === 0) return [];
4317
+ return this.list({
4318
+ ...opts.deviceIds !== void 0 ? { whereIn: { deviceId: [...new Set(opts.deviceIds)] } } : {},
4319
+ orderBy: {
4320
+ field: "deviceId",
4321
+ direction: "asc"
4322
+ },
4323
+ limit: DEVICE_ROWS_FLEET_LIMIT
4324
+ }, opts.slim === true ? DEVICE_ROWS_SLIM_COLUMNS : void 0);
4325
+ }
4326
+ /**
4220
4327
  * The device an addon knows as `stableId`, or `null`.
4221
4328
  *
4222
4329
  * `(addonId, stableId)` is the addon-facing identity — unique by
@@ -4287,11 +4394,12 @@ var DeviceRowStore = class {
4287
4394
  await this.declare();
4288
4395
  return this.backend.count({ collection: DEVICE_ROWS_COLLECTION });
4289
4396
  }
4290
- async list(filter) {
4397
+ async list(filter, columns) {
4291
4398
  await this.declare();
4292
4399
  const records = await this.backend.query({
4293
4400
  collection: DEVICE_ROWS_COLLECTION,
4294
- filter
4401
+ filter,
4402
+ ...columns === void 0 ? {} : { columns }
4295
4403
  });
4296
4404
  const out = [];
4297
4405
  for (const record of records) {
@@ -43,6 +43,36 @@ export declare const DEVICE_ROWS_COLLECTION = "device-manager:devices";
43
43
  */
44
44
  export declare const DEVICE_ROWS_FLEET_LIMIT = 20000;
45
45
  export declare const DEVICE_ROWS_COLUMNS: readonly CollectionColumn[];
46
+ /**
47
+ * The columns a SLIM fleet read selects: every declared column except the
48
+ * primary key (always returned) and `metadata`.
49
+ *
50
+ * ## Why this is a DENYLIST and must stay one
51
+ *
52
+ * `decodeDeviceRow` returns `null` when `deviceId`, `addonId`, `stableId`,
53
+ * `type` or `name` is missing, and `list()` then SKIPS the row with a warn. A
54
+ * hand-written allowlist that forgot one of the five would not make the fleet
55
+ * slower — it would make devices vanish from every listing, quietly, and the
56
+ * only trace would be a log line nobody is reading at the time. Deriving the
57
+ * list by subtraction means a column added tomorrow is carried by default and
58
+ * the only way to lose one is to name it here.
59
+ *
60
+ * `metadata` is the one slim provably discards: `device-queries.ts#listAll`
61
+ * sets `metadata: null` on every slim row. It is also the largest JSON column
62
+ * on the row, which is why this is worth doing at all.
63
+ *
64
+ * ## What is NOT here, and will not be
65
+ *
66
+ * There is no `where: { type: 'camera' }` companion to this. `isCameraDevice`
67
+ * is a method-presence check (`getStreamSources`), not the persisted `type`, so
68
+ * a doorbell with a camera answers YES while its row says `doorbell`. Filtering
69
+ * the rows by type would strip exactly those devices of their row — and
70
+ * `toDeviceInfo` reads `name` / `location` / `disabled` from the ROW precisely
71
+ * because the live device's copies are stale by construction. The fleet would
72
+ * keep working and start reporting pre-rename names on doorbells. The
73
+ * `isCamera` filter stays in JS.
74
+ */
75
+ export declare const DEVICE_ROWS_SLIM_COLUMNS: readonly string[];
46
76
  export declare const DEVICE_ROWS_INDEXES: readonly CollectionIndex[];
47
77
  /**
48
78
  * The settings-store door this store needs, as a plain object surface.
@@ -193,6 +223,26 @@ export declare class DeviceRowStore {
193
223
  get(deviceId: number): Promise<DeviceRow | null>;
194
224
  /** Every device, ordered by numeric id. */
195
225
  listAll(): Promise<readonly DeviceRow[]>;
226
+ /**
227
+ * The fleet, read as narrowly as the question allows.
228
+ *
229
+ * `listAll` is a full-table `SELECT … ORDER BY "deviceId"` — ~1 017 rows a
230
+ * call on the live hub, 14–54 calls a minute — and its callers then filtered
231
+ * the result in JS. The two filters that CAN be pushed down are here:
232
+ *
233
+ * - `deviceIds` becomes a `whereIn`. The viewer's linked-devices panel asks
234
+ * about ~8 named ids; it was answered by reading the whole ledger. An
235
+ * EMPTY set reads nothing at all rather than degrading to "everything" —
236
+ * a request for no devices is a request, not an omission.
237
+ * - `slim` becomes a column projection, {@link DEVICE_ROWS_SLIM_COLUMNS}.
238
+ *
239
+ * `isCamera` is deliberately NOT one of them — see that constant's note and
240
+ * `device-row-store-filtered.spec.ts`.
241
+ */
242
+ listFleet(opts: {
243
+ readonly deviceIds?: readonly number[];
244
+ readonly slim?: boolean;
245
+ }): Promise<readonly DeviceRow[]>;
196
246
  /**
197
247
  * The device an addon knows as `stableId`, or `null`.
198
248
  *
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { G as isSameAddonId, Gt as EventCategory, I as doorbellCapability, Mt as createEvent, Tt as DeviceType, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { G as isSameAddonId, Gt as EventCategory, I as doorbellCapability, Mt as createEvent, Tt as DeviceType, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-DqcaE-k1.js");
3
+ const require_dist = require("../../dist-D-R0SuP-.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, Mt as createEvent, vt as errMsg, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { Gt as EventCategory, Mt as createEvent, vt as errMsg, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");