@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-
|
|
1
|
+
import { B as deviceManagerCapability, G as enumerateItemArrayFields, H as deviceStatusCapability, It as DeviceFeature, K as enumerateSchemaFields, Lt as DeviceRole, M as buildStreamParamsConfigSchema, Mt as BaseAddon, Rt as DeviceType, V as deviceStateCapability, Vt as WELL_KNOWN_TAB_MAP, Xt as isDeviceConfigCap, a as CAP_NAMES_WITH_STATUS, d as DeviceStatusSchema, ft as parseStreamParamsFormPatch, in as EventCategory, jt as errMsg, rn as sleep, t as ALL_CAPABILITY_DEFINITIONS, ut as normalizeUnit, vt as runtimeStatePolicyFor, y as STREAM_PROFILE_META } from "../../dist-DKs---Vx.mjs";
|
|
2
|
+
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-C3eONTvV.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
|
|
@@ -564,11 +564,31 @@ function resolveDeviceType(deps, row, deviceId) {
|
|
|
564
564
|
* same reason `getAllBindings` warns instead of reporting zero devices. That is
|
|
565
565
|
* the only reason this is async: the `COUNT(*)` runs solely on the miss path,
|
|
566
566
|
* where the alternative is to call a device gone because the store is empty.
|
|
567
|
+
*
|
|
568
|
+
* `ledgerNonEmpty` is passed IN rather than defaulted, deliberately. A default
|
|
569
|
+
* that issued its own `rows.count()` would be a per-device read hiding behind
|
|
570
|
+
* an optional argument — the exact shape this file just removed from the
|
|
571
|
+
* activations blob — and nothing would fail when a future caller took it.
|
|
567
572
|
*/
|
|
568
|
-
async function resolveDevicePresence(deps, row, deviceId) {
|
|
573
|
+
async function resolveDevicePresence(deps, row, deviceId, ledgerNonEmpty) {
|
|
569
574
|
if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
|
|
570
575
|
if (row !== null) return "present";
|
|
571
|
-
return await
|
|
576
|
+
return await ledgerNonEmpty() ? "absent" : "unknown";
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* `deps.rows.count()`, collapsed to at most one call per pass.
|
|
580
|
+
*
|
|
581
|
+
* The count answers a FLEET question ("has device restore run at all?"), not a
|
|
582
|
+
* per-device one, and it is reached only when a device misses both the live
|
|
583
|
+
* registry and the ledger. A fleet sweep over a store that is genuinely empty
|
|
584
|
+
* hits that path once per device.
|
|
585
|
+
*/
|
|
586
|
+
function onceLedgerNonEmpty(deps) {
|
|
587
|
+
let pending = null;
|
|
588
|
+
return () => {
|
|
589
|
+
if (pending === null) pending = deps.rows.count().then((n) => n > 0);
|
|
590
|
+
return pending;
|
|
591
|
+
};
|
|
572
592
|
}
|
|
573
593
|
/**
|
|
574
594
|
* Does a capability apply to a device of type `deviceType`?
|
|
@@ -589,8 +609,11 @@ function capAppliesToDeviceType(def, deviceType) {
|
|
|
589
609
|
return declared.some((t) => t === deviceType);
|
|
590
610
|
}
|
|
591
611
|
async function getBindings(deps, input) {
|
|
592
|
-
const row = await deps.rows.get(input.deviceId);
|
|
593
|
-
return resolveBindingsForDevice(deps, input.deviceId, row
|
|
612
|
+
const [row, store] = await Promise.all([deps.rows.get(input.deviceId), readBindingsStore(deps)]);
|
|
613
|
+
return resolveBindingsForDevice(deps, input.deviceId, row, {
|
|
614
|
+
store,
|
|
615
|
+
ledgerNonEmpty: onceLedgerNonEmpty(deps)
|
|
616
|
+
});
|
|
594
617
|
}
|
|
595
618
|
/**
|
|
596
619
|
* The `getBindings` resolver, taking the device's row as a PARAMETER instead
|
|
@@ -605,12 +628,22 @@ async function getBindings(deps, input) {
|
|
|
605
628
|
* iteration shape measured on the live hub 2026-08-25: `get
|
|
606
629
|
* device-manager:devices` at 1 656 calls/min steady-state and 17 687 during
|
|
607
630
|
* boot, one row per call.
|
|
631
|
+
*
|
|
632
|
+
* The {@link BindingsPass} is the SECOND half of that same split, and it is
|
|
633
|
+
* the one that was missing. This function used to issue its own
|
|
634
|
+
* `readBindingsStore(deps)` — one settings-door round trip per device, for a
|
|
635
|
+
* blob that is identical for every device in the pass. `getAllBindings` on the
|
|
636
|
+
* live 1 019-device hub therefore cost 1 020 round trips; it never answered
|
|
637
|
+
* inside `SystemMirror.init`'s 15 s budget, and the admin UI refused to boot.
|
|
638
|
+
* Now every entry point resolves the pass once and hands it down — so this
|
|
639
|
+
* function performs NO store reads of its own, and every device in a sweep
|
|
640
|
+
* sees the same version of the activations.
|
|
608
641
|
*/
|
|
609
|
-
async function resolveBindingsForDevice(deps, deviceId, row) {
|
|
642
|
+
async function resolveBindingsForDevice(deps, deviceId, row, pass) {
|
|
610
643
|
const storeKey = String(deviceId);
|
|
611
|
-
const perDevice =
|
|
644
|
+
const perDevice = pass.store.deviceBindings[storeKey] ?? {};
|
|
612
645
|
const deviceType = resolveDeviceType(deps, row, deviceId);
|
|
613
|
-
const presence = await resolveDevicePresence(deps, row, deviceId);
|
|
646
|
+
const presence = await resolveDevicePresence(deps, row, deviceId, pass.ledgerNonEmpty);
|
|
614
647
|
const entries = [];
|
|
615
648
|
const seenCaps = /* @__PURE__ */ new Set();
|
|
616
649
|
const resolveRemote = (capName) => deps.remoteNativeCaps.get(deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, deviceId);
|
|
@@ -718,21 +751,33 @@ async function resolveBindingsForDevice(deps, deviceId, row) {
|
|
|
718
751
|
* `getAllBindings` (511 ms / 679 KB of mostly-unwanted rows).
|
|
719
752
|
*
|
|
720
753
|
* Sequential on purpose, like `getAllBindings`: `resolveBindingsForDevice`'s
|
|
721
|
-
* remaining per-device work
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
754
|
+
* remaining per-device work is the routing rules, which run on this process's
|
|
755
|
+
* own event loop, so concurrency buys nothing there and a fan-out of
|
|
756
|
+
* `Promise.all` over hundreds of ids only makes the queue less fair to
|
|
757
|
+
* everything else on it. Both parts that DO amortise across the whole set are
|
|
758
|
+
* fetched ONCE, ahead of the loop, and in parallel with each other:
|
|
759
|
+
*
|
|
760
|
+
* - the device rows ({@link DeviceRowStore.getMany}) rather than once per
|
|
761
|
+
* iteration — measured on the live hub 2026-08-25, `get
|
|
762
|
+
* device-manager:devices` ran 1 656 calls/min steady-state, one row per
|
|
763
|
+
* call, which is exactly what looping `getBindings`'s own
|
|
764
|
+
* `rows.get(deviceId)` here produced;
|
|
765
|
+
* - the wrapper-activation blob, which the per-device resolver used to read
|
|
766
|
+
* itself — a second settings-door round trip per id, on the call that
|
|
767
|
+
* exists precisely so a caller needing many devices does not pay per
|
|
768
|
+
* device. Measured on the live hub 2026-08-30, mid-drain:
|
|
769
|
+
* `getBindingsBatch` of 1 id = 5.78 s, of 8 ids = 17.16 s — a slope of
|
|
770
|
+
* 1.63 s PER ADDITIONAL DEVICE, on a call that ships 4.5 KB a row.
|
|
730
771
|
*/
|
|
731
772
|
async function getBindingsBatch(deps, input) {
|
|
732
773
|
const ids = [...new Set(input.deviceIds)];
|
|
733
|
-
const rows = await deps.rows.getMany(ids);
|
|
774
|
+
const [rows, store] = await Promise.all([deps.rows.getMany(ids), readBindingsStore(deps)]);
|
|
775
|
+
const pass = {
|
|
776
|
+
store,
|
|
777
|
+
ledgerNonEmpty: onceLedgerNonEmpty(deps)
|
|
778
|
+
};
|
|
734
779
|
const out = [];
|
|
735
|
-
for (const deviceId of ids) out.push(await resolveBindingsForDevice(deps, deviceId, rows.get(deviceId) ?? null));
|
|
780
|
+
for (const deviceId of ids) out.push(await resolveBindingsForDevice(deps, deviceId, rows.get(deviceId) ?? null, pass));
|
|
736
781
|
return out;
|
|
737
782
|
}
|
|
738
783
|
/**
|
|
@@ -745,9 +790,18 @@ async function getBindingsBatch(deps, input) {
|
|
|
745
790
|
* clients invalidate via the existing
|
|
746
791
|
* `capability.binding-changed` event rather than re-fetching this
|
|
747
792
|
* payload periodically.
|
|
793
|
+
*
|
|
794
|
+
* **"Single round-trip" was a claim, not a fact, until 2026-08-30.** The
|
|
795
|
+
* per-device resolver read the wrapper-activation blob itself, so this call
|
|
796
|
+
* issued one settings-door RPC per device — 1 020 of them on the live
|
|
797
|
+
* 1 019-device hub, at ~2.9 s each while the storage drain ran. It did not
|
|
798
|
+
* return in 240 s, and `SystemMirror.init` gives it 15 s, so the admin UI
|
|
799
|
+
* could not boot at all. Now the whole pass costs exactly TWO reads (the fleet
|
|
800
|
+
* listing and the blob), issued in parallel, and nothing in the loop awaits a
|
|
801
|
+
* store.
|
|
748
802
|
*/
|
|
749
803
|
async function getAllBindings(deps) {
|
|
750
|
-
const fleet = await deps.rows.listAll();
|
|
804
|
+
const [fleet, store] = await Promise.all([deps.rows.listAll(), readBindingsStore(deps)]);
|
|
751
805
|
const rowById = /* @__PURE__ */ new Map();
|
|
752
806
|
const ids = /* @__PURE__ */ new Set();
|
|
753
807
|
for (const row of fleet) {
|
|
@@ -760,8 +814,12 @@ async function getAllBindings(deps) {
|
|
|
760
814
|
deps.ctx.logger.warn("getAllBindings found no devices — warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
|
|
761
815
|
return [];
|
|
762
816
|
}
|
|
817
|
+
const pass = {
|
|
818
|
+
store,
|
|
819
|
+
ledgerNonEmpty: onceLedgerNonEmpty(deps)
|
|
820
|
+
};
|
|
763
821
|
const out = [];
|
|
764
|
-
for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await resolveBindingsForDevice(deps, deviceId, rowById.get(deviceId) ?? null));
|
|
822
|
+
for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await resolveBindingsForDevice(deps, deviceId, rowById.get(deviceId) ?? null, pass));
|
|
765
823
|
return out;
|
|
766
824
|
}
|
|
767
825
|
/**
|
|
@@ -1461,15 +1519,39 @@ async function getDevice(pctx, input) {
|
|
|
1461
1519
|
};
|
|
1462
1520
|
}
|
|
1463
1521
|
/**
|
|
1464
|
-
* The
|
|
1465
|
-
*
|
|
1522
|
+
* The child ids whose config comes from the SETTINGS STORE — the rows the live
|
|
1523
|
+
* registry does not answer for.
|
|
1524
|
+
*
|
|
1525
|
+
* The one place the "persisted fills in what the registry does not hold" rule
|
|
1526
|
+
* is spelled, so the pre-read and the projection cannot disagree about which
|
|
1527
|
+
* children need a store value. On the live hub 2026-08-30 this is every child
|
|
1528
|
+
* there is: all 896 belong to forked provider addons, so hub-main's registry
|
|
1529
|
+
* holds none of them.
|
|
1530
|
+
*/
|
|
1531
|
+
function persistedChildIds(childRows, liveChildren) {
|
|
1532
|
+
const live = /* @__PURE__ */ new Set();
|
|
1533
|
+
for (const device of liveChildren) live.add(device.id);
|
|
1534
|
+
return childRows.filter((row) => !live.has(row.meta.id)).map((row) => row.meta.id);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* The children of ONE parent, projected from rows, live devices AND configs the
|
|
1538
|
+
* caller has ALREADY read.
|
|
1466
1539
|
*
|
|
1467
1540
|
* Extracted so `getChildren` and `getChildrenBatch` share one projection: the
|
|
1468
1541
|
* batch differs only in HOW the rows were fetched (one `listByParentMany`
|
|
1469
1542
|
* instead of N `listByParent`), never in what a parent's answer looks like.
|
|
1470
|
-
*
|
|
1543
|
+
*
|
|
1544
|
+
* **Not async, and that is the contract.** This function used to `await
|
|
1545
|
+
* pctx.settings.readDeviceStore(m.id)` inside its own loop — one settings-door
|
|
1546
|
+
* round trip per persisted child, sequentially, on hub-main's event loop. The
|
|
1547
|
+
* caller above (`getChildrenBatch`, coalescing every accessory reconcile in
|
|
1548
|
+
* flight) had already removed the ledger N+1 and left this one: 896 door reads
|
|
1549
|
+
* per boot reconcile and per device page, at ~2.9 s each mid-drain. `configs`
|
|
1550
|
+
* arrives resolved through `readDeviceConfigs` — one round trip for the whole
|
|
1551
|
+
* pass — and a signature with no `Promise` in it means a future read cannot be
|
|
1552
|
+
* put back here without the type saying so.
|
|
1471
1553
|
*/
|
|
1472
|
-
|
|
1554
|
+
function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren, configs) {
|
|
1473
1555
|
const results = [];
|
|
1474
1556
|
const seen = /* @__PURE__ */ new Set();
|
|
1475
1557
|
const rowById = /* @__PURE__ */ new Map();
|
|
@@ -1485,9 +1567,9 @@ async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, li
|
|
|
1485
1567
|
const childStableId = m.stableId;
|
|
1486
1568
|
const key = String(m.id);
|
|
1487
1569
|
if (seen.has(key)) continue;
|
|
1488
|
-
const persistedConfig =
|
|
1570
|
+
const persistedConfig = configs.get(m.id) ?? {};
|
|
1489
1571
|
const metadata = row.metadata;
|
|
1490
|
-
const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig
|
|
1572
|
+
const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig, childStableId, ownerAddonId);
|
|
1491
1573
|
results.push({
|
|
1492
1574
|
id: m.id,
|
|
1493
1575
|
stableId: childStableId,
|
|
@@ -1502,7 +1584,7 @@ async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, li
|
|
|
1502
1584
|
probed: pctx.host.resolveDeviceProbed(m.id),
|
|
1503
1585
|
features: persistedFeatures(m.features),
|
|
1504
1586
|
isCamera: false,
|
|
1505
|
-
config: persistedConfig
|
|
1587
|
+
config: persistedConfig,
|
|
1506
1588
|
metadata,
|
|
1507
1589
|
...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
1508
1590
|
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
@@ -1536,18 +1618,23 @@ async function getChildren(pctx, input) {
|
|
|
1536
1618
|
}
|
|
1537
1619
|
const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
|
|
1538
1620
|
const liveChildren = pctx.registry?.getChildren(parentDeviceId) ?? [];
|
|
1539
|
-
|
|
1621
|
+
const configs = await readDeviceConfigs(pctx, persistedChildIds(childRows, liveChildren));
|
|
1622
|
+
return [...projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren, configs)];
|
|
1540
1623
|
}
|
|
1541
1624
|
/**
|
|
1542
|
-
* `getChildren` for a set of parents —
|
|
1543
|
-
*
|
|
1625
|
+
* `getChildren` for a set of parents — THREE store reads for the whole set, not
|
|
1626
|
+
* three per parent (and not one per child).
|
|
1544
1627
|
*
|
|
1545
1628
|
* The single-parent path costs one `resolvePersistedById` (a primary-key read)
|
|
1546
1629
|
* plus one `listByParent` (an indexed scan) EACH. Asked once per device at
|
|
1547
1630
|
* boot that was 1 024 of each, measured on the live hub 2026-08-27 — see the
|
|
1548
1631
|
* cap method's note. Here the ownership lookup for every parent the live
|
|
1549
1632
|
* registry does not hold is ONE `getMany`, and the children of every parent
|
|
1550
|
-
* are ONE `listByParentMany`.
|
|
1633
|
+
* are ONE `listByParentMany`. The third read is the settings door, and it is
|
|
1634
|
+
* one for the whole batch: `readDeviceConfigs` over every persisted child of
|
|
1635
|
+
* every parent. That one was the leftover — the ledger N+1 was removed and a
|
|
1636
|
+
* door round trip PER CHILD stayed behind it (896 on the live hub 2026-08-30,
|
|
1637
|
+
* all of them forked-owned and therefore all persisted).
|
|
1551
1638
|
*
|
|
1552
1639
|
* The live half is indexed once too: `registry.getChildren` is
|
|
1553
1640
|
* `getAll().filter(…)`, so calling it per parent would trade an RPC N+1 for an
|
|
@@ -1585,11 +1672,14 @@ async function getChildrenBatch(pctx, input) {
|
|
|
1585
1672
|
if (bucket === void 0) liveByParent.set(parentId, [device]);
|
|
1586
1673
|
else bucket.push(device);
|
|
1587
1674
|
}
|
|
1675
|
+
const configIds = [];
|
|
1676
|
+
for (const parentId of owners.keys()) configIds.push(...persistedChildIds(rowsByParent.get(parentId) ?? [], liveByParent.get(parentId) ?? []));
|
|
1677
|
+
const configs = await readDeviceConfigs(pctx, configIds);
|
|
1588
1678
|
for (const [parentId, ownerAddonId] of owners) {
|
|
1589
1679
|
const childRows = rowsByParent.get(parentId) ?? [];
|
|
1590
1680
|
const liveChildren = liveByParent.get(parentId) ?? [];
|
|
1591
1681
|
if (childRows.length === 0 && liveChildren.length === 0) continue;
|
|
1592
|
-
const projected =
|
|
1682
|
+
const projected = projectChildren(pctx, parentId, ownerAddonId, childRows, liveChildren, configs);
|
|
1593
1683
|
if (projected.length > 0) out[String(parentId)] = [...projected];
|
|
1594
1684
|
}
|
|
1595
1685
|
return out;
|
|
@@ -2951,18 +3041,20 @@ async function listBindableCapsForDeviceType(deps, input) {
|
|
|
2951
3041
|
}
|
|
2952
3042
|
async function setWrapperActive(deps, input) {
|
|
2953
3043
|
const storeKey = String(input.deviceId);
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3044
|
+
await deps.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
3045
|
+
const store = await readBindingsStore(deps.bindingsDeps);
|
|
3046
|
+
const perDevice = { ...store.deviceBindings[storeKey] };
|
|
3047
|
+
if (input.active) perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
|
|
3048
|
+
else perDevice[input.capName] = { wrapperAddonId: null };
|
|
3049
|
+
const nextDeviceBindings = Object.keys(perDevice).length > 0 ? {
|
|
3050
|
+
...store.deviceBindings,
|
|
3051
|
+
[storeKey]: perDevice
|
|
3052
|
+
} : (() => {
|
|
3053
|
+
const { [storeKey]: _drop, ...rest } = store.deviceBindings;
|
|
3054
|
+
return rest;
|
|
3055
|
+
})();
|
|
3056
|
+
await writeBindingsStore(deps.bindingsDeps, { deviceBindings: nextDeviceBindings });
|
|
3057
|
+
});
|
|
2966
3058
|
deps.ctx.eventBus.emit({
|
|
2967
3059
|
id: randomUUID(),
|
|
2968
3060
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3329,12 +3421,13 @@ async function removeDevice(pctx, input) {
|
|
|
3329
3421
|
});
|
|
3330
3422
|
await pctx.settings.clearDeviceStore(deviceId);
|
|
3331
3423
|
await pctx.settings.clearDeviceRuntimeState(deviceId);
|
|
3332
|
-
const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
|
|
3333
3424
|
const bindingKey = String(deviceId);
|
|
3334
|
-
|
|
3425
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
3426
|
+
const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
|
|
3427
|
+
if (!bindingsStore.deviceBindings[bindingKey]) return;
|
|
3335
3428
|
const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
|
|
3336
3429
|
await writeBindingsStore(pctx.bindingsDeps, { deviceBindings: restBindings });
|
|
3337
|
-
}
|
|
3430
|
+
});
|
|
3338
3431
|
pctx.host.remoteNativeCaps.delete(deviceId);
|
|
3339
3432
|
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
|
|
3340
3433
|
pctx.metaStore.idToAddonId.delete(deviceId);
|
|
@@ -3796,16 +3889,24 @@ async function getRoleDisplayDefaults(pctx, _input) {
|
|
|
3796
3889
|
/**
|
|
3797
3890
|
* Replace the per-role display defaults whole-record (full replace). Override
|
|
3798
3891
|
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
3799
|
-
* looks up canonical spellings.
|
|
3800
|
-
*
|
|
3801
|
-
*
|
|
3892
|
+
* looks up canonical spellings. Not per-device, so no event is emitted; the UI
|
|
3893
|
+
* invalidates its own query on mutate.
|
|
3894
|
+
*
|
|
3895
|
+
* This key is not itself read-modify-written, but `writeAddonStore` IS — the
|
|
3896
|
+
* door loads this addon's whole key range, merges the patch and re-inserts the
|
|
3897
|
+
* range (`settings-door-view.ts`). An unordered write of ANY key on that range
|
|
3898
|
+
* can therefore carry a stale copy of every other key, so it takes the same
|
|
3899
|
+
* lock as `deviceBindings` and `locations`. The old comment here claimed "no
|
|
3900
|
+
* lock, no RMW", which described the top-level key and not the write.
|
|
3802
3901
|
*/
|
|
3803
3902
|
async function setRoleDisplayDefaults(pctx, input) {
|
|
3804
3903
|
const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
|
|
3805
3904
|
...def,
|
|
3806
3905
|
unit: normalizeUnit(def.unit) ?? def.unit
|
|
3807
3906
|
} : def]));
|
|
3808
|
-
await pctx.
|
|
3907
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
3908
|
+
await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
|
|
3909
|
+
});
|
|
3809
3910
|
}
|
|
3810
3911
|
/**
|
|
3811
3912
|
* Batched meta pre-seed. Applies every provided field to the
|
|
@@ -4003,9 +4104,11 @@ async function listLocations(pctx) {
|
|
|
4003
4104
|
async function addLocation(pctx, input) {
|
|
4004
4105
|
const trimmed = input.name.trim();
|
|
4005
4106
|
if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4107
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
4108
|
+
const current = (await pctx.metaStore.readStore()).locations ?? [];
|
|
4109
|
+
if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
|
|
4110
|
+
await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
|
|
4111
|
+
});
|
|
4009
4112
|
}
|
|
4010
4113
|
/**
|
|
4011
4114
|
* Remove a label from the curated registry. Match is
|
|
@@ -4019,9 +4122,12 @@ async function addLocation(pctx, input) {
|
|
|
4019
4122
|
async function removeLocation(pctx, input) {
|
|
4020
4123
|
const trimmed = input.name.trim();
|
|
4021
4124
|
if (trimmed.length === 0) return;
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4125
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
4126
|
+
const current = (await pctx.metaStore.readStore()).locations ?? [];
|
|
4127
|
+
const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
|
|
4128
|
+
if (remaining.length === current.length) return;
|
|
4129
|
+
await pctx.settings.writeAddonStore({ locations: remaining });
|
|
4130
|
+
});
|
|
4025
4131
|
if (input.cascade !== true) return;
|
|
4026
4132
|
const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
4027
4133
|
const out = [];
|
|
@@ -4072,24 +4178,41 @@ function isRoleDisplayDefaults(value) {
|
|
|
4072
4178
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4073
4179
|
}
|
|
4074
4180
|
//#endregion
|
|
4181
|
+
//#region src/builtins/device-manager/write-lock.ts
|
|
4182
|
+
function createWriteLock() {
|
|
4183
|
+
let chain = Promise.resolve();
|
|
4184
|
+
return async (fn) => {
|
|
4185
|
+
const previous = chain;
|
|
4186
|
+
let release = () => {};
|
|
4187
|
+
chain = new Promise((resolve) => {
|
|
4188
|
+
release = resolve;
|
|
4189
|
+
});
|
|
4190
|
+
try {
|
|
4191
|
+
await previous.catch(() => {});
|
|
4192
|
+
return await fn();
|
|
4193
|
+
} finally {
|
|
4194
|
+
release();
|
|
4195
|
+
}
|
|
4196
|
+
};
|
|
4197
|
+
}
|
|
4198
|
+
//#endregion
|
|
4075
4199
|
//#region src/builtins/device-manager/device-meta-store.ts
|
|
4076
4200
|
var DeviceMetaStore = class {
|
|
4077
4201
|
settings;
|
|
4078
4202
|
registry;
|
|
4079
4203
|
rows;
|
|
4204
|
+
withAddonStoreWriteLock;
|
|
4080
4205
|
/** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
|
|
4081
4206
|
* The persisted row store is authoritative but reads are async; hub-side
|
|
4082
4207
|
* callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
|
|
4083
4208
|
* ownership without awaiting. Kept in sync with every register/remove and
|
|
4084
4209
|
* warmed from persistence on boot. */
|
|
4085
4210
|
idToAddonId = /* @__PURE__ */ new Map();
|
|
4086
|
-
|
|
4087
|
-
* chain (see `withMetaWriteLock`). Per-instance state. */
|
|
4088
|
-
metaWriteChain = Promise.resolve();
|
|
4089
|
-
constructor(settings, registry, rows) {
|
|
4211
|
+
constructor(settings, registry, rows, withAddonStoreWriteLock = createWriteLock()) {
|
|
4090
4212
|
this.settings = settings;
|
|
4091
4213
|
this.registry = registry;
|
|
4092
4214
|
this.rows = rows;
|
|
4215
|
+
this.withAddonStoreWriteLock = withAddonStoreWriteLock;
|
|
4093
4216
|
}
|
|
4094
4217
|
/** The read currently in flight, or null. Never a settled value — see
|
|
4095
4218
|
* {@link readStore}. */
|
|
@@ -4120,20 +4243,7 @@ var DeviceMetaStore = class {
|
|
|
4120
4243
|
this.inFlightRead = read;
|
|
4121
4244
|
return read;
|
|
4122
4245
|
};
|
|
4123
|
-
withMetaWriteLock =
|
|
4124
|
-
const previous = this.metaWriteChain;
|
|
4125
|
-
let release = () => {};
|
|
4126
|
-
const next = new Promise((resolve) => {
|
|
4127
|
-
release = resolve;
|
|
4128
|
-
});
|
|
4129
|
-
this.metaWriteChain = next;
|
|
4130
|
-
try {
|
|
4131
|
-
await previous.catch(() => {});
|
|
4132
|
-
return await fn();
|
|
4133
|
-
} finally {
|
|
4134
|
-
release();
|
|
4135
|
-
}
|
|
4136
|
-
};
|
|
4246
|
+
withMetaWriteLock = createWriteLock();
|
|
4137
4247
|
/** The whole persisted row for one device, or `null`. */
|
|
4138
4248
|
getRow = async (deviceId) => this.rows.get(deviceId);
|
|
4139
4249
|
/**
|
|
@@ -4167,11 +4277,24 @@ var DeviceMetaStore = class {
|
|
|
4167
4277
|
ids.delete(parentId);
|
|
4168
4278
|
return [...ids];
|
|
4169
4279
|
};
|
|
4170
|
-
|
|
4280
|
+
/**
|
|
4281
|
+
* Mint the next numeric device id.
|
|
4282
|
+
*
|
|
4283
|
+
* A read-modify-write of `nextDeviceId`, and it must hold the addon-store
|
|
4284
|
+
* lock for the whole of it. Serialising the WRITES is not enough and never
|
|
4285
|
+
* was: two callers that both read `N` both write `N + 1` and both return
|
|
4286
|
+
* `N`, so two devices receive the same id. (`writeAddonStore` is itself
|
|
4287
|
+
* read-modify-write over the addon's whole key range, so an unlocked bump
|
|
4288
|
+
* can also be reverted wholesale by a concurrent `setWrapperActive`.)
|
|
4289
|
+
*
|
|
4290
|
+
* Callers hold {@link withMetaWriteLock} — this is the one nesting, and it
|
|
4291
|
+
* only ever goes meta ⊃ addon-store.
|
|
4292
|
+
*/
|
|
4293
|
+
allocateNextDeviceId = async () => this.withAddonStoreWriteLock(async () => {
|
|
4171
4294
|
const current = (await this.readStore()).nextDeviceId ?? 1;
|
|
4172
4295
|
await this.settings.writeAddonStore({ nextDeviceId: current + 1 });
|
|
4173
4296
|
return current;
|
|
4174
|
-
};
|
|
4297
|
+
});
|
|
4175
4298
|
};
|
|
4176
4299
|
//#endregion
|
|
4177
4300
|
//#region src/builtins/device-manager/fleet-read-census.ts
|
|
@@ -5554,6 +5677,17 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
5554
5677
|
* it through {@link bindingsDeps} or the `ProviderContext`.
|
|
5555
5678
|
*/
|
|
5556
5679
|
deviceRows = null;
|
|
5680
|
+
/**
|
|
5681
|
+
* The ONE lock ordering every read-modify-write of this addon's
|
|
5682
|
+
* `addon-settings` key range (`deviceBindings`, `locations`, `nextDeviceId`,
|
|
5683
|
+
* `roleDisplayDefaults`).
|
|
5684
|
+
*
|
|
5685
|
+
* It lives on the addon rather than on `DeviceMetaStore` because the writers
|
|
5686
|
+
* do not all go through that class — `setWrapperActive` and `removeDevice`'s
|
|
5687
|
+
* bindings purge reach the store through `BindingsDeps` — and two locks over
|
|
5688
|
+
* one key range is the same defect one level up. Handed to both.
|
|
5689
|
+
*/
|
|
5690
|
+
addonStoreWriteLock = createWriteLock();
|
|
5557
5691
|
/** Build the dependency context the extracted binding resolvers consume. */
|
|
5558
5692
|
get bindingsDeps() {
|
|
5559
5693
|
const rows = this.deviceRows;
|
|
@@ -5562,7 +5696,8 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
5562
5696
|
ctx: this.ctx,
|
|
5563
5697
|
capabilityRegistry: this.capabilityRegistry,
|
|
5564
5698
|
remoteNativeCaps: this.remoteNativeCaps,
|
|
5565
|
-
rows
|
|
5699
|
+
rows,
|
|
5700
|
+
withAddonStoreWriteLock: this.addonStoreWriteLock
|
|
5566
5701
|
};
|
|
5567
5702
|
}
|
|
5568
5703
|
async getBindings(input) {
|
|
@@ -5631,7 +5766,7 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
5631
5766
|
} catch (err) {
|
|
5632
5767
|
this.ctx.logger.warn("retired-row purge failed", { meta: { error: errMsg(err) } });
|
|
5633
5768
|
}
|
|
5634
|
-
const metaStore = new DeviceMetaStore(settings, registry, deviceRows);
|
|
5769
|
+
const metaStore = new DeviceMetaStore(settings, registry, deviceRows, this.addonStoreWriteLock);
|
|
5635
5770
|
this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
|
|
5636
5771
|
const stateMirror = this.stateMirrorImpl;
|
|
5637
5772
|
const resolvePersistedById = metaStore.resolvePersistedById;
|
|
@@ -132,9 +132,15 @@ export declare function getRoleDisplayDefaults(pctx: ProviderContext, _input: Pa
|
|
|
132
132
|
/**
|
|
133
133
|
* Replace the per-role display defaults whole-record (full replace). Override
|
|
134
134
|
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
135
|
-
* looks up canonical spellings.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
135
|
+
* looks up canonical spellings. Not per-device, so no event is emitted; the UI
|
|
136
|
+
* invalidates its own query on mutate.
|
|
137
|
+
*
|
|
138
|
+
* This key is not itself read-modify-written, but `writeAddonStore` IS — the
|
|
139
|
+
* door loads this addon's whole key range, merges the patch and re-inserts the
|
|
140
|
+
* range (`settings-door-view.ts`). An unordered write of ANY key on that range
|
|
141
|
+
* can therefore carry a stale copy of every other key, so it takes the same
|
|
142
|
+
* lock as `deviceBindings` and `locations`. The old comment here claimed "no
|
|
143
|
+
* lock, no RMW", which described the top-level key and not the write.
|
|
138
144
|
*/
|
|
139
145
|
export declare function setRoleDisplayDefaults(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setRoleDisplayDefaults']>[0]): ReturnType<IDeviceManagerProvider['setRoleDisplayDefaults']>;
|
|
140
146
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AddonContext, IDeviceRegistry } from '@camstack/types';
|
|
2
2
|
import { AddonStore, PersistedDeviceMeta } from './device-meta-types.js';
|
|
3
3
|
import { DeviceRow, DeviceRowStore } from './device-row-store.js';
|
|
4
|
+
import { WriteLock } from './write-lock.js';
|
|
4
5
|
/** The settings surface the meta store uses (`ctx.settings`, narrowed non-null). */
|
|
5
6
|
export type DeviceManagerSettings = NonNullable<AddonContext['settings']>;
|
|
6
7
|
export declare class DeviceMetaStore {
|
|
@@ -9,20 +10,33 @@ export declare class DeviceMetaStore {
|
|
|
9
10
|
readonly registry: IDeviceRegistry | null;
|
|
10
11
|
/** Row access for `device-manager:devices`. */
|
|
11
12
|
readonly rows: DeviceRowStore;
|
|
13
|
+
/**
|
|
14
|
+
* The addon's OWN settings row set lock, shared with every other writer of
|
|
15
|
+
* it (`setWrapperActive`, `removeDevice`'s bindings purge, `addLocation`,
|
|
16
|
+
* `removeLocation`, `setRoleDisplayDefaults`) — see
|
|
17
|
+
* {@link BindingsDeps.addonStoreLock}. Owned by the addon, not by this
|
|
18
|
+
* class, because those writers do not all go through it.
|
|
19
|
+
*/
|
|
20
|
+
readonly withAddonStoreWriteLock: WriteLock;
|
|
12
21
|
/** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
|
|
13
22
|
* The persisted row store is authoritative but reads are async; hub-side
|
|
14
23
|
* callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
|
|
15
24
|
* ownership without awaiting. Kept in sync with every register/remove and
|
|
16
25
|
* warmed from persistence on boot. */
|
|
17
26
|
readonly idToAddonId: Map<number, string>;
|
|
18
|
-
/** Serialises every read-modify-write of a device row through one promise
|
|
19
|
-
* chain (see `withMetaWriteLock`). Per-instance state. */
|
|
20
|
-
private metaWriteChain;
|
|
21
27
|
constructor(settings: DeviceManagerSettings,
|
|
22
28
|
/** Hub-process device registry (null when this node owns no registry). */
|
|
23
29
|
registry: IDeviceRegistry | null,
|
|
24
30
|
/** Row access for `device-manager:devices`. */
|
|
25
|
-
rows: DeviceRowStore
|
|
31
|
+
rows: DeviceRowStore,
|
|
32
|
+
/**
|
|
33
|
+
* The addon's OWN settings row set lock, shared with every other writer of
|
|
34
|
+
* it (`setWrapperActive`, `removeDevice`'s bindings purge, `addLocation`,
|
|
35
|
+
* `removeLocation`, `setRoleDisplayDefaults`) — see
|
|
36
|
+
* {@link BindingsDeps.addonStoreLock}. Owned by the addon, not by this
|
|
37
|
+
* class, because those writers do not all go through it.
|
|
38
|
+
*/
|
|
39
|
+
withAddonStoreWriteLock?: WriteLock);
|
|
26
40
|
/** The read currently in flight, or null. Never a settled value — see
|
|
27
41
|
* {@link readStore}. */
|
|
28
42
|
private inFlightRead;
|
|
@@ -40,7 +54,7 @@ export declare class DeviceMetaStore {
|
|
|
40
54
|
* and the next caller reaches the store again.
|
|
41
55
|
*/
|
|
42
56
|
readStore: () => Promise<AddonStore>;
|
|
43
|
-
withMetaWriteLock:
|
|
57
|
+
withMetaWriteLock: WriteLock;
|
|
44
58
|
/** The whole persisted row for one device, or `null`. */
|
|
45
59
|
getRow: (deviceId: number) => Promise<DeviceRow | null>;
|
|
46
60
|
/**
|
|
@@ -62,5 +76,18 @@ export declare class DeviceMetaStore {
|
|
|
62
76
|
* and excluding self. Shared by the `remove` cascade and the `resetToSource`
|
|
63
77
|
* resync purge (#19). */
|
|
64
78
|
directChildIds: (parentId: number) => Promise<readonly number[]>;
|
|
79
|
+
/**
|
|
80
|
+
* Mint the next numeric device id.
|
|
81
|
+
*
|
|
82
|
+
* A read-modify-write of `nextDeviceId`, and it must hold the addon-store
|
|
83
|
+
* lock for the whole of it. Serialising the WRITES is not enough and never
|
|
84
|
+
* was: two callers that both read `N` both write `N + 1` and both return
|
|
85
|
+
* `N`, so two devices receive the same id. (`writeAddonStore` is itself
|
|
86
|
+
* read-modify-write over the addon's whole key range, so an unlocked bump
|
|
87
|
+
* can also be reverted wholesale by a concurrent `setWrapperActive`.)
|
|
88
|
+
*
|
|
89
|
+
* Callers hold {@link withMetaWriteLock} — this is the one nesting, and it
|
|
90
|
+
* only ever goes meta ⊃ addon-store.
|
|
91
|
+
*/
|
|
65
92
|
allocateNextDeviceId: () => Promise<number>;
|
|
66
93
|
}
|
|
@@ -6,15 +6,19 @@ export declare function listAll(pctx: ProviderContext, input: Parameters<IDevice
|
|
|
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
8
|
/**
|
|
9
|
-
* `getChildren` for a set of parents —
|
|
10
|
-
*
|
|
9
|
+
* `getChildren` for a set of parents — THREE store reads for the whole set, not
|
|
10
|
+
* three per parent (and not one per child).
|
|
11
11
|
*
|
|
12
12
|
* The single-parent path costs one `resolvePersistedById` (a primary-key read)
|
|
13
13
|
* plus one `listByParent` (an indexed scan) EACH. Asked once per device at
|
|
14
14
|
* boot that was 1 024 of each, measured on the live hub 2026-08-27 — see the
|
|
15
15
|
* cap method's note. Here the ownership lookup for every parent the live
|
|
16
16
|
* registry does not hold is ONE `getMany`, and the children of every parent
|
|
17
|
-
* are ONE `listByParentMany`.
|
|
17
|
+
* are ONE `listByParentMany`. The third read is the settings door, and it is
|
|
18
|
+
* one for the whole batch: `readDeviceConfigs` over every persisted child of
|
|
19
|
+
* every parent. That one was the leftover — the ledger N+1 was removed and a
|
|
20
|
+
* door round trip PER CHILD stayed behind it (896 on the live hub 2026-08-30,
|
|
21
|
+
* all of them forked-owned and therefore all persisted).
|
|
18
22
|
*
|
|
19
23
|
* The live half is indexed once too: `registry.getChildren` is
|
|
20
24
|
* `getAll().filter(…)`, so calling it per parent would trade an RPC N+1 for an
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The promise-chain mutex the device-manager orders its read-modify-writes
|
|
3
|
+
* with.
|
|
4
|
+
*
|
|
5
|
+
* One owner at a time, FIFO, no timers and no timeout: a waiter is parked on
|
|
6
|
+
* the previous holder's completion, so nothing is delayed that was not already
|
|
7
|
+
* waiting for a store round trip. A rejected holder still releases the chain —
|
|
8
|
+
* a failed write must not wedge every writer behind it.
|
|
9
|
+
*
|
|
10
|
+
* Extracted from `DeviceMetaStore.withMetaWriteLock`, which was the only
|
|
11
|
+
* instance until the addon's OWN settings row set needed the same ordering.
|
|
12
|
+
* Two subtly different copies of a mutex is not a thing this file wants to
|
|
13
|
+
* exist.
|
|
14
|
+
*/
|
|
15
|
+
/** Run `fn` with exclusive ownership of one lock. */
|
|
16
|
+
export type WriteLock = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
17
|
+
export declare function createWriteLock(): WriteLock;
|
|
@@ -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-
|
|
6
|
+
const require_dist = require("../../dist-CcvXUhHK.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
|