@camstack/system 1.2.143 → 1.2.145

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 (62) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +157 -35
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +157 -35
  17. package/dist/builtins/device-manager/device-queries.d.ts +22 -0
  18. package/dist/builtins/device-manager/device-row-store.d.ts +18 -0
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  21. package/dist/builtins/hub-forwarder/index.js +1 -1
  22. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  27. package/dist/builtins/local-network/local-network.addon.js +1 -1
  28. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  29. package/dist/builtins/loki-logging/index.js +1 -1
  30. package/dist/builtins/loki-logging/index.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  33. package/dist/builtins/platform-probe/index.js +1 -1
  34. package/dist/builtins/platform-probe/index.mjs +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/snapshot/index.js +1 -1
  38. package/dist/builtins/snapshot/index.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +14 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{dist-CQs1b7uh.js → dist-BVU5JADq.js} +488 -29
  51. package/dist/{dist-CVuVKA-O.mjs → dist-CG7kwIZp.mjs} +488 -29
  52. package/dist/index.js +28 -9
  53. package/dist/index.mjs +28 -9
  54. package/dist/kernel/index.d.ts +1 -1
  55. package/dist/kernel/moleculer/children-batch-loader.d.ts +46 -0
  56. package/dist/kernel/moleculer/core-cap-service.d.ts +17 -2
  57. package/dist/kernel/moleculer/process-service.d.ts +15 -6
  58. package/dist/{manifest-python-deps-BuhBXCg0.mjs → manifest-python-deps-BRyNCnnG.mjs} +191 -1
  59. package/dist/{manifest-python-deps-B_mCU6gz.js → manifest-python-deps-GjlyPjm0.js} +191 -1
  60. package/dist/{retired-settings-keys-DnMYSIEr.mjs → retired-settings-keys-Dm1z4mO4.mjs} +1 -1
  61. package/dist/{retired-settings-keys-Dvei9nhG.js → retired-settings-keys-_PLI9w0k.js} +1 -1
  62. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-B_mCU6gz.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-GjlyPjm0.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { F as createUdsLoggerWithControl, I as LocalChildClient, Nt as startRunnerHeapWatch, bt as resolveAddonClass, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-python-deps-BuhBXCg0.mjs";
1
+ import { F as createUdsLoggerWithControl, I as LocalChildClient, Nt as startRunnerHeapWatch, bt as resolveAddonClass, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-python-deps-BRyNCnnG.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
3
3
  import { register } from "node:module";
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-CQs1b7uh.js");
6
+ const require_dist = require("../../dist-BVU5JADq.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 { Et as BaseAddon, Qt as EventCategory, S as addonPagesCapability, Tt as errMsg } from "../../dist-CVuVKA-O.mjs";
1
+ import { Et as BaseAddon, Qt as EventCategory, S as addonPagesCapability, Tt as errMsg } from "../../dist-CG7kwIZp.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-CQs1b7uh.js");
6
+ const require_dist = require("../../dist-BVU5JADq.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 { Et as BaseAddon, Qt as EventCategory, Tt as errMsg, w as addonWidgetsCapability } from "../../dist-CVuVKA-O.mjs";
1
+ import { Et as BaseAddon, Qt as EventCategory, Tt as errMsg, w as addonWidgetsCapability } from "../../dist-CG7kwIZp.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-CQs1b7uh.js");
6
+ const require_dist = require("../../dist-BVU5JADq.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 { Et as BaseAddon, Qt as EventCategory, T as alertsCapability, Tt as errMsg, n as AlertSchema, zt as createEvent } from "../../dist-CVuVKA-O.mjs";
1
+ import { Et as BaseAddon, Qt as EventCategory, T as alertsCapability, Tt as errMsg, n as AlertSchema, zt as createEvent } from "../../dist-CG7kwIZp.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-CQs1b7uh.js");
6
+ const require_dist = require("../../dist-BVU5JADq.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 { D as backupCapability, Et as BaseAddon, Qt as EventCategory } from "../../dist-CVuVKA-O.mjs";
1
+ import { D as backupCapability, Et as BaseAddon, Qt as EventCategory } from "../../dist-CG7kwIZp.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-CQs1b7uh.js");
6
+ const require_dist = require("../../dist-BVU5JADq.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 { Et as BaseAddon, nt as logDestinationCapability } from "../../dist-CVuVKA-O.mjs";
1
+ import { Et as BaseAddon, nt as logDestinationCapability } from "../../dist-CG7kwIZp.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-CQs1b7uh.js");
3
+ const require_dist = require("../../dist-BVU5JADq.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 { Et as BaseAddon, M as coreBlockIdFromAddonId, N as coreBlocksCapability, Tt as errMsg, c as CoreBlockSchema, j as coreBlockAddonId, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, u as DeclaredDevices } from "../../dist-CVuVKA-O.mjs";
1
+ import { Et as BaseAddon, M as coreBlockIdFromAddonId, N as coreBlocksCapability, Tt as errMsg, c as CoreBlockSchema, j as coreBlockAddonId, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, u as DeclaredDevices } from "../../dist-CG7kwIZp.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-CQs1b7uh.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-Dvei9nhG.js");
6
+ const require_dist = require("../../dist-BVU5JADq.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-_PLI9w0k.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
@@ -1465,30 +1465,25 @@ async function getDevice(pctx, input) {
1465
1465
  ...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
1466
1466
  };
1467
1467
  }
1468
- async function getChildren(pctx, input) {
1469
- const { parentDeviceId } = input;
1470
- let ownerAddonId = null;
1471
- if (pctx.registry) {
1472
- if (pctx.registry.getById(parentDeviceId)) ownerAddonId = pctx.registry.getAddonId(parentDeviceId);
1473
- }
1474
- if (!ownerAddonId) {
1475
- const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
1476
- if (!persisted) return [];
1477
- ownerAddonId = persisted.addonId;
1478
- }
1468
+ /**
1469
+ * The children of ONE parent, projected from rows and live devices the caller
1470
+ * has ALREADY read.
1471
+ *
1472
+ * Extracted so `getChildren` and `getChildrenBatch` share one projection: the
1473
+ * batch differs only in HOW the rows were fetched (one `listByParentMany`
1474
+ * instead of N `listByParent`), never in what a parent's answer looks like.
1475
+ * Byte-for-byte the body `getChildren` had.
1476
+ */
1477
+ async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren) {
1479
1478
  const results = [];
1480
1479
  const seen = /* @__PURE__ */ new Set();
1481
- const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
1482
1480
  const rowById = /* @__PURE__ */ new Map();
1483
1481
  for (const row of childRows) rowById.set(row.meta.id, row);
1484
- if (pctx.registry) {
1485
- const liveChildren = pctx.registry.getChildren(parentDeviceId);
1486
- for (const device of liveChildren) {
1487
- const key = String(device.id);
1488
- const row = rowById.get(device.id);
1489
- results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
1490
- seen.add(key);
1491
- }
1482
+ for (const device of liveChildren) {
1483
+ const key = String(device.id);
1484
+ const row = rowById.get(device.id);
1485
+ results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
1486
+ seen.add(key);
1492
1487
  }
1493
1488
  for (const row of childRows) {
1494
1489
  const m = row.meta;
@@ -1524,6 +1519,86 @@ async function getChildren(pctx, input) {
1524
1519
  }
1525
1520
  return results;
1526
1521
  }
1522
+ /**
1523
+ * Owning addonId for a parent, or `null` when the fleet does not know it.
1524
+ *
1525
+ * Children are always registered under the same addonId as their parent, so
1526
+ * the persisted-child projection is scoped by it. The live registry answers
1527
+ * for free; only a parent it does not hold costs a row read.
1528
+ */
1529
+ function liveOwnerOf(pctx, parentDeviceId) {
1530
+ if (!pctx.registry) return null;
1531
+ if (!pctx.registry.getById(parentDeviceId)) return null;
1532
+ return pctx.registry.getAddonId(parentDeviceId);
1533
+ }
1534
+ async function getChildren(pctx, input) {
1535
+ const { parentDeviceId } = input;
1536
+ let ownerAddonId = liveOwnerOf(pctx, parentDeviceId);
1537
+ if (!ownerAddonId) {
1538
+ const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
1539
+ if (!persisted) return [];
1540
+ ownerAddonId = persisted.addonId;
1541
+ }
1542
+ const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
1543
+ const liveChildren = pctx.registry?.getChildren(parentDeviceId) ?? [];
1544
+ return [...await projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren)];
1545
+ }
1546
+ /**
1547
+ * `getChildren` for a set of parents — TWO store reads for the whole set, not
1548
+ * two per parent.
1549
+ *
1550
+ * The single-parent path costs one `resolvePersistedById` (a primary-key read)
1551
+ * plus one `listByParent` (an indexed scan) EACH. Asked once per device at
1552
+ * boot that was 1 024 of each, measured on the live hub 2026-08-27 — see the
1553
+ * cap method's note. Here the ownership lookup for every parent the live
1554
+ * registry does not hold is ONE `getMany`, and the children of every parent
1555
+ * are ONE `listByParentMany`.
1556
+ *
1557
+ * The live half is indexed once too: `registry.getChildren` is
1558
+ * `getAll().filter(…)`, so calling it per parent would trade an RPC N+1 for an
1559
+ * in-memory one — 256 full sweeps of the registry per batch. One pass over
1560
+ * `getAll()` buckets every live device by its parent instead.
1561
+ *
1562
+ * A parent with no children at all is ABSENT from the record; so is a parent
1563
+ * the fleet does not know. Same contract as `DeviceRowStore.getMany` — the
1564
+ * caller asked about it and gets no row, rather than an invented empty one it
1565
+ * cannot tell apart from a real childless parent.
1566
+ */
1567
+ async function getChildrenBatch(pctx, input) {
1568
+ const parentIds = [...new Set(input.parentDeviceIds)];
1569
+ const out = {};
1570
+ if (parentIds.length === 0) return out;
1571
+ const owners = /* @__PURE__ */ new Map();
1572
+ const unownedIds = [];
1573
+ for (const parentId of parentIds) {
1574
+ const live = liveOwnerOf(pctx, parentId);
1575
+ if (live !== null) owners.set(parentId, live);
1576
+ else unownedIds.push(parentId);
1577
+ }
1578
+ if (unownedIds.length > 0) {
1579
+ const rows = await pctx.metaStore.rows.getMany(unownedIds);
1580
+ for (const [parentId, row] of rows) owners.set(parentId, row.meta.addonId);
1581
+ }
1582
+ const requested = new Set(parentIds);
1583
+ const rowsByParent = await pctx.metaStore.rows.listByParentMany([...owners.keys()]);
1584
+ const liveByParent = /* @__PURE__ */ new Map();
1585
+ for (const device of pctx.registry?.getAll() ?? []) {
1586
+ const parentId = device.parentDeviceId;
1587
+ if (parentId === null || parentId === void 0) continue;
1588
+ if (!requested.has(parentId)) continue;
1589
+ const bucket = liveByParent.get(parentId);
1590
+ if (bucket === void 0) liveByParent.set(parentId, [device]);
1591
+ else bucket.push(device);
1592
+ }
1593
+ for (const [parentId, ownerAddonId] of owners) {
1594
+ const childRows = rowsByParent.get(parentId) ?? [];
1595
+ const liveChildren = liveByParent.get(parentId) ?? [];
1596
+ if (childRows.length === 0 && liveChildren.length === 0) continue;
1597
+ const projected = await projectChildren(pctx, parentId, ownerAddonId, childRows, liveChildren);
1598
+ if (projected.length > 0) out[String(parentId)] = [...projected];
1599
+ }
1600
+ return out;
1601
+ }
1527
1602
  async function getStreamSources(pctx, input) {
1528
1603
  const { deviceId } = input;
1529
1604
  if (pctx.registry) {
@@ -3395,27 +3470,33 @@ async function setName(pctx, input) {
3395
3470
  });
3396
3471
  }
3397
3472
  /**
3398
- * Every descendant of `rootId`, walked via `parentDeviceId` chains
3399
- * (BFS, `listByParent` per level — the same indexed lookup `setName`'s
3400
- * rename cascade uses one level of). `visited` seeded with `rootId` guards a
3401
- * pathological parent-cycle in persisted data from recursing forever; a row
3402
- * already seen is never re-queued or re-emitted. Depth is unbounded on
3403
- * purposea nested container's own children are still `rootId`'s
3404
- * descendants and must align too.
3473
+ * Every descendant of `rootId`, walked via `parentDeviceId` chains.
3474
+ *
3475
+ * BFS one LEVEL per query, not one node per query: `listByParentMany` asks for
3476
+ * the whole frontier's children in a single indexed `whereIn`, so a tree costs
3477
+ * as many reads as it is DEEP, never as many as it has nodes. It used to be
3478
+ * `listByParent` per node the same N+1 the boot census measured at 1 024
3479
+ * calls on `getChildren`.
3480
+ *
3481
+ * `visited` seeded with `rootId` guards a pathological parent-cycle in
3482
+ * persisted data from recursing forever; a row already seen is never re-queued
3483
+ * or re-emitted. Depth is unbounded on purpose — a nested container's own
3484
+ * children are still `rootId`'s descendants and must align too.
3405
3485
  */
3406
3486
  async function collectDescendants(metaStore, rootId) {
3407
3487
  const out = [];
3408
3488
  const visited = new Set([rootId]);
3409
- const queue = [rootId];
3410
- while (queue.length > 0) {
3411
- const parentId = queue.shift();
3412
- if (parentId === void 0) break;
3413
- for (const row of await metaStore.rows.listByParent(parentId)) {
3489
+ let frontier = [rootId];
3490
+ while (frontier.length > 0) {
3491
+ const byParent = await metaStore.rows.listByParentMany(frontier);
3492
+ const next = [];
3493
+ for (const parentId of frontier) for (const row of byParent.get(parentId) ?? []) {
3414
3494
  if (visited.has(row.meta.id)) continue;
3415
3495
  visited.add(row.meta.id);
3416
3496
  out.push(row.meta);
3417
- queue.push(row.meta.id);
3497
+ next.push(row.meta.id);
3418
3498
  }
3499
+ frontier = next;
3419
3500
  }
3420
3501
  return out;
3421
3502
  }
@@ -4745,6 +4826,46 @@ var DeviceRowStore = class {
4745
4826
  limit: DEVICE_ROWS_FLEET_LIMIT
4746
4827
  });
4747
4828
  }
4829
+ /**
4830
+ * Direct children of MANY devices, in ONE query.
4831
+ *
4832
+ * `listByParent` called once per parent is the same shape `get` was before
4833
+ * {@link getMany}: an indexed lookup this store makes cheap, repeated until
4834
+ * it isn't. Measured on the live hub 2026-08-27, 120-second boot window:
4835
+ * `DeviceRowStore.list < DeviceRowStore.listByParent < getChildren` at
4836
+ * **1 024 calls** — one per device — for 919 rows in total. The BFS in
4837
+ * `device-meta-actions.ts#collectDescendants` walked a container tree the
4838
+ * same way, one query per node.
4839
+ *
4840
+ * `whereIn` on the indexed `parentDeviceId` column collapses a level of that
4841
+ * walk into a single statement. Contract, verbatim from {@link getMany}: an
4842
+ * EMPTY `parentDeviceIds` reads nothing at all rather than degrading to
4843
+ * "everything", and a parent with no children — or one the fleet does not
4844
+ * know — is simply ABSENT from the map, never an invented empty row.
4845
+ */
4846
+ async listByParentMany(parentDeviceIds) {
4847
+ const out = /* @__PURE__ */ new Map();
4848
+ const unique = [...new Set(parentDeviceIds)];
4849
+ if (unique.length === 0) return out;
4850
+ const rows = await this.list({
4851
+ whereIn: { parentDeviceId: unique },
4852
+ orderBy: {
4853
+ field: "deviceId",
4854
+ direction: "asc"
4855
+ },
4856
+ limit: DEVICE_ROWS_FLEET_LIMIT
4857
+ });
4858
+ const byParent = /* @__PURE__ */ new Map();
4859
+ for (const row of rows) {
4860
+ const parent = row.meta.parentDeviceId;
4861
+ if (parent === null || parent === void 0) continue;
4862
+ const bucket = byParent.get(parent);
4863
+ if (bucket === void 0) byParent.set(parent, [row]);
4864
+ else bucket.push(row);
4865
+ }
4866
+ for (const [parent, bucket] of byParent) out.set(parent, bucket);
4867
+ return out;
4868
+ }
4748
4869
  /** How many devices the fleet holds. Used to tell "empty store" from "gone device". */
4749
4870
  async count() {
4750
4871
  await this.declare();
@@ -5565,6 +5686,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
5565
5686
  listAll: (input) => listAll(pctx, input),
5566
5687
  getDevice: (input) => getDevice(pctx, input),
5567
5688
  getChildren: (input) => getChildren(pctx, input),
5689
+ getChildrenBatch: (input) => getChildrenBatch(pctx, input),
5568
5690
  getLinkedDevices: (input) => getLinkedDevices(pctx, input),
5569
5691
  getLinkedDevicesBatch: (input) => getLinkedDevicesBatch(pctx, input),
5570
5692
  getDeviceSettingsContribution: (input) => buildLinkedDevicesContribution(pctx, input.deviceId),
@@ -1,5 +1,5 @@
1
- import { A as buildStreamParamsConfigSchema, At as DeviceFeature, Et as BaseAddon, Ft as WELL_KNOWN_TAB_MAP, H as enumerateItemArrayFields, L as deviceManagerCapability, Mt as DeviceType, Qt as EventCategory, R as deviceStateCapability, Tt as errMsg, U as enumerateSchemaFields, Wt as isDeviceConfigCap, Zt as sleep, _ as STREAM_PROFILE_META, a as CAP_NAMES_WITH_STATUS, ct as parseStreamParamsFormPatch, d as DeviceStatusSchema, ft as runtimeStatePolicyFor, jt as DeviceRole, ot as normalizeUnit, t as ALL_CAPABILITY_DEFINITIONS, z as deviceStatusCapability } from "../../dist-CVuVKA-O.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-DnMYSIEr.mjs";
1
+ import { A as buildStreamParamsConfigSchema, At as DeviceFeature, Et as BaseAddon, Ft as WELL_KNOWN_TAB_MAP, H as enumerateItemArrayFields, L as deviceManagerCapability, Mt as DeviceType, Qt as EventCategory, R as deviceStateCapability, Tt as errMsg, U as enumerateSchemaFields, Wt as isDeviceConfigCap, Zt as sleep, _ as STREAM_PROFILE_META, a as CAP_NAMES_WITH_STATUS, ct as parseStreamParamsFormPatch, d as DeviceStatusSchema, ft as runtimeStatePolicyFor, jt as DeviceRole, ot as normalizeUnit, t as ALL_CAPABILITY_DEFINITIONS, z as deviceStatusCapability } from "../../dist-CG7kwIZp.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Dm1z4mO4.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
@@ -1460,30 +1460,25 @@ async function getDevice(pctx, input) {
1460
1460
  ...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
1461
1461
  };
1462
1462
  }
1463
- async function getChildren(pctx, input) {
1464
- const { parentDeviceId } = input;
1465
- let ownerAddonId = null;
1466
- if (pctx.registry) {
1467
- if (pctx.registry.getById(parentDeviceId)) ownerAddonId = pctx.registry.getAddonId(parentDeviceId);
1468
- }
1469
- if (!ownerAddonId) {
1470
- const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
1471
- if (!persisted) return [];
1472
- ownerAddonId = persisted.addonId;
1473
- }
1463
+ /**
1464
+ * The children of ONE parent, projected from rows and live devices the caller
1465
+ * has ALREADY read.
1466
+ *
1467
+ * Extracted so `getChildren` and `getChildrenBatch` share one projection: the
1468
+ * batch differs only in HOW the rows were fetched (one `listByParentMany`
1469
+ * instead of N `listByParent`), never in what a parent's answer looks like.
1470
+ * Byte-for-byte the body `getChildren` had.
1471
+ */
1472
+ async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren) {
1474
1473
  const results = [];
1475
1474
  const seen = /* @__PURE__ */ new Set();
1476
- const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
1477
1475
  const rowById = /* @__PURE__ */ new Map();
1478
1476
  for (const row of childRows) rowById.set(row.meta.id, row);
1479
- if (pctx.registry) {
1480
- const liveChildren = pctx.registry.getChildren(parentDeviceId);
1481
- for (const device of liveChildren) {
1482
- const key = String(device.id);
1483
- const row = rowById.get(device.id);
1484
- results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
1485
- seen.add(key);
1486
- }
1477
+ for (const device of liveChildren) {
1478
+ const key = String(device.id);
1479
+ const row = rowById.get(device.id);
1480
+ results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
1481
+ seen.add(key);
1487
1482
  }
1488
1483
  for (const row of childRows) {
1489
1484
  const m = row.meta;
@@ -1519,6 +1514,86 @@ async function getChildren(pctx, input) {
1519
1514
  }
1520
1515
  return results;
1521
1516
  }
1517
+ /**
1518
+ * Owning addonId for a parent, or `null` when the fleet does not know it.
1519
+ *
1520
+ * Children are always registered under the same addonId as their parent, so
1521
+ * the persisted-child projection is scoped by it. The live registry answers
1522
+ * for free; only a parent it does not hold costs a row read.
1523
+ */
1524
+ function liveOwnerOf(pctx, parentDeviceId) {
1525
+ if (!pctx.registry) return null;
1526
+ if (!pctx.registry.getById(parentDeviceId)) return null;
1527
+ return pctx.registry.getAddonId(parentDeviceId);
1528
+ }
1529
+ async function getChildren(pctx, input) {
1530
+ const { parentDeviceId } = input;
1531
+ let ownerAddonId = liveOwnerOf(pctx, parentDeviceId);
1532
+ if (!ownerAddonId) {
1533
+ const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
1534
+ if (!persisted) return [];
1535
+ ownerAddonId = persisted.addonId;
1536
+ }
1537
+ const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
1538
+ const liveChildren = pctx.registry?.getChildren(parentDeviceId) ?? [];
1539
+ return [...await projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren)];
1540
+ }
1541
+ /**
1542
+ * `getChildren` for a set of parents — TWO store reads for the whole set, not
1543
+ * two per parent.
1544
+ *
1545
+ * The single-parent path costs one `resolvePersistedById` (a primary-key read)
1546
+ * plus one `listByParent` (an indexed scan) EACH. Asked once per device at
1547
+ * boot that was 1 024 of each, measured on the live hub 2026-08-27 — see the
1548
+ * cap method's note. Here the ownership lookup for every parent the live
1549
+ * registry does not hold is ONE `getMany`, and the children of every parent
1550
+ * are ONE `listByParentMany`.
1551
+ *
1552
+ * The live half is indexed once too: `registry.getChildren` is
1553
+ * `getAll().filter(…)`, so calling it per parent would trade an RPC N+1 for an
1554
+ * in-memory one — 256 full sweeps of the registry per batch. One pass over
1555
+ * `getAll()` buckets every live device by its parent instead.
1556
+ *
1557
+ * A parent with no children at all is ABSENT from the record; so is a parent
1558
+ * the fleet does not know. Same contract as `DeviceRowStore.getMany` — the
1559
+ * caller asked about it and gets no row, rather than an invented empty one it
1560
+ * cannot tell apart from a real childless parent.
1561
+ */
1562
+ async function getChildrenBatch(pctx, input) {
1563
+ const parentIds = [...new Set(input.parentDeviceIds)];
1564
+ const out = {};
1565
+ if (parentIds.length === 0) return out;
1566
+ const owners = /* @__PURE__ */ new Map();
1567
+ const unownedIds = [];
1568
+ for (const parentId of parentIds) {
1569
+ const live = liveOwnerOf(pctx, parentId);
1570
+ if (live !== null) owners.set(parentId, live);
1571
+ else unownedIds.push(parentId);
1572
+ }
1573
+ if (unownedIds.length > 0) {
1574
+ const rows = await pctx.metaStore.rows.getMany(unownedIds);
1575
+ for (const [parentId, row] of rows) owners.set(parentId, row.meta.addonId);
1576
+ }
1577
+ const requested = new Set(parentIds);
1578
+ const rowsByParent = await pctx.metaStore.rows.listByParentMany([...owners.keys()]);
1579
+ const liveByParent = /* @__PURE__ */ new Map();
1580
+ for (const device of pctx.registry?.getAll() ?? []) {
1581
+ const parentId = device.parentDeviceId;
1582
+ if (parentId === null || parentId === void 0) continue;
1583
+ if (!requested.has(parentId)) continue;
1584
+ const bucket = liveByParent.get(parentId);
1585
+ if (bucket === void 0) liveByParent.set(parentId, [device]);
1586
+ else bucket.push(device);
1587
+ }
1588
+ for (const [parentId, ownerAddonId] of owners) {
1589
+ const childRows = rowsByParent.get(parentId) ?? [];
1590
+ const liveChildren = liveByParent.get(parentId) ?? [];
1591
+ if (childRows.length === 0 && liveChildren.length === 0) continue;
1592
+ const projected = await projectChildren(pctx, parentId, ownerAddonId, childRows, liveChildren);
1593
+ if (projected.length > 0) out[String(parentId)] = [...projected];
1594
+ }
1595
+ return out;
1596
+ }
1522
1597
  async function getStreamSources(pctx, input) {
1523
1598
  const { deviceId } = input;
1524
1599
  if (pctx.registry) {
@@ -3390,27 +3465,33 @@ async function setName(pctx, input) {
3390
3465
  });
3391
3466
  }
3392
3467
  /**
3393
- * Every descendant of `rootId`, walked via `parentDeviceId` chains
3394
- * (BFS, `listByParent` per level — the same indexed lookup `setName`'s
3395
- * rename cascade uses one level of). `visited` seeded with `rootId` guards a
3396
- * pathological parent-cycle in persisted data from recursing forever; a row
3397
- * already seen is never re-queued or re-emitted. Depth is unbounded on
3398
- * purposea nested container's own children are still `rootId`'s
3399
- * descendants and must align too.
3468
+ * Every descendant of `rootId`, walked via `parentDeviceId` chains.
3469
+ *
3470
+ * BFS one LEVEL per query, not one node per query: `listByParentMany` asks for
3471
+ * the whole frontier's children in a single indexed `whereIn`, so a tree costs
3472
+ * as many reads as it is DEEP, never as many as it has nodes. It used to be
3473
+ * `listByParent` per node the same N+1 the boot census measured at 1 024
3474
+ * calls on `getChildren`.
3475
+ *
3476
+ * `visited` seeded with `rootId` guards a pathological parent-cycle in
3477
+ * persisted data from recursing forever; a row already seen is never re-queued
3478
+ * or re-emitted. Depth is unbounded on purpose — a nested container's own
3479
+ * children are still `rootId`'s descendants and must align too.
3400
3480
  */
3401
3481
  async function collectDescendants(metaStore, rootId) {
3402
3482
  const out = [];
3403
3483
  const visited = new Set([rootId]);
3404
- const queue = [rootId];
3405
- while (queue.length > 0) {
3406
- const parentId = queue.shift();
3407
- if (parentId === void 0) break;
3408
- for (const row of await metaStore.rows.listByParent(parentId)) {
3484
+ let frontier = [rootId];
3485
+ while (frontier.length > 0) {
3486
+ const byParent = await metaStore.rows.listByParentMany(frontier);
3487
+ const next = [];
3488
+ for (const parentId of frontier) for (const row of byParent.get(parentId) ?? []) {
3409
3489
  if (visited.has(row.meta.id)) continue;
3410
3490
  visited.add(row.meta.id);
3411
3491
  out.push(row.meta);
3412
- queue.push(row.meta.id);
3492
+ next.push(row.meta.id);
3413
3493
  }
3494
+ frontier = next;
3414
3495
  }
3415
3496
  return out;
3416
3497
  }
@@ -4740,6 +4821,46 @@ var DeviceRowStore = class {
4740
4821
  limit: DEVICE_ROWS_FLEET_LIMIT
4741
4822
  });
4742
4823
  }
4824
+ /**
4825
+ * Direct children of MANY devices, in ONE query.
4826
+ *
4827
+ * `listByParent` called once per parent is the same shape `get` was before
4828
+ * {@link getMany}: an indexed lookup this store makes cheap, repeated until
4829
+ * it isn't. Measured on the live hub 2026-08-27, 120-second boot window:
4830
+ * `DeviceRowStore.list < DeviceRowStore.listByParent < getChildren` at
4831
+ * **1 024 calls** — one per device — for 919 rows in total. The BFS in
4832
+ * `device-meta-actions.ts#collectDescendants` walked a container tree the
4833
+ * same way, one query per node.
4834
+ *
4835
+ * `whereIn` on the indexed `parentDeviceId` column collapses a level of that
4836
+ * walk into a single statement. Contract, verbatim from {@link getMany}: an
4837
+ * EMPTY `parentDeviceIds` reads nothing at all rather than degrading to
4838
+ * "everything", and a parent with no children — or one the fleet does not
4839
+ * know — is simply ABSENT from the map, never an invented empty row.
4840
+ */
4841
+ async listByParentMany(parentDeviceIds) {
4842
+ const out = /* @__PURE__ */ new Map();
4843
+ const unique = [...new Set(parentDeviceIds)];
4844
+ if (unique.length === 0) return out;
4845
+ const rows = await this.list({
4846
+ whereIn: { parentDeviceId: unique },
4847
+ orderBy: {
4848
+ field: "deviceId",
4849
+ direction: "asc"
4850
+ },
4851
+ limit: DEVICE_ROWS_FLEET_LIMIT
4852
+ });
4853
+ const byParent = /* @__PURE__ */ new Map();
4854
+ for (const row of rows) {
4855
+ const parent = row.meta.parentDeviceId;
4856
+ if (parent === null || parent === void 0) continue;
4857
+ const bucket = byParent.get(parent);
4858
+ if (bucket === void 0) byParent.set(parent, [row]);
4859
+ else bucket.push(row);
4860
+ }
4861
+ for (const [parent, bucket] of byParent) out.set(parent, bucket);
4862
+ return out;
4863
+ }
4743
4864
  /** How many devices the fleet holds. Used to tell "empty store" from "gone device". */
4744
4865
  async count() {
4745
4866
  await this.declare();
@@ -5560,6 +5681,7 @@ var DeviceManagerAddon = class extends BaseAddon {
5560
5681
  listAll: (input) => listAll(pctx, input),
5561
5682
  getDevice: (input) => getDevice(pctx, input),
5562
5683
  getChildren: (input) => getChildren(pctx, input),
5684
+ getChildrenBatch: (input) => getChildrenBatch(pctx, input),
5563
5685
  getLinkedDevices: (input) => getLinkedDevices(pctx, input),
5564
5686
  getLinkedDevicesBatch: (input) => getLinkedDevicesBatch(pctx, input),
5565
5687
  getDeviceSettingsContribution: (input) => buildLinkedDevicesContribution(pctx, input.deviceId),
@@ -5,6 +5,28 @@ export declare function listPersistedByAddon(pctx: ProviderContext, input: Param
5
5
  export declare function listAll(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['listAll']>[0]): ReturnType<IDeviceManagerProvider['listAll']>;
6
6
  export declare function getDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getDevice']>[0]): ReturnType<IDeviceManagerProvider['getDevice']>;
7
7
  export declare function getChildren(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getChildren']>[0]): ReturnType<IDeviceManagerProvider['getChildren']>;
8
+ /**
9
+ * `getChildren` for a set of parents — TWO store reads for the whole set, not
10
+ * two per parent.
11
+ *
12
+ * The single-parent path costs one `resolvePersistedById` (a primary-key read)
13
+ * plus one `listByParent` (an indexed scan) EACH. Asked once per device at
14
+ * boot that was 1 024 of each, measured on the live hub 2026-08-27 — see the
15
+ * cap method's note. Here the ownership lookup for every parent the live
16
+ * registry does not hold is ONE `getMany`, and the children of every parent
17
+ * are ONE `listByParentMany`.
18
+ *
19
+ * The live half is indexed once too: `registry.getChildren` is
20
+ * `getAll().filter(…)`, so calling it per parent would trade an RPC N+1 for an
21
+ * in-memory one — 256 full sweeps of the registry per batch. One pass over
22
+ * `getAll()` buckets every live device by its parent instead.
23
+ *
24
+ * A parent with no children at all is ABSENT from the record; so is a parent
25
+ * the fleet does not know. Same contract as `DeviceRowStore.getMany` — the
26
+ * caller asked about it and gets no row, rather than an invented empty one it
27
+ * cannot tell apart from a real childless parent.
28
+ */
29
+ export declare function getChildrenBatch(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getChildrenBatch']>[0]): ReturnType<IDeviceManagerProvider['getChildrenBatch']>;
8
30
  export declare function getStreamSources(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getStreamSources']>[0]): ReturnType<IDeviceManagerProvider['getStreamSources']>;
9
31
  export declare function getConfigSchema(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getConfigSchema']>[0]): ReturnType<IDeviceManagerProvider['getConfigSchema']>;
10
32
  export declare function getSettingsSchema(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getSettingsSchema']>[0]): ReturnType<IDeviceManagerProvider['getSettingsSchema']>;