@camstack/system 1.2.157 → 1.2.160
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +17 -16
- package/dist/addon-runner.mjs +2 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +10 -7
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +50 -12
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +50 -12
- package/dist/builtins/backup-orchestrator/write-targets.d.ts +63 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-bindings-store.d.ts +56 -10
- package/dist/builtins/device-manager/device-manager.addon.d.ts +11 -0
- package/dist/builtins/device-manager/device-manager.addon.js +216 -81
- package/dist/builtins/device-manager/device-manager.addon.mjs +216 -81
- package/dist/builtins/device-manager/device-meta-actions.d.ts +9 -3
- package/dist/builtins/device-manager/device-meta-store.d.ts +32 -5
- package/dist/builtins/device-manager/device-queries.d.ts +7 -3
- package/dist/builtins/device-manager/write-lock.d.ts +17 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/filter-compiler.d.ts +41 -2
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +15 -7
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/sqlite-storage/vector-index-shared.d.ts +30 -5
- package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +28 -6
- package/dist/builtins/storage-orchestrator/access-guards.d.ts +32 -0
- package/dist/builtins/storage-orchestrator/location-secrets.d.ts +33 -0
- package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +74 -3
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +26 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +552 -15
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +552 -15
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +56 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-CDgIzo82.js → dist-CcvXUhHK.js} +537 -17
- package/dist/{dist-LvKus-mT.mjs → dist-DKs---Vx.mjs} +520 -18
- package/dist/index.js +141 -139
- package/dist/index.mjs +3 -3
- package/dist/kernel/deps/manifest-system-deps.d.ts +15 -0
- package/dist/kernel/index.d.ts +4 -2
- package/dist/kernel/moleculer/cap-usage-registry.d.ts +132 -7
- package/dist/kernel/storage-location-registry.d.ts +16 -1
- package/dist/kernel/transport/local-child-registry.d.ts +57 -0
- package/dist/{manifest-python-deps-CD14Pv0-.mjs → manifest-system-deps-DnoPpLGp.mjs} +466 -83
- package/dist/{manifest-python-deps-DVODn-qc.js → manifest-system-deps-oJMkWSX-.js} +477 -82
- package/dist/{retired-settings-keys-CV4QVdXj.mjs → retired-settings-keys-C3eONTvV.mjs} +1 -1
- package/dist/{retired-settings-keys-BfAzWvPC.js → retired-settings-keys-DXZ2xe7C.js} +1 -1
- package/package.json +2 -1
|
@@ -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-
|
|
7
|
-
const require_retired_settings_keys = require("../../retired-settings-keys-
|
|
6
|
+
const require_dist = require("../../dist-CcvXUhHK.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-DXZ2xe7C.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
|
|
@@ -569,11 +569,31 @@ function resolveDeviceType(deps, row, deviceId) {
|
|
|
569
569
|
* same reason `getAllBindings` warns instead of reporting zero devices. That is
|
|
570
570
|
* the only reason this is async: the `COUNT(*)` runs solely on the miss path,
|
|
571
571
|
* where the alternative is to call a device gone because the store is empty.
|
|
572
|
+
*
|
|
573
|
+
* `ledgerNonEmpty` is passed IN rather than defaulted, deliberately. A default
|
|
574
|
+
* that issued its own `rows.count()` would be a per-device read hiding behind
|
|
575
|
+
* an optional argument — the exact shape this file just removed from the
|
|
576
|
+
* activations blob — and nothing would fail when a future caller took it.
|
|
572
577
|
*/
|
|
573
|
-
async function resolveDevicePresence(deps, row, deviceId) {
|
|
578
|
+
async function resolveDevicePresence(deps, row, deviceId, ledgerNonEmpty) {
|
|
574
579
|
if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
|
|
575
580
|
if (row !== null) return "present";
|
|
576
|
-
return await
|
|
581
|
+
return await ledgerNonEmpty() ? "absent" : "unknown";
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* `deps.rows.count()`, collapsed to at most one call per pass.
|
|
585
|
+
*
|
|
586
|
+
* The count answers a FLEET question ("has device restore run at all?"), not a
|
|
587
|
+
* per-device one, and it is reached only when a device misses both the live
|
|
588
|
+
* registry and the ledger. A fleet sweep over a store that is genuinely empty
|
|
589
|
+
* hits that path once per device.
|
|
590
|
+
*/
|
|
591
|
+
function onceLedgerNonEmpty(deps) {
|
|
592
|
+
let pending = null;
|
|
593
|
+
return () => {
|
|
594
|
+
if (pending === null) pending = deps.rows.count().then((n) => n > 0);
|
|
595
|
+
return pending;
|
|
596
|
+
};
|
|
577
597
|
}
|
|
578
598
|
/**
|
|
579
599
|
* Does a capability apply to a device of type `deviceType`?
|
|
@@ -594,8 +614,11 @@ function capAppliesToDeviceType(def, deviceType) {
|
|
|
594
614
|
return declared.some((t) => t === deviceType);
|
|
595
615
|
}
|
|
596
616
|
async function getBindings(deps, input) {
|
|
597
|
-
const row = await deps.rows.get(input.deviceId);
|
|
598
|
-
return resolveBindingsForDevice(deps, input.deviceId, row
|
|
617
|
+
const [row, store] = await Promise.all([deps.rows.get(input.deviceId), readBindingsStore(deps)]);
|
|
618
|
+
return resolveBindingsForDevice(deps, input.deviceId, row, {
|
|
619
|
+
store,
|
|
620
|
+
ledgerNonEmpty: onceLedgerNonEmpty(deps)
|
|
621
|
+
});
|
|
599
622
|
}
|
|
600
623
|
/**
|
|
601
624
|
* The `getBindings` resolver, taking the device's row as a PARAMETER instead
|
|
@@ -610,12 +633,22 @@ async function getBindings(deps, input) {
|
|
|
610
633
|
* iteration shape measured on the live hub 2026-08-25: `get
|
|
611
634
|
* device-manager:devices` at 1 656 calls/min steady-state and 17 687 during
|
|
612
635
|
* boot, one row per call.
|
|
636
|
+
*
|
|
637
|
+
* The {@link BindingsPass} is the SECOND half of that same split, and it is
|
|
638
|
+
* the one that was missing. This function used to issue its own
|
|
639
|
+
* `readBindingsStore(deps)` — one settings-door round trip per device, for a
|
|
640
|
+
* blob that is identical for every device in the pass. `getAllBindings` on the
|
|
641
|
+
* live 1 019-device hub therefore cost 1 020 round trips; it never answered
|
|
642
|
+
* inside `SystemMirror.init`'s 15 s budget, and the admin UI refused to boot.
|
|
643
|
+
* Now every entry point resolves the pass once and hands it down — so this
|
|
644
|
+
* function performs NO store reads of its own, and every device in a sweep
|
|
645
|
+
* sees the same version of the activations.
|
|
613
646
|
*/
|
|
614
|
-
async function resolveBindingsForDevice(deps, deviceId, row) {
|
|
647
|
+
async function resolveBindingsForDevice(deps, deviceId, row, pass) {
|
|
615
648
|
const storeKey = String(deviceId);
|
|
616
|
-
const perDevice =
|
|
649
|
+
const perDevice = pass.store.deviceBindings[storeKey] ?? {};
|
|
617
650
|
const deviceType = resolveDeviceType(deps, row, deviceId);
|
|
618
|
-
const presence = await resolveDevicePresence(deps, row, deviceId);
|
|
651
|
+
const presence = await resolveDevicePresence(deps, row, deviceId, pass.ledgerNonEmpty);
|
|
619
652
|
const entries = [];
|
|
620
653
|
const seenCaps = /* @__PURE__ */ new Set();
|
|
621
654
|
const resolveRemote = (capName) => deps.remoteNativeCaps.get(deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, deviceId);
|
|
@@ -723,21 +756,33 @@ async function resolveBindingsForDevice(deps, deviceId, row) {
|
|
|
723
756
|
* `getAllBindings` (511 ms / 679 KB of mostly-unwanted rows).
|
|
724
757
|
*
|
|
725
758
|
* Sequential on purpose, like `getAllBindings`: `resolveBindingsForDevice`'s
|
|
726
|
-
* remaining per-device work
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
759
|
+
* remaining per-device work is the routing rules, which run on this process's
|
|
760
|
+
* own event loop, so concurrency buys nothing there and a fan-out of
|
|
761
|
+
* `Promise.all` over hundreds of ids only makes the queue less fair to
|
|
762
|
+
* everything else on it. Both parts that DO amortise across the whole set are
|
|
763
|
+
* fetched ONCE, ahead of the loop, and in parallel with each other:
|
|
764
|
+
*
|
|
765
|
+
* - the device rows ({@link DeviceRowStore.getMany}) rather than once per
|
|
766
|
+
* iteration — measured on the live hub 2026-08-25, `get
|
|
767
|
+
* device-manager:devices` ran 1 656 calls/min steady-state, one row per
|
|
768
|
+
* call, which is exactly what looping `getBindings`'s own
|
|
769
|
+
* `rows.get(deviceId)` here produced;
|
|
770
|
+
* - the wrapper-activation blob, which the per-device resolver used to read
|
|
771
|
+
* itself — a second settings-door round trip per id, on the call that
|
|
772
|
+
* exists precisely so a caller needing many devices does not pay per
|
|
773
|
+
* device. Measured on the live hub 2026-08-30, mid-drain:
|
|
774
|
+
* `getBindingsBatch` of 1 id = 5.78 s, of 8 ids = 17.16 s — a slope of
|
|
775
|
+
* 1.63 s PER ADDITIONAL DEVICE, on a call that ships 4.5 KB a row.
|
|
735
776
|
*/
|
|
736
777
|
async function getBindingsBatch(deps, input) {
|
|
737
778
|
const ids = [...new Set(input.deviceIds)];
|
|
738
|
-
const rows = await deps.rows.getMany(ids);
|
|
779
|
+
const [rows, store] = await Promise.all([deps.rows.getMany(ids), readBindingsStore(deps)]);
|
|
780
|
+
const pass = {
|
|
781
|
+
store,
|
|
782
|
+
ledgerNonEmpty: onceLedgerNonEmpty(deps)
|
|
783
|
+
};
|
|
739
784
|
const out = [];
|
|
740
|
-
for (const deviceId of ids) out.push(await resolveBindingsForDevice(deps, deviceId, rows.get(deviceId) ?? null));
|
|
785
|
+
for (const deviceId of ids) out.push(await resolveBindingsForDevice(deps, deviceId, rows.get(deviceId) ?? null, pass));
|
|
741
786
|
return out;
|
|
742
787
|
}
|
|
743
788
|
/**
|
|
@@ -750,9 +795,18 @@ async function getBindingsBatch(deps, input) {
|
|
|
750
795
|
* clients invalidate via the existing
|
|
751
796
|
* `capability.binding-changed` event rather than re-fetching this
|
|
752
797
|
* payload periodically.
|
|
798
|
+
*
|
|
799
|
+
* **"Single round-trip" was a claim, not a fact, until 2026-08-30.** The
|
|
800
|
+
* per-device resolver read the wrapper-activation blob itself, so this call
|
|
801
|
+
* issued one settings-door RPC per device — 1 020 of them on the live
|
|
802
|
+
* 1 019-device hub, at ~2.9 s each while the storage drain ran. It did not
|
|
803
|
+
* return in 240 s, and `SystemMirror.init` gives it 15 s, so the admin UI
|
|
804
|
+
* could not boot at all. Now the whole pass costs exactly TWO reads (the fleet
|
|
805
|
+
* listing and the blob), issued in parallel, and nothing in the loop awaits a
|
|
806
|
+
* store.
|
|
753
807
|
*/
|
|
754
808
|
async function getAllBindings(deps) {
|
|
755
|
-
const fleet = await deps.rows.listAll();
|
|
809
|
+
const [fleet, store] = await Promise.all([deps.rows.listAll(), readBindingsStore(deps)]);
|
|
756
810
|
const rowById = /* @__PURE__ */ new Map();
|
|
757
811
|
const ids = /* @__PURE__ */ new Set();
|
|
758
812
|
for (const row of fleet) {
|
|
@@ -765,8 +819,12 @@ async function getAllBindings(deps) {
|
|
|
765
819
|
deps.ctx.logger.warn("getAllBindings found no devices — warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
|
|
766
820
|
return [];
|
|
767
821
|
}
|
|
822
|
+
const pass = {
|
|
823
|
+
store,
|
|
824
|
+
ledgerNonEmpty: onceLedgerNonEmpty(deps)
|
|
825
|
+
};
|
|
768
826
|
const out = [];
|
|
769
|
-
for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await resolveBindingsForDevice(deps, deviceId, rowById.get(deviceId) ?? null));
|
|
827
|
+
for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await resolveBindingsForDevice(deps, deviceId, rowById.get(deviceId) ?? null, pass));
|
|
770
828
|
return out;
|
|
771
829
|
}
|
|
772
830
|
/**
|
|
@@ -1466,15 +1524,39 @@ async function getDevice(pctx, input) {
|
|
|
1466
1524
|
};
|
|
1467
1525
|
}
|
|
1468
1526
|
/**
|
|
1469
|
-
* The
|
|
1470
|
-
*
|
|
1527
|
+
* The child ids whose config comes from the SETTINGS STORE — the rows the live
|
|
1528
|
+
* registry does not answer for.
|
|
1529
|
+
*
|
|
1530
|
+
* The one place the "persisted fills in what the registry does not hold" rule
|
|
1531
|
+
* is spelled, so the pre-read and the projection cannot disagree about which
|
|
1532
|
+
* children need a store value. On the live hub 2026-08-30 this is every child
|
|
1533
|
+
* there is: all 896 belong to forked provider addons, so hub-main's registry
|
|
1534
|
+
* holds none of them.
|
|
1535
|
+
*/
|
|
1536
|
+
function persistedChildIds(childRows, liveChildren) {
|
|
1537
|
+
const live = /* @__PURE__ */ new Set();
|
|
1538
|
+
for (const device of liveChildren) live.add(device.id);
|
|
1539
|
+
return childRows.filter((row) => !live.has(row.meta.id)).map((row) => row.meta.id);
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* The children of ONE parent, projected from rows, live devices AND configs the
|
|
1543
|
+
* caller has ALREADY read.
|
|
1471
1544
|
*
|
|
1472
1545
|
* Extracted so `getChildren` and `getChildrenBatch` share one projection: the
|
|
1473
1546
|
* batch differs only in HOW the rows were fetched (one `listByParentMany`
|
|
1474
1547
|
* instead of N `listByParent`), never in what a parent's answer looks like.
|
|
1475
|
-
*
|
|
1548
|
+
*
|
|
1549
|
+
* **Not async, and that is the contract.** This function used to `await
|
|
1550
|
+
* pctx.settings.readDeviceStore(m.id)` inside its own loop — one settings-door
|
|
1551
|
+
* round trip per persisted child, sequentially, on hub-main's event loop. The
|
|
1552
|
+
* caller above (`getChildrenBatch`, coalescing every accessory reconcile in
|
|
1553
|
+
* flight) had already removed the ledger N+1 and left this one: 896 door reads
|
|
1554
|
+
* per boot reconcile and per device page, at ~2.9 s each mid-drain. `configs`
|
|
1555
|
+
* arrives resolved through `readDeviceConfigs` — one round trip for the whole
|
|
1556
|
+
* pass — and a signature with no `Promise` in it means a future read cannot be
|
|
1557
|
+
* put back here without the type saying so.
|
|
1476
1558
|
*/
|
|
1477
|
-
|
|
1559
|
+
function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren, configs) {
|
|
1478
1560
|
const results = [];
|
|
1479
1561
|
const seen = /* @__PURE__ */ new Set();
|
|
1480
1562
|
const rowById = /* @__PURE__ */ new Map();
|
|
@@ -1490,9 +1572,9 @@ async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, li
|
|
|
1490
1572
|
const childStableId = m.stableId;
|
|
1491
1573
|
const key = String(m.id);
|
|
1492
1574
|
if (seen.has(key)) continue;
|
|
1493
|
-
const persistedConfig =
|
|
1575
|
+
const persistedConfig = configs.get(m.id) ?? {};
|
|
1494
1576
|
const metadata = row.metadata;
|
|
1495
|
-
const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig
|
|
1577
|
+
const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig, childStableId, ownerAddonId);
|
|
1496
1578
|
results.push({
|
|
1497
1579
|
id: m.id,
|
|
1498
1580
|
stableId: childStableId,
|
|
@@ -1507,7 +1589,7 @@ async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, li
|
|
|
1507
1589
|
probed: pctx.host.resolveDeviceProbed(m.id),
|
|
1508
1590
|
features: persistedFeatures(m.features),
|
|
1509
1591
|
isCamera: false,
|
|
1510
|
-
config: persistedConfig
|
|
1592
|
+
config: persistedConfig,
|
|
1511
1593
|
metadata,
|
|
1512
1594
|
...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
1513
1595
|
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
@@ -1541,18 +1623,23 @@ async function getChildren(pctx, input) {
|
|
|
1541
1623
|
}
|
|
1542
1624
|
const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
|
|
1543
1625
|
const liveChildren = pctx.registry?.getChildren(parentDeviceId) ?? [];
|
|
1544
|
-
|
|
1626
|
+
const configs = await readDeviceConfigs(pctx, persistedChildIds(childRows, liveChildren));
|
|
1627
|
+
return [...projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren, configs)];
|
|
1545
1628
|
}
|
|
1546
1629
|
/**
|
|
1547
|
-
* `getChildren` for a set of parents —
|
|
1548
|
-
*
|
|
1630
|
+
* `getChildren` for a set of parents — THREE store reads for the whole set, not
|
|
1631
|
+
* three per parent (and not one per child).
|
|
1549
1632
|
*
|
|
1550
1633
|
* The single-parent path costs one `resolvePersistedById` (a primary-key read)
|
|
1551
1634
|
* plus one `listByParent` (an indexed scan) EACH. Asked once per device at
|
|
1552
1635
|
* boot that was 1 024 of each, measured on the live hub 2026-08-27 — see the
|
|
1553
1636
|
* cap method's note. Here the ownership lookup for every parent the live
|
|
1554
1637
|
* registry does not hold is ONE `getMany`, and the children of every parent
|
|
1555
|
-
* are ONE `listByParentMany`.
|
|
1638
|
+
* are ONE `listByParentMany`. The third read is the settings door, and it is
|
|
1639
|
+
* one for the whole batch: `readDeviceConfigs` over every persisted child of
|
|
1640
|
+
* every parent. That one was the leftover — the ledger N+1 was removed and a
|
|
1641
|
+
* door round trip PER CHILD stayed behind it (896 on the live hub 2026-08-30,
|
|
1642
|
+
* all of them forked-owned and therefore all persisted).
|
|
1556
1643
|
*
|
|
1557
1644
|
* The live half is indexed once too: `registry.getChildren` is
|
|
1558
1645
|
* `getAll().filter(…)`, so calling it per parent would trade an RPC N+1 for an
|
|
@@ -1590,11 +1677,14 @@ async function getChildrenBatch(pctx, input) {
|
|
|
1590
1677
|
if (bucket === void 0) liveByParent.set(parentId, [device]);
|
|
1591
1678
|
else bucket.push(device);
|
|
1592
1679
|
}
|
|
1680
|
+
const configIds = [];
|
|
1681
|
+
for (const parentId of owners.keys()) configIds.push(...persistedChildIds(rowsByParent.get(parentId) ?? [], liveByParent.get(parentId) ?? []));
|
|
1682
|
+
const configs = await readDeviceConfigs(pctx, configIds);
|
|
1593
1683
|
for (const [parentId, ownerAddonId] of owners) {
|
|
1594
1684
|
const childRows = rowsByParent.get(parentId) ?? [];
|
|
1595
1685
|
const liveChildren = liveByParent.get(parentId) ?? [];
|
|
1596
1686
|
if (childRows.length === 0 && liveChildren.length === 0) continue;
|
|
1597
|
-
const projected =
|
|
1687
|
+
const projected = projectChildren(pctx, parentId, ownerAddonId, childRows, liveChildren, configs);
|
|
1598
1688
|
if (projected.length > 0) out[String(parentId)] = [...projected];
|
|
1599
1689
|
}
|
|
1600
1690
|
return out;
|
|
@@ -2956,18 +3046,20 @@ async function listBindableCapsForDeviceType(deps, input) {
|
|
|
2956
3046
|
}
|
|
2957
3047
|
async function setWrapperActive(deps, input) {
|
|
2958
3048
|
const storeKey = String(input.deviceId);
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3049
|
+
await deps.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
3050
|
+
const store = await readBindingsStore(deps.bindingsDeps);
|
|
3051
|
+
const perDevice = { ...store.deviceBindings[storeKey] };
|
|
3052
|
+
if (input.active) perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
|
|
3053
|
+
else perDevice[input.capName] = { wrapperAddonId: null };
|
|
3054
|
+
const nextDeviceBindings = Object.keys(perDevice).length > 0 ? {
|
|
3055
|
+
...store.deviceBindings,
|
|
3056
|
+
[storeKey]: perDevice
|
|
3057
|
+
} : (() => {
|
|
3058
|
+
const { [storeKey]: _drop, ...rest } = store.deviceBindings;
|
|
3059
|
+
return rest;
|
|
3060
|
+
})();
|
|
3061
|
+
await writeBindingsStore(deps.bindingsDeps, { deviceBindings: nextDeviceBindings });
|
|
3062
|
+
});
|
|
2971
3063
|
deps.ctx.eventBus.emit({
|
|
2972
3064
|
id: (0, node_crypto.randomUUID)(),
|
|
2973
3065
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3334,12 +3426,13 @@ async function removeDevice(pctx, input) {
|
|
|
3334
3426
|
});
|
|
3335
3427
|
await pctx.settings.clearDeviceStore(deviceId);
|
|
3336
3428
|
await pctx.settings.clearDeviceRuntimeState(deviceId);
|
|
3337
|
-
const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
|
|
3338
3429
|
const bindingKey = String(deviceId);
|
|
3339
|
-
|
|
3430
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
3431
|
+
const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
|
|
3432
|
+
if (!bindingsStore.deviceBindings[bindingKey]) return;
|
|
3340
3433
|
const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
|
|
3341
3434
|
await writeBindingsStore(pctx.bindingsDeps, { deviceBindings: restBindings });
|
|
3342
|
-
}
|
|
3435
|
+
});
|
|
3343
3436
|
pctx.host.remoteNativeCaps.delete(deviceId);
|
|
3344
3437
|
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
|
|
3345
3438
|
pctx.metaStore.idToAddonId.delete(deviceId);
|
|
@@ -3801,16 +3894,24 @@ async function getRoleDisplayDefaults(pctx, _input) {
|
|
|
3801
3894
|
/**
|
|
3802
3895
|
* Replace the per-role display defaults whole-record (full replace). Override
|
|
3803
3896
|
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
3804
|
-
* looks up canonical spellings.
|
|
3805
|
-
*
|
|
3806
|
-
*
|
|
3897
|
+
* looks up canonical spellings. Not per-device, so no event is emitted; the UI
|
|
3898
|
+
* invalidates its own query on mutate.
|
|
3899
|
+
*
|
|
3900
|
+
* This key is not itself read-modify-written, but `writeAddonStore` IS — the
|
|
3901
|
+
* door loads this addon's whole key range, merges the patch and re-inserts the
|
|
3902
|
+
* range (`settings-door-view.ts`). An unordered write of ANY key on that range
|
|
3903
|
+
* can therefore carry a stale copy of every other key, so it takes the same
|
|
3904
|
+
* lock as `deviceBindings` and `locations`. The old comment here claimed "no
|
|
3905
|
+
* lock, no RMW", which described the top-level key and not the write.
|
|
3807
3906
|
*/
|
|
3808
3907
|
async function setRoleDisplayDefaults(pctx, input) {
|
|
3809
3908
|
const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
|
|
3810
3909
|
...def,
|
|
3811
3910
|
unit: require_dist.normalizeUnit(def.unit) ?? def.unit
|
|
3812
3911
|
} : def]));
|
|
3813
|
-
await pctx.
|
|
3912
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
3913
|
+
await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
|
|
3914
|
+
});
|
|
3814
3915
|
}
|
|
3815
3916
|
/**
|
|
3816
3917
|
* Batched meta pre-seed. Applies every provided field to the
|
|
@@ -4008,9 +4109,11 @@ async function listLocations(pctx) {
|
|
|
4008
4109
|
async function addLocation(pctx, input) {
|
|
4009
4110
|
const trimmed = input.name.trim();
|
|
4010
4111
|
if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4112
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
4113
|
+
const current = (await pctx.metaStore.readStore()).locations ?? [];
|
|
4114
|
+
if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
|
|
4115
|
+
await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
|
|
4116
|
+
});
|
|
4014
4117
|
}
|
|
4015
4118
|
/**
|
|
4016
4119
|
* Remove a label from the curated registry. Match is
|
|
@@ -4024,9 +4127,12 @@ async function addLocation(pctx, input) {
|
|
|
4024
4127
|
async function removeLocation(pctx, input) {
|
|
4025
4128
|
const trimmed = input.name.trim();
|
|
4026
4129
|
if (trimmed.length === 0) return;
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4130
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
4131
|
+
const current = (await pctx.metaStore.readStore()).locations ?? [];
|
|
4132
|
+
const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
|
|
4133
|
+
if (remaining.length === current.length) return;
|
|
4134
|
+
await pctx.settings.writeAddonStore({ locations: remaining });
|
|
4135
|
+
});
|
|
4030
4136
|
if (input.cascade !== true) return;
|
|
4031
4137
|
const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
4032
4138
|
const out = [];
|
|
@@ -4077,24 +4183,41 @@ function isRoleDisplayDefaults(value) {
|
|
|
4077
4183
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4078
4184
|
}
|
|
4079
4185
|
//#endregion
|
|
4186
|
+
//#region src/builtins/device-manager/write-lock.ts
|
|
4187
|
+
function createWriteLock() {
|
|
4188
|
+
let chain = Promise.resolve();
|
|
4189
|
+
return async (fn) => {
|
|
4190
|
+
const previous = chain;
|
|
4191
|
+
let release = () => {};
|
|
4192
|
+
chain = new Promise((resolve) => {
|
|
4193
|
+
release = resolve;
|
|
4194
|
+
});
|
|
4195
|
+
try {
|
|
4196
|
+
await previous.catch(() => {});
|
|
4197
|
+
return await fn();
|
|
4198
|
+
} finally {
|
|
4199
|
+
release();
|
|
4200
|
+
}
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
//#endregion
|
|
4080
4204
|
//#region src/builtins/device-manager/device-meta-store.ts
|
|
4081
4205
|
var DeviceMetaStore = class {
|
|
4082
4206
|
settings;
|
|
4083
4207
|
registry;
|
|
4084
4208
|
rows;
|
|
4209
|
+
withAddonStoreWriteLock;
|
|
4085
4210
|
/** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
|
|
4086
4211
|
* The persisted row store is authoritative but reads are async; hub-side
|
|
4087
4212
|
* callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
|
|
4088
4213
|
* ownership without awaiting. Kept in sync with every register/remove and
|
|
4089
4214
|
* warmed from persistence on boot. */
|
|
4090
4215
|
idToAddonId = /* @__PURE__ */ new Map();
|
|
4091
|
-
|
|
4092
|
-
* chain (see `withMetaWriteLock`). Per-instance state. */
|
|
4093
|
-
metaWriteChain = Promise.resolve();
|
|
4094
|
-
constructor(settings, registry, rows) {
|
|
4216
|
+
constructor(settings, registry, rows, withAddonStoreWriteLock = createWriteLock()) {
|
|
4095
4217
|
this.settings = settings;
|
|
4096
4218
|
this.registry = registry;
|
|
4097
4219
|
this.rows = rows;
|
|
4220
|
+
this.withAddonStoreWriteLock = withAddonStoreWriteLock;
|
|
4098
4221
|
}
|
|
4099
4222
|
/** The read currently in flight, or null. Never a settled value — see
|
|
4100
4223
|
* {@link readStore}. */
|
|
@@ -4125,20 +4248,7 @@ var DeviceMetaStore = class {
|
|
|
4125
4248
|
this.inFlightRead = read;
|
|
4126
4249
|
return read;
|
|
4127
4250
|
};
|
|
4128
|
-
withMetaWriteLock =
|
|
4129
|
-
const previous = this.metaWriteChain;
|
|
4130
|
-
let release = () => {};
|
|
4131
|
-
const next = new Promise((resolve) => {
|
|
4132
|
-
release = resolve;
|
|
4133
|
-
});
|
|
4134
|
-
this.metaWriteChain = next;
|
|
4135
|
-
try {
|
|
4136
|
-
await previous.catch(() => {});
|
|
4137
|
-
return await fn();
|
|
4138
|
-
} finally {
|
|
4139
|
-
release();
|
|
4140
|
-
}
|
|
4141
|
-
};
|
|
4251
|
+
withMetaWriteLock = createWriteLock();
|
|
4142
4252
|
/** The whole persisted row for one device, or `null`. */
|
|
4143
4253
|
getRow = async (deviceId) => this.rows.get(deviceId);
|
|
4144
4254
|
/**
|
|
@@ -4172,11 +4282,24 @@ var DeviceMetaStore = class {
|
|
|
4172
4282
|
ids.delete(parentId);
|
|
4173
4283
|
return [...ids];
|
|
4174
4284
|
};
|
|
4175
|
-
|
|
4285
|
+
/**
|
|
4286
|
+
* Mint the next numeric device id.
|
|
4287
|
+
*
|
|
4288
|
+
* A read-modify-write of `nextDeviceId`, and it must hold the addon-store
|
|
4289
|
+
* lock for the whole of it. Serialising the WRITES is not enough and never
|
|
4290
|
+
* was: two callers that both read `N` both write `N + 1` and both return
|
|
4291
|
+
* `N`, so two devices receive the same id. (`writeAddonStore` is itself
|
|
4292
|
+
* read-modify-write over the addon's whole key range, so an unlocked bump
|
|
4293
|
+
* can also be reverted wholesale by a concurrent `setWrapperActive`.)
|
|
4294
|
+
*
|
|
4295
|
+
* Callers hold {@link withMetaWriteLock} — this is the one nesting, and it
|
|
4296
|
+
* only ever goes meta ⊃ addon-store.
|
|
4297
|
+
*/
|
|
4298
|
+
allocateNextDeviceId = async () => this.withAddonStoreWriteLock(async () => {
|
|
4176
4299
|
const current = (await this.readStore()).nextDeviceId ?? 1;
|
|
4177
4300
|
await this.settings.writeAddonStore({ nextDeviceId: current + 1 });
|
|
4178
4301
|
return current;
|
|
4179
|
-
};
|
|
4302
|
+
});
|
|
4180
4303
|
};
|
|
4181
4304
|
//#endregion
|
|
4182
4305
|
//#region src/builtins/device-manager/fleet-read-census.ts
|
|
@@ -5559,6 +5682,17 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
5559
5682
|
* it through {@link bindingsDeps} or the `ProviderContext`.
|
|
5560
5683
|
*/
|
|
5561
5684
|
deviceRows = null;
|
|
5685
|
+
/**
|
|
5686
|
+
* The ONE lock ordering every read-modify-write of this addon's
|
|
5687
|
+
* `addon-settings` key range (`deviceBindings`, `locations`, `nextDeviceId`,
|
|
5688
|
+
* `roleDisplayDefaults`).
|
|
5689
|
+
*
|
|
5690
|
+
* It lives on the addon rather than on `DeviceMetaStore` because the writers
|
|
5691
|
+
* do not all go through that class — `setWrapperActive` and `removeDevice`'s
|
|
5692
|
+
* bindings purge reach the store through `BindingsDeps` — and two locks over
|
|
5693
|
+
* one key range is the same defect one level up. Handed to both.
|
|
5694
|
+
*/
|
|
5695
|
+
addonStoreWriteLock = createWriteLock();
|
|
5562
5696
|
/** Build the dependency context the extracted binding resolvers consume. */
|
|
5563
5697
|
get bindingsDeps() {
|
|
5564
5698
|
const rows = this.deviceRows;
|
|
@@ -5567,7 +5701,8 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
5567
5701
|
ctx: this.ctx,
|
|
5568
5702
|
capabilityRegistry: this.capabilityRegistry,
|
|
5569
5703
|
remoteNativeCaps: this.remoteNativeCaps,
|
|
5570
|
-
rows
|
|
5704
|
+
rows,
|
|
5705
|
+
withAddonStoreWriteLock: this.addonStoreWriteLock
|
|
5571
5706
|
};
|
|
5572
5707
|
}
|
|
5573
5708
|
async getBindings(input) {
|
|
@@ -5636,7 +5771,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
5636
5771
|
} catch (err) {
|
|
5637
5772
|
this.ctx.logger.warn("retired-row purge failed", { meta: { error: require_dist.errMsg(err) } });
|
|
5638
5773
|
}
|
|
5639
|
-
const metaStore = new DeviceMetaStore(settings, registry, deviceRows);
|
|
5774
|
+
const metaStore = new DeviceMetaStore(settings, registry, deviceRows, this.addonStoreWriteLock);
|
|
5640
5775
|
this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
|
|
5641
5776
|
const stateMirror = this.stateMirrorImpl;
|
|
5642
5777
|
const resolvePersistedById = metaStore.resolvePersistedById;
|