@camstack/system 1.2.62 → 1.2.64
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 +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -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.js +3 -3
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/block-package.d.ts +9 -27
- package/dist/builtins/core-blocks/block-supervisor.d.ts +15 -0
- package/dist/builtins/core-blocks/core-blocks.addon.js +91 -24
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +91 -25
- package/dist/builtins/device-manager/device-bindings-store.d.ts +28 -5
- package/dist/builtins/device-manager/device-linked-devices.d.ts +2 -2
- package/dist/builtins/device-manager/device-manager.addon.d.ts +3 -45
- package/dist/builtins/device-manager/device-manager.addon.js +123 -884
- package/dist/builtins/device-manager/device-manager.addon.mjs +123 -884
- package/dist/builtins/device-manager/device-meta-actions.d.ts +0 -8
- package/dist/builtins/device-manager/device-meta-store.d.ts +1 -28
- package/dist/builtins/device-manager/device-meta-types.d.ts +2 -6
- package/dist/builtins/device-manager/device-projection.d.ts +1 -2
- package/dist/builtins/device-manager/device-provider-context.d.ts +2 -13
- package/dist/builtins/device-manager/device-queries.d.ts +1 -1
- package/dist/builtins/device-manager/device-state-mirror.d.ts +8 -49
- package/dist/builtins/device-manager/device-sync-resolvers.d.ts +0 -11
- 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/homeassistant-integration.d.ts +7 -0
- package/dist/builtins/local-auth/index.d.ts +1 -0
- package/dist/builtins/local-auth/index.js +1 -0
- package/dist/builtins/local-auth/index.mjs +2 -2
- package/dist/builtins/local-auth/local-auth.addon.js +561 -497
- package/dist/builtins/local-auth/local-auth.addon.mjs +580 -517
- package/dist/builtins/local-auth/oauth-grants.d.ts +11 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts +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 +2 -2
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +2 -2
- package/dist/builtins/platform-probe/index.js +2 -2
- package/dist/builtins/platform-probe/index.mjs +2 -2
- 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 +319 -20
- package/dist/builtins/snapshot/index.mjs +319 -20
- package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +86 -0
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +48 -1
- package/dist/builtins/snapshot/snapshot.addon.d.ts +65 -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/retired-settings-keys.d.ts +15 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +73 -7
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +73 -7
- package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +19 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -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-DnKpFaP1.mjs → dist-CMb7aKS0.mjs} +1227 -1330
- package/dist/{dist-DgPUarcs.js → dist-DP3nHlNM.js} +1251 -1366
- package/dist/http/asset-compression.d.ts +93 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -9
- package/dist/index.mjs +65 -11
- package/dist/kernel/heap-watch.d.ts +55 -0
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
- package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-BE6l8Y4W.js} +21 -15
- package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-Dz1ryHIN.mjs} +21 -15
- package/dist/{model-download-service-D8B-4ktF.mjs → model-download-service-CTBHzORJ.mjs} +220 -2
- package/dist/{model-download-service-Cpc6SWzV.js → model-download-service-Caj1kaSi.js} +220 -2
- package/package.json +5 -1
- package/dist/builtins/device-manager/device-link-cycle.d.ts +0 -18
- package/dist/builtins/device-manager/device-link-overlay.d.ts +0 -33
- package/dist/builtins/device-manager/device-link-resolver.d.ts +0 -44
- package/dist/main-CVLq8IzA.js +0 -2356
- package/dist/main-CzH19GkU.mjs +0 -2353
|
@@ -3,9 +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-
|
|
6
|
+
const require_dist = require("../../dist-DP3nHlNM.js");
|
|
7
7
|
let _camstack_types_node = require("@camstack/types/node");
|
|
8
|
-
let zod = require("zod");
|
|
9
8
|
let node_crypto = require("node:crypto");
|
|
10
9
|
//#region src/builtins/device-manager/day-night-config-schema.ts
|
|
11
10
|
var MODE_LABELS = {
|
|
@@ -537,13 +536,6 @@ function mergeAggregates(parts) {
|
|
|
537
536
|
//#endregion
|
|
538
537
|
//#region src/builtins/device-manager/device-projection.ts
|
|
539
538
|
/**
|
|
540
|
-
* Pure device-projection + small predicate helpers for the device-manager
|
|
541
|
-
* addon. Extracted verbatim from `device-manager.addon.ts`. None of these
|
|
542
|
-
* functions close over the addon instance — they take everything they need
|
|
543
|
-
* as explicit parameters, so they are safe to share across the addon's
|
|
544
|
-
* query / aggregation modules.
|
|
545
|
-
*/
|
|
546
|
-
/**
|
|
547
539
|
* Return true when `err` is a transient Moleculer error that is worth
|
|
548
540
|
* retrying — specifically any `MoleculerRetryableError` subclass
|
|
549
541
|
* (ServiceNotAvailableError, ServiceNotFoundError, BrokerDisconnectedError,
|
|
@@ -572,10 +564,6 @@ function shallowEqual(a, b) {
|
|
|
572
564
|
for (const k of ak) if (a[k] !== b[k]) return false;
|
|
573
565
|
return true;
|
|
574
566
|
}
|
|
575
|
-
/** Returns true when `x` is a non-null, non-array plain object. */
|
|
576
|
-
function isRecord$1(x) {
|
|
577
|
-
return x !== null && typeof x === "object" && !Array.isArray(x);
|
|
578
|
-
}
|
|
579
567
|
function isCameraDevice(device) {
|
|
580
568
|
return "getStreamSources" in device && typeof device.getStreamSources === "function";
|
|
581
569
|
}
|
|
@@ -660,7 +648,6 @@ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
|
|
|
660
648
|
...metaRow?.linkDeviceId !== void 0 ? { linkDeviceId: metaRow.linkDeviceId } : {},
|
|
661
649
|
...metaRow?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: metaRow.primaryChildEntityId } : {},
|
|
662
650
|
...metaRow?.childLayout !== void 0 ? { childLayout: metaRow.childLayout } : {},
|
|
663
|
-
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {},
|
|
664
651
|
...metaRow?.display !== void 0 ? { display: metaRow.display } : {}
|
|
665
652
|
};
|
|
666
653
|
}
|
|
@@ -676,28 +663,6 @@ function resolveDeviceById(registry, deviceId) {
|
|
|
676
663
|
}
|
|
677
664
|
//#endregion
|
|
678
665
|
//#region src/builtins/device-manager/device-queries.ts
|
|
679
|
-
/**
|
|
680
|
-
* Device query + live-runtime + provider/adoption operations for the
|
|
681
|
-
* device-manager addon — the read/live half of the `IDeviceManagerProvider`
|
|
682
|
-
* surface. Extracted verbatim from the `onInitialize` provider literal as free
|
|
683
|
-
* functions taking a `ProviderContext` (DI). Input/return types are pinned to
|
|
684
|
-
* the cap contract via `Parameters<IDeviceManagerProvider['x']>[0]` /
|
|
685
|
-
* `ReturnType<…>` so the typed surface is identical to the inferred methods.
|
|
686
|
-
*
|
|
687
|
-
* Covers: persisted/live device queries (listPersistedByAddon, listAll,
|
|
688
|
-
* getDevice, getChildren), live runtime methods (getStreamSources,
|
|
689
|
-
* getConfigSchema, getSettingsSchema, updateConfig, enable, disable, remove,
|
|
690
|
-
* removeByIntegration, getStreamProfileMap, setStreamProfileMap, probeStreams),
|
|
691
|
-
* device-provider ops (discoverDevices, adoptDevice, getCreationSchema,
|
|
692
|
-
* createDevice, testCreationField), and device-adoption ops
|
|
693
|
-
* (adoptionListCandidates/Refresh/Adopt/Release/Resync, testField).
|
|
694
|
-
*
|
|
695
|
-
* The hub-live-then-persisted-fallback union, the cross-process device-ops
|
|
696
|
-
* dispatch, the depth-first remove cascade, and the resetToSource resync purge
|
|
697
|
-
* are byte-for-byte preserved. `remove`/`removeByIntegration`/`enable`/`disable`/
|
|
698
|
-
* `probeStreams`/`getStreamProfileMap` self-call sibling cap methods via
|
|
699
|
-
* `pctx.provider`. Pure refactor — no behavior change.
|
|
700
|
-
*/
|
|
701
666
|
async function listPersistedByAddon(pctx, input) {
|
|
702
667
|
const { addonId } = input;
|
|
703
668
|
const [index, meta] = await Promise.all([pctx.metaStore.readIndex(), pctx.metaStore.readMeta()]);
|
|
@@ -774,7 +739,6 @@ async function listAll(pctx, input) {
|
|
|
774
739
|
...m?.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
775
740
|
...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
776
741
|
...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
777
|
-
...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
778
742
|
...m?.display !== void 0 ? { display: m.display } : {},
|
|
779
743
|
...(() => {
|
|
780
744
|
if (slim) return {};
|
|
@@ -824,7 +788,6 @@ async function getDevice(pctx, input) {
|
|
|
824
788
|
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
825
789
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
826
790
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
827
|
-
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
828
791
|
...m.display !== void 0 ? { display: m.display } : {},
|
|
829
792
|
...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
|
|
830
793
|
};
|
|
@@ -887,7 +850,6 @@ async function getChildren(pctx, input) {
|
|
|
887
850
|
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
888
851
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
889
852
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
890
|
-
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
891
853
|
...m.display !== void 0 ? { display: m.display } : {},
|
|
892
854
|
...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
|
|
893
855
|
});
|
|
@@ -1443,18 +1405,6 @@ function splitDeviceStoreKeys(patch) {
|
|
|
1443
1405
|
//#endregion
|
|
1444
1406
|
//#region src/builtins/device-manager/device-bindings-store.ts
|
|
1445
1407
|
/**
|
|
1446
|
-
* Persisted wrapper-activation + per-device binding resolution for the
|
|
1447
|
-
* device-manager addon. Extracted from `device-manager.addon.ts` as free
|
|
1448
|
-
* functions that receive an explicit `BindingsDeps` context (dependency
|
|
1449
|
-
* injection) instead of closing over the addon instance — keeps the routing
|
|
1450
|
-
* logic cohesive and independently testable while preserving the EXACT call
|
|
1451
|
-
* semantics, ordering, and side effects of the original class methods.
|
|
1452
|
-
*
|
|
1453
|
-
* The addon keeps thin delegator methods (`getBindings`, `getAllBindings`, …)
|
|
1454
|
-
* that build `BindingsDeps` from its own state and forward here, so every
|
|
1455
|
-
* existing internal + cap-router call site is unchanged.
|
|
1456
|
-
*/
|
|
1457
|
-
/**
|
|
1458
1408
|
* Wire the push-fed cross-process native-cap cache (`remoteNativeCaps`).
|
|
1459
1409
|
* Workers emit `DeviceBindingsChanged` on `ctx.registerNativeCap` / device
|
|
1460
1410
|
* removal; we mirror those into `remoteNativeCaps` so `getBindings` returns the
|
|
@@ -1556,9 +1506,47 @@ function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
|
|
|
1556
1506
|
};
|
|
1557
1507
|
return null;
|
|
1558
1508
|
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Resolve the device's declared TYPE (`'camera'`, `'event-emitter'`, …), or
|
|
1511
|
+
* `undefined` when it cannot be established.
|
|
1512
|
+
*
|
|
1513
|
+
* The PERSISTED meta is the authority: `ctx.kernel.deviceRegistry` is hub-only
|
|
1514
|
+
* and has been observed empty in the very process that answers `getBindings`
|
|
1515
|
+
* correctly (see the note on `getAllBindings`). The registry is consulted only
|
|
1516
|
+
* as a secondary source, for a device constructed but not yet persisted.
|
|
1517
|
+
*
|
|
1518
|
+
* `undefined` is a first-class answer and callers must treat it as "no
|
|
1519
|
+
* filtering" — an absent row must never be the reason a device loses bindings.
|
|
1520
|
+
*/
|
|
1521
|
+
function resolveDeviceType(deps, rawStore, deviceId) {
|
|
1522
|
+
const persisted = rawStore.deviceMeta?.[String(deviceId)]?.type;
|
|
1523
|
+
if (typeof persisted === "string" && persisted.length > 0) return persisted;
|
|
1524
|
+
const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
|
|
1525
|
+
return typeof live === "string" && live.length > 0 ? live : void 0;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Does a capability apply to a device of type `deviceType`?
|
|
1529
|
+
*
|
|
1530
|
+
* The cap's `deviceTypes` is the ONLY declaration consulted (D4: behavioural
|
|
1531
|
+
* cap metadata lives in the `*.cap.ts`, never in a manifest). Two deliberate
|
|
1532
|
+
* fail-open cases:
|
|
1533
|
+
*
|
|
1534
|
+
* - a cap that declares no `deviceTypes` (or an empty list) applies to every
|
|
1535
|
+
* device — the pre-existing, back-compatible semantics;
|
|
1536
|
+
* - an UNKNOWN `deviceType` never filters, so a missing/failed meta lookup
|
|
1537
|
+
* changes nothing (D49: a read that fails must not destroy work).
|
|
1538
|
+
*/
|
|
1539
|
+
function capAppliesToDeviceType(def, deviceType) {
|
|
1540
|
+
if (deviceType === void 0) return true;
|
|
1541
|
+
const declared = def?.deviceTypes;
|
|
1542
|
+
if (!declared || declared.length === 0) return true;
|
|
1543
|
+
return declared.some((t) => t === deviceType);
|
|
1544
|
+
}
|
|
1559
1545
|
async function getBindings(deps, input) {
|
|
1560
1546
|
const storeKey = String(input.deviceId);
|
|
1561
|
-
const
|
|
1547
|
+
const rawStore = await deps.ctx.settings.readAddonStore();
|
|
1548
|
+
const perDevice = (rawStore.deviceBindings ?? {})[storeKey] ?? {};
|
|
1549
|
+
const deviceType = resolveDeviceType(deps, rawStore, input.deviceId);
|
|
1562
1550
|
const entries = [];
|
|
1563
1551
|
const seenCaps = /* @__PURE__ */ new Set();
|
|
1564
1552
|
const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
|
|
@@ -1580,21 +1568,35 @@ async function getBindings(deps, input) {
|
|
|
1580
1568
|
});
|
|
1581
1569
|
seenCaps.add(capName);
|
|
1582
1570
|
}
|
|
1583
|
-
if (deps.capabilityRegistry)
|
|
1584
|
-
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
capName
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
nativeAddonId
|
|
1571
|
+
if (deps.capabilityRegistry) {
|
|
1572
|
+
const skippedForType = [];
|
|
1573
|
+
for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
|
|
1574
|
+
if (seenCaps.has(capName)) continue;
|
|
1575
|
+
if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
|
|
1576
|
+
skippedForType.push(capName);
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
|
|
1580
|
+
if (!defaultWrapperAddonId) continue;
|
|
1581
|
+
const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
|
|
1582
|
+
const remoteNative = resolveRemote(capName);
|
|
1583
|
+
const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
|
|
1584
|
+
entries.push({
|
|
1585
|
+
capName,
|
|
1586
|
+
kind: "wrapped",
|
|
1587
|
+
providerAddonId: defaultWrapperAddonId,
|
|
1588
|
+
providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
|
|
1589
|
+
nativeAddonId
|
|
1590
|
+
});
|
|
1591
|
+
seenCaps.add(capName);
|
|
1592
|
+
}
|
|
1593
|
+
if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped — deviceTypes mismatch", {
|
|
1594
|
+
tags: { deviceId: input.deviceId },
|
|
1595
|
+
meta: {
|
|
1596
|
+
deviceType,
|
|
1597
|
+
skipped: skippedForType
|
|
1598
|
+
}
|
|
1596
1599
|
});
|
|
1597
|
-
seenCaps.add(capName);
|
|
1598
1600
|
}
|
|
1599
1601
|
if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
|
|
1600
1602
|
if (seenCaps.has(capName)) continue;
|
|
@@ -1636,21 +1638,6 @@ async function getBindings(deps, input) {
|
|
|
1636
1638
|
});
|
|
1637
1639
|
seenCaps.add(entry.capName);
|
|
1638
1640
|
}
|
|
1639
|
-
if (deps.devicesWithLinks?.has(input.deviceId)) {
|
|
1640
|
-
const row = ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(input.deviceId)];
|
|
1641
|
-
for (const link of row?.deviceLinks ?? []) {
|
|
1642
|
-
const capName = link.target.cap;
|
|
1643
|
-
if (seenCaps.has(capName)) continue;
|
|
1644
|
-
entries.push({
|
|
1645
|
-
capName,
|
|
1646
|
-
kind: "linked",
|
|
1647
|
-
providerAddonId: require_dist.bareAddonId(deps.ctx.id),
|
|
1648
|
-
providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
|
|
1649
|
-
nativeAddonId: ""
|
|
1650
|
-
});
|
|
1651
|
-
seenCaps.add(capName);
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
1641
|
return {
|
|
1655
1642
|
deviceId: input.deviceId,
|
|
1656
1643
|
entries
|
|
@@ -1668,10 +1655,20 @@ async function getBindings(deps, input) {
|
|
|
1668
1655
|
* payload periodically.
|
|
1669
1656
|
*/
|
|
1670
1657
|
async function getAllBindings(deps) {
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1658
|
+
const store = await deps.ctx.settings.readAddonStore();
|
|
1659
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1660
|
+
for (const key of Object.keys(store.deviceMeta ?? {})) {
|
|
1661
|
+
const id = Number(key);
|
|
1662
|
+
if (Number.isInteger(id)) ids.add(id);
|
|
1663
|
+
}
|
|
1664
|
+
const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
|
|
1665
|
+
for (const device of registered) ids.add(device.id);
|
|
1666
|
+
if (ids.size === 0) {
|
|
1667
|
+
deps.ctx.logger.warn("getAllBindings found no devices — warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
|
|
1668
|
+
return [];
|
|
1669
|
+
}
|
|
1673
1670
|
const out = [];
|
|
1674
|
-
for (const
|
|
1671
|
+
for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
|
|
1675
1672
|
return out;
|
|
1676
1673
|
}
|
|
1677
1674
|
/**
|
|
@@ -2595,120 +2592,6 @@ var DeviceEventPropagator = class {
|
|
|
2595
2592
|
}
|
|
2596
2593
|
};
|
|
2597
2594
|
//#endregion
|
|
2598
|
-
//#region src/builtins/device-manager/device-link-cycle.ts
|
|
2599
|
-
function nodeKey(deviceId, cap) {
|
|
2600
|
-
return `${deviceId}:${cap}`;
|
|
2601
|
-
}
|
|
2602
|
-
/** The target's container stableId — its parent's (falling back to its own for
|
|
2603
|
-
* a top-level target). Sibling FIELD sources resolve relative to this, the same
|
|
2604
|
-
* rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
|
|
2605
|
-
function containerStableId(targetRow, rowById) {
|
|
2606
|
-
return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
|
|
2607
|
-
}
|
|
2608
|
-
/** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
|
|
2609
|
-
* null when it is a literal or resolves to no known device. */
|
|
2610
|
-
function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
|
|
2611
|
-
if (binding.kind === "literal") return null;
|
|
2612
|
-
const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
|
|
2613
|
-
const srcId = idByStableId.get(wantedStableId);
|
|
2614
|
-
if (srcId === void 0) return null;
|
|
2615
|
-
return nodeKey(srcId, binding.cap);
|
|
2616
|
-
}
|
|
2617
|
-
/**
|
|
2618
|
-
* Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
|
|
2619
|
-
* A literal source depends on nothing; a field/global source depends on exactly
|
|
2620
|
-
* one node; an EXPRESSION source depends on one node per resolvable non-literal
|
|
2621
|
-
* binding. Unresolvable (dangling) bindings contribute no edge — they cannot
|
|
2622
|
-
* loop while dangling; a cycle that only becomes resolvable later is bounded by
|
|
2623
|
-
* the resolve-time re-entrancy guard.
|
|
2624
|
-
*/
|
|
2625
|
-
function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
|
|
2626
|
-
const src = link.source;
|
|
2627
|
-
if (src.kind === "expression") {
|
|
2628
|
-
const nodes = [];
|
|
2629
|
-
for (const binding of Object.values(src.bindings)) {
|
|
2630
|
-
const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
|
|
2631
|
-
if (node !== null) nodes.push(node);
|
|
2632
|
-
}
|
|
2633
|
-
return nodes;
|
|
2634
|
-
}
|
|
2635
|
-
const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
|
|
2636
|
-
return node !== null ? [node] : [];
|
|
2637
|
-
}
|
|
2638
|
-
/**
|
|
2639
|
-
* Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
|
|
2640
|
-
* a dependency cycle. Returns the cycle as an ordered list of node keys
|
|
2641
|
-
* (first node repeated at the end) or null when the set is safe.
|
|
2642
|
-
*/
|
|
2643
|
-
function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
|
|
2644
|
-
const rows = Object.values(allMeta);
|
|
2645
|
-
const rowById = /* @__PURE__ */ new Map();
|
|
2646
|
-
const idByStableId = /* @__PURE__ */ new Map();
|
|
2647
|
-
for (const r of rows) {
|
|
2648
|
-
rowById.set(r.id, r);
|
|
2649
|
-
if (!idByStableId.has(r.stableId)) idByStableId.set(r.stableId, r.id);
|
|
2650
|
-
}
|
|
2651
|
-
const dependsOn = /* @__PURE__ */ new Map();
|
|
2652
|
-
const addEdges = (row, links) => {
|
|
2653
|
-
for (const link of links) {
|
|
2654
|
-
const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
|
|
2655
|
-
if (srcNodes.length === 0) continue;
|
|
2656
|
-
const tNode = nodeKey(row.id, link.target.cap);
|
|
2657
|
-
const list = dependsOn.get(tNode) ?? [];
|
|
2658
|
-
for (const srcNode of srcNodes) list.push(srcNode);
|
|
2659
|
-
dependsOn.set(tNode, list);
|
|
2660
|
-
}
|
|
2661
|
-
};
|
|
2662
|
-
for (const r of rows) {
|
|
2663
|
-
const links = r.id === editedDeviceId ? editedLinks : r.deviceLinks ?? [];
|
|
2664
|
-
if (links.length > 0) addEdges(r, links);
|
|
2665
|
-
}
|
|
2666
|
-
const roots = [...new Set(editedLinks.map((l) => nodeKey(editedDeviceId, l.target.cap)))];
|
|
2667
|
-
for (const root of roots) {
|
|
2668
|
-
const cycle = dfsFindCycle(root, dependsOn);
|
|
2669
|
-
if (cycle) return cycle;
|
|
2670
|
-
}
|
|
2671
|
-
return null;
|
|
2672
|
-
}
|
|
2673
|
-
/** Iterative DFS from `root` over `dependsOn`; returns the first back-edge
|
|
2674
|
-
* cycle path (closed — first node repeated last) or null. */
|
|
2675
|
-
function dfsFindCycle(root, dependsOn) {
|
|
2676
|
-
const onPath = /* @__PURE__ */ new Set();
|
|
2677
|
-
const done = /* @__PURE__ */ new Set();
|
|
2678
|
-
const path = [];
|
|
2679
|
-
const stack = [{
|
|
2680
|
-
node: root,
|
|
2681
|
-
nextChild: 0
|
|
2682
|
-
}];
|
|
2683
|
-
onPath.add(root);
|
|
2684
|
-
path.push(root);
|
|
2685
|
-
while (stack.length > 0) {
|
|
2686
|
-
const frame = stack[stack.length - 1];
|
|
2687
|
-
const children = dependsOn.get(frame.node) ?? [];
|
|
2688
|
-
if (frame.nextChild < children.length) {
|
|
2689
|
-
const child = children[frame.nextChild];
|
|
2690
|
-
frame.nextChild += 1;
|
|
2691
|
-
if (onPath.has(child)) {
|
|
2692
|
-
const start = path.indexOf(child);
|
|
2693
|
-
return [...path.slice(start), child];
|
|
2694
|
-
}
|
|
2695
|
-
if (done.has(child)) continue;
|
|
2696
|
-
stack.push({
|
|
2697
|
-
node: child,
|
|
2698
|
-
nextChild: 0
|
|
2699
|
-
});
|
|
2700
|
-
onPath.add(child);
|
|
2701
|
-
path.push(child);
|
|
2702
|
-
continue;
|
|
2703
|
-
}
|
|
2704
|
-
stack.pop();
|
|
2705
|
-
onPath.delete(frame.node);
|
|
2706
|
-
path.pop();
|
|
2707
|
-
done.add(frame.node);
|
|
2708
|
-
}
|
|
2709
|
-
return null;
|
|
2710
|
-
}
|
|
2711
|
-
//#endregion
|
|
2712
2595
|
//#region src/builtins/device-manager/device-meta-actions.ts
|
|
2713
2596
|
/**
|
|
2714
2597
|
* Device meta-mutation + persistence actions for the device-manager addon —
|
|
@@ -2722,7 +2605,7 @@ function dfsFindCycle(root, dependsOn) {
|
|
|
2722
2605
|
* removeDevice), config persistence (persistConfig, loadConfig), the meta
|
|
2723
2606
|
* surface load (loadMeta, loadRuntimeState), every meta setter (setName,
|
|
2724
2607
|
* setLocation, setType, setIntegrationId, setLinkDeviceId,
|
|
2725
|
-
* setPrimaryChildEntityId, setChildLayout,
|
|
2608
|
+
* setPrimaryChildEntityId, setChildLayout, setRole, setDisplay,
|
|
2726
2609
|
* applyInitialMeta, setMetadata, setDisabled), and the location registry
|
|
2727
2610
|
* (listLocations, addLocation, removeLocation).
|
|
2728
2611
|
*
|
|
@@ -2829,7 +2712,6 @@ async function registerDevice(pctx, input) {
|
|
|
2829
2712
|
...existingMeta?.linkDeviceId !== void 0 ? { linkDeviceId: existingMeta.linkDeviceId } : {},
|
|
2830
2713
|
...existingMeta?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: existingMeta.primaryChildEntityId } : {},
|
|
2831
2714
|
...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
|
|
2832
|
-
...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
|
|
2833
2715
|
...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
|
|
2834
2716
|
...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
|
|
2835
2717
|
parentDeviceId,
|
|
@@ -2891,7 +2773,6 @@ async function registerDevice(pctx, input) {
|
|
|
2891
2773
|
generation: Date.now()
|
|
2892
2774
|
}
|
|
2893
2775
|
});
|
|
2894
|
-
if (pctx.host.devicesWithLinks.size > 0 && pctx.host.expectedSourceStableIds.has(stableId)) await pctx.metaStore.rebuildLinkDependents();
|
|
2895
2776
|
}
|
|
2896
2777
|
async function removeDevice(pctx, input) {
|
|
2897
2778
|
const { deviceId } = input;
|
|
@@ -2930,9 +2811,6 @@ async function removeDevice(pctx, input) {
|
|
|
2930
2811
|
pctx.host.remoteNativeCaps.delete(deviceId);
|
|
2931
2812
|
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
|
|
2932
2813
|
pctx.metaStore.idToAddonId.delete(deviceId);
|
|
2933
|
-
pctx.host.devicesWithLinks.delete(deviceId);
|
|
2934
|
-
pctx.host.dropDeviceOverlays(deviceId);
|
|
2935
|
-
await pctx.metaStore.rebuildLinkDependents();
|
|
2936
2814
|
pctx.host.ctx.logger.info("removed device", { tags: {
|
|
2937
2815
|
deviceId,
|
|
2938
2816
|
deviceName: deviceName.length > 0 ? deviceName : stableId
|
|
@@ -3263,54 +3141,6 @@ async function setChildLayout(pctx, input) {
|
|
|
3263
3141
|
});
|
|
3264
3142
|
}
|
|
3265
3143
|
/**
|
|
3266
|
-
* Set (or replace) the operator-authored cross-device field wirings on
|
|
3267
|
-
* a device's meta row. Mirrors `setChildLayout` but persists the
|
|
3268
|
-
* `deviceLinks` array. Persisted, projected, and preserved across
|
|
3269
|
-
* re-register/restore. Also maintains the in-memory `devicesWithLinks`
|
|
3270
|
-
* Set used as an O(1) gate by `resolveLinkedStatus`.
|
|
3271
|
-
*/
|
|
3272
|
-
async function setDeviceLinks(pctx, input) {
|
|
3273
|
-
const { deviceId, deviceLinks } = input;
|
|
3274
|
-
const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
|
|
3275
|
-
if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
|
|
3276
|
-
for (const link of deviceLinks) {
|
|
3277
|
-
if (link.source.kind !== "expression") continue;
|
|
3278
|
-
const err = require_dist.validateExpressionSource(link.source);
|
|
3279
|
-
if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
|
|
3280
|
-
}
|
|
3281
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3282
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3283
|
-
if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
|
|
3284
|
-
const { meta: m } = persisted;
|
|
3285
|
-
const key = String(deviceId);
|
|
3286
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3287
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3288
|
-
...allMeta,
|
|
3289
|
-
[key]: {
|
|
3290
|
-
...m,
|
|
3291
|
-
deviceLinks
|
|
3292
|
-
}
|
|
3293
|
-
} });
|
|
3294
|
-
});
|
|
3295
|
-
if (deviceLinks.length > 0) pctx.host.devicesWithLinks.add(deviceId);
|
|
3296
|
-
else pctx.host.devicesWithLinks.delete(deviceId);
|
|
3297
|
-
await pctx.metaStore.rebuildLinkDependents();
|
|
3298
|
-
pctx.host.ctx.eventBus.emit({
|
|
3299
|
-
id: (0, node_crypto.randomUUID)(),
|
|
3300
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
3301
|
-
source: {
|
|
3302
|
-
type: "device",
|
|
3303
|
-
id: deviceId
|
|
3304
|
-
},
|
|
3305
|
-
category: require_dist.EventCategory.DeviceMetaChanged,
|
|
3306
|
-
data: {
|
|
3307
|
-
deviceId,
|
|
3308
|
-
field: "deviceLinks",
|
|
3309
|
-
value: deviceLinks
|
|
3310
|
-
}
|
|
3311
|
-
});
|
|
3312
|
-
}
|
|
3313
|
-
/**
|
|
3314
3144
|
* Stamp (or update) the semantic role on the device's meta row.
|
|
3315
3145
|
* Called by the kernel's `create()` / `spawnAccessoryChild`
|
|
3316
3146
|
* pre-seed when `initialMeta.role` is set (analogous to
|
|
@@ -3665,69 +3495,10 @@ async function removeLocation(pctx, input) {
|
|
|
3665
3495
|
});
|
|
3666
3496
|
}
|
|
3667
3497
|
//#endregion
|
|
3668
|
-
//#region src/builtins/device-manager/device-link-overlay.ts
|
|
3669
|
-
/** Build both lookup maps from resolved link entries. Pure — order-preserving. */
|
|
3670
|
-
function buildLinkIndexes(entries) {
|
|
3671
|
-
const targets = /* @__PURE__ */ new Map();
|
|
3672
|
-
const dependents = /* @__PURE__ */ new Map();
|
|
3673
|
-
for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
|
|
3674
|
-
const tKey = `${targetDeviceId}:${link.target.cap}`;
|
|
3675
|
-
const tList = targets.get(tKey) ?? [];
|
|
3676
|
-
tList.push({
|
|
3677
|
-
link,
|
|
3678
|
-
sourceDeviceId,
|
|
3679
|
-
...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
|
|
3680
|
-
});
|
|
3681
|
-
targets.set(tKey, tList);
|
|
3682
|
-
const addDependent = (sourceId, sourceCap) => {
|
|
3683
|
-
const sKey = `${sourceId}:${sourceCap}`;
|
|
3684
|
-
const sList = dependents.get(sKey) ?? [];
|
|
3685
|
-
sList.push({
|
|
3686
|
-
targetDeviceId,
|
|
3687
|
-
targetCap: link.target.cap
|
|
3688
|
-
});
|
|
3689
|
-
dependents.set(sKey, sList);
|
|
3690
|
-
};
|
|
3691
|
-
if (link.source.kind === "expression") {
|
|
3692
|
-
if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
|
|
3693
|
-
const binding = link.source.bindings[name];
|
|
3694
|
-
if (binding === void 0 || binding.kind === "literal") continue;
|
|
3695
|
-
addDependent(bindingDeviceId, binding.cap);
|
|
3696
|
-
}
|
|
3697
|
-
continue;
|
|
3698
|
-
}
|
|
3699
|
-
if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
|
|
3700
|
-
}
|
|
3701
|
-
return {
|
|
3702
|
-
targets,
|
|
3703
|
-
dependents
|
|
3704
|
-
};
|
|
3705
|
-
}
|
|
3706
|
-
//#endregion
|
|
3707
3498
|
//#region src/builtins/device-manager/device-meta-store.ts
|
|
3708
|
-
/**
|
|
3709
|
-
* Meta persistence layer for the device-manager addon.
|
|
3710
|
-
*
|
|
3711
|
-
* `DeviceMetaStore` encapsulates the read/modify/write helpers that previously
|
|
3712
|
-
* lived as closures inside `onInitialize`: the `ctx.settings`-backed reads
|
|
3713
|
-
* (`readStore` / `readIndex` / `readMeta` / `readMetadataMap`), the
|
|
3714
|
-
* single-writer mutex (`withMetaWriteLock`), id allocation
|
|
3715
|
-
* (`allocateNextDeviceId`), ownership/child resolution, link-source resolution,
|
|
3716
|
-
* and the cross-device link reverse-index rebuild. Extracting it as a class
|
|
3717
|
-
* (constructed once per `onInitialize`) keeps the EXACT semantics — the
|
|
3718
|
-
* `withMetaWriteLock` promise-chain is per-instance state, identical to the
|
|
3719
|
-
* former closure — while letting the meta-action + query modules depend on a
|
|
3720
|
-
* named, injectable surface instead of captured closures.
|
|
3721
|
-
*
|
|
3722
|
-
* Cross-device link state (the `linkTargets` / `linkDependents` /
|
|
3723
|
-
* `expectedSourceStableIds` maps + the `devicesWithLinks` gate) lives on the
|
|
3724
|
-
* addon; the store reaches it through the injected `LinkIndexHost` so the
|
|
3725
|
-
* single owner of that state stays the addon instance.
|
|
3726
|
-
*/
|
|
3727
3499
|
var DeviceMetaStore = class {
|
|
3728
3500
|
settings;
|
|
3729
3501
|
registry;
|
|
3730
|
-
linkHost;
|
|
3731
3502
|
/** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
|
|
3732
3503
|
* The persisted meta store is authoritative but reads are async; hub-side
|
|
3733
3504
|
* callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
|
|
@@ -3738,10 +3509,9 @@ var DeviceMetaStore = class {
|
|
|
3738
3509
|
* through one promise chain (see `withMetaWriteLock`). Per-instance state —
|
|
3739
3510
|
* identical to the former `onInitialize` closure variable. */
|
|
3740
3511
|
metaWriteChain = Promise.resolve();
|
|
3741
|
-
constructor(settings, registry
|
|
3512
|
+
constructor(settings, registry) {
|
|
3742
3513
|
this.settings = settings;
|
|
3743
3514
|
this.registry = registry;
|
|
3744
|
-
this.linkHost = linkHost;
|
|
3745
3515
|
}
|
|
3746
3516
|
readStore = async () => {
|
|
3747
3517
|
return await this.settings.readAddonStore();
|
|
@@ -3803,279 +3573,31 @@ var DeviceMetaStore = class {
|
|
|
3803
3573
|
ids.delete(parentId);
|
|
3804
3574
|
return [...ids];
|
|
3805
3575
|
};
|
|
3806
|
-
/** Resolve a link's `sourceKey` to a live device id: the sibling accessory
|
|
3807
|
-
* whose stableId is `${parentStableId}-${sourceKey}`. Null when absent.
|
|
3808
|
-
* Pure over a pre-read meta map so a multi-link resolve reads the store once. */
|
|
3809
|
-
resolveSourceDeviceId = (parentStableId, sourceKey, meta) => {
|
|
3810
|
-
const wanted = `${parentStableId}-${sourceKey}`;
|
|
3811
|
-
for (const m of Object.values(meta)) if (m.stableId === wanted) return m.id;
|
|
3812
|
-
return null;
|
|
3813
|
-
};
|
|
3814
|
-
/** Resolve a GLOBAL link source (P2e) to a live device id: the device whose
|
|
3815
|
-
* FULL stableId equals `sourceStableId`, regardless of parent container.
|
|
3816
|
-
* First match wins (stableIds are effectively unique cluster-wide — see the
|
|
3817
|
-
* `DeviceLinkGlobalSource` docblock). Null when absent. Pure over a
|
|
3818
|
-
* pre-read meta map so a multi-link resolve reads the store once. */
|
|
3819
|
-
resolveGlobalSourceDeviceId = (sourceStableId, meta) => {
|
|
3820
|
-
for (const m of Object.values(meta)) if (m.stableId === sourceStableId) return m.id;
|
|
3821
|
-
return null;
|
|
3822
|
-
};
|
|
3823
3576
|
allocateNextDeviceId = async () => {
|
|
3824
3577
|
const current = (await this.readStore()).nextDeviceId ?? 1;
|
|
3825
3578
|
await this.settings.writeAddonStore({ nextDeviceId: current + 1 });
|
|
3826
3579
|
return current;
|
|
3827
3580
|
};
|
|
3828
|
-
/** Rebuild the `linkTargets` / `linkDependents` reverse-index maps from the
|
|
3829
|
-
* current persisted meta. Called at boot (once the `devicesWithLinks` seed
|
|
3830
|
-
* has run) and whenever the link topology can change: `setDeviceLinks`,
|
|
3831
|
-
* `registerDevice` (a new sibling source may resolve previously-dangling
|
|
3832
|
-
* links), and `removeDevice` (its entries must be evicted). */
|
|
3833
|
-
rebuildLinkDependents = async () => {
|
|
3834
|
-
if (this.linkHost.devicesWithLinks.size === 0) {
|
|
3835
|
-
this.linkHost.setLinkIndexes(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
3836
|
-
return;
|
|
3837
|
-
}
|
|
3838
|
-
const allMeta = await this.readMeta();
|
|
3839
|
-
const stableIdById = /* @__PURE__ */ new Map();
|
|
3840
|
-
const idByStableId = /* @__PURE__ */ new Map();
|
|
3841
|
-
for (const m of Object.values(allMeta)) {
|
|
3842
|
-
stableIdById.set(m.id, m.stableId);
|
|
3843
|
-
idByStableId.set(m.stableId, m.id);
|
|
3844
|
-
}
|
|
3845
|
-
const entries = [];
|
|
3846
|
-
const expectedSources = /* @__PURE__ */ new Set();
|
|
3847
|
-
for (const targetId of this.linkHost.devicesWithLinks) {
|
|
3848
|
-
const targetMeta = Object.values(allMeta).find((m) => m.id === targetId);
|
|
3849
|
-
if (!targetMeta) continue;
|
|
3850
|
-
const container = targetMeta.parentDeviceId !== null ? stableIdById.get(targetMeta.parentDeviceId) ?? stableIdById.get(targetId) : stableIdById.get(targetId);
|
|
3851
|
-
if (container === void 0) continue;
|
|
3852
|
-
for (const link of targetMeta.deviceLinks ?? []) {
|
|
3853
|
-
const src = link.source;
|
|
3854
|
-
if (src.kind === "expression") {
|
|
3855
|
-
const bindingSourceIds = {};
|
|
3856
|
-
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3857
|
-
if (b.kind === "literal") continue;
|
|
3858
|
-
const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
|
|
3859
|
-
expectedSources.add(wanted);
|
|
3860
|
-
const bid = idByStableId.get(wanted);
|
|
3861
|
-
if (bid !== void 0) bindingSourceIds[name] = bid;
|
|
3862
|
-
}
|
|
3863
|
-
entries.push({
|
|
3864
|
-
targetDeviceId: targetId,
|
|
3865
|
-
link,
|
|
3866
|
-
sourceDeviceId: -1,
|
|
3867
|
-
bindingSourceIds
|
|
3868
|
-
});
|
|
3869
|
-
continue;
|
|
3870
|
-
}
|
|
3871
|
-
if (src.kind === "literal") {
|
|
3872
|
-
entries.push({
|
|
3873
|
-
targetDeviceId: targetId,
|
|
3874
|
-
link,
|
|
3875
|
-
sourceDeviceId: -1
|
|
3876
|
-
});
|
|
3877
|
-
continue;
|
|
3878
|
-
}
|
|
3879
|
-
const wantedStableId = src.kind === "global" ? src.sourceStableId : `${container}-${src.sourceKey}`;
|
|
3880
|
-
expectedSources.add(wantedStableId);
|
|
3881
|
-
const srcId = idByStableId.get(wantedStableId);
|
|
3882
|
-
if (srcId === void 0) continue;
|
|
3883
|
-
entries.push({
|
|
3884
|
-
targetDeviceId: targetId,
|
|
3885
|
-
link,
|
|
3886
|
-
sourceDeviceId: srcId
|
|
3887
|
-
});
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
const { targets, dependents } = buildLinkIndexes(entries);
|
|
3891
|
-
this.linkHost.setLinkIndexes(targets, dependents, expectedSources);
|
|
3892
|
-
};
|
|
3893
3581
|
};
|
|
3894
3582
|
//#endregion
|
|
3895
|
-
//#region src/builtins/device-manager/device-link-resolver.ts
|
|
3896
|
-
/** Returns true when `x` is a non-null, non-array plain object. */
|
|
3897
|
-
function isRecord(x) {
|
|
3898
|
-
return x !== null && typeof x === "object" && !Array.isArray(x);
|
|
3899
|
-
}
|
|
3900
|
-
/**
|
|
3901
|
-
* Resolve one link's transformed value. Field sources read via `readField`
|
|
3902
|
-
* (the caller supplies the sibling lookup — sync, e.g. the in-hub state
|
|
3903
|
-
* mirror); literal sources use their per-device constant and never consult
|
|
3904
|
-
* the reader. Returns `undefined` to skip the overlay (unreadable field).
|
|
3905
|
-
* Pure. Async source reads (`resolveLinkedStatus`'s provider `getStatus`
|
|
3906
|
-
* path) cannot use this helper for field sources — they branch on
|
|
3907
|
-
* `source.kind` directly.
|
|
3908
|
-
*/
|
|
3909
|
-
function resolveLinkValue(link, readField) {
|
|
3910
|
-
const src = link.source;
|
|
3911
|
-
if (src.kind === "expression") return void 0;
|
|
3912
|
-
return require_dist.applyTransform(src.kind === "literal" ? src.value : readField(src.cap, src.fieldPath), link.transform);
|
|
3913
|
-
}
|
|
3914
|
-
/**
|
|
3915
|
-
* Sync-channel expression resolution: bindings read from the mirror via
|
|
3916
|
-
* `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
|
|
3917
|
-
* on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
|
|
3918
|
-
* binding id (dangling source) or a non-primitive read resolves to `null`.
|
|
3919
|
-
* Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
|
|
3920
|
-
* the mirror path is hot and churn-free; the async channel logs. Compile
|
|
3921
|
-
* failures cost one parse ever thanks to the negative LRU compile cache. Pure.
|
|
3922
|
-
*/
|
|
3923
|
-
function resolveExpressionLinkValue(rl, readField, now) {
|
|
3924
|
-
const src = rl.link.source;
|
|
3925
|
-
if (src.kind !== "expression") return void 0;
|
|
3926
|
-
const bindingValues = {};
|
|
3927
|
-
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3928
|
-
if (b.kind === "literal") {
|
|
3929
|
-
bindingValues[name] = b.value;
|
|
3930
|
-
continue;
|
|
3931
|
-
}
|
|
3932
|
-
const bId = rl.bindingSourceIds?.[name];
|
|
3933
|
-
if (bId === void 0) {
|
|
3934
|
-
bindingValues[name] = null;
|
|
3935
|
-
continue;
|
|
3936
|
-
}
|
|
3937
|
-
bindingValues[name] = require_dist.toExpressionValue(readField(bId, b.cap, b.fieldPath)) ?? null;
|
|
3938
|
-
}
|
|
3939
|
-
const result = require_dist.evaluateLinkExpression(src.expr, bindingValues, now);
|
|
3940
|
-
return result.ok ? require_dist.applyTransform(result.value, rl.link.transform) : void 0;
|
|
3941
|
-
}
|
|
3942
|
-
/** Narrow Zod v4's structural `$ZodType` (returned by `.unwrap()`) back to the
|
|
3943
|
-
* concrete classic `z.ZodType`. Every runtime schema is a `z.ZodType`, so this
|
|
3944
|
-
* is a true `instanceof` guard rather than a cast. */
|
|
3945
|
-
function asZodType(schema) {
|
|
3946
|
-
return schema instanceof zod.z.ZodType ? schema : null;
|
|
3947
|
-
}
|
|
3948
|
-
/** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner
|
|
3949
|
-
* type. This lets the repair logic recognise a `TankStatus.nullable()` field
|
|
3950
|
-
* as a ZodObject so it can fill in missing nullable keys. */
|
|
3951
|
-
function unwrapSchema(schema) {
|
|
3952
|
-
if (schema instanceof zod.z.ZodNullable || schema instanceof zod.z.ZodOptional || schema instanceof zod.z.ZodDefault) {
|
|
3953
|
-
const inner = asZodType(schema.unwrap());
|
|
3954
|
-
return inner ? unwrapSchema(inner) : schema;
|
|
3955
|
-
}
|
|
3956
|
-
return schema;
|
|
3957
|
-
}
|
|
3958
|
-
/** For a ZodObject schema, ensure every nullable key present under `value` is
|
|
3959
|
-
* filled — a missing key whose field accepts `null` is set to `null`. Generic
|
|
3960
|
-
* repair for the common "set one leaf of a previously-null structured field"
|
|
3961
|
-
* case (e.g. TankStatus.level). Recurses into nested object fields. */
|
|
3962
|
-
function fillNullableDefaults(schema, value) {
|
|
3963
|
-
const inner = unwrapSchema(schema);
|
|
3964
|
-
if (!(inner instanceof zod.z.ZodObject) || !isRecord(value)) return value;
|
|
3965
|
-
const shape = inner.shape;
|
|
3966
|
-
const out = { ...value };
|
|
3967
|
-
for (const [key, field] of Object.entries(shape)) if (out[key] === void 0) {
|
|
3968
|
-
if (field.safeParse(null).success) out[key] = null;
|
|
3969
|
-
} else out[key] = fillNullableDefaults(field, out[key]);
|
|
3970
|
-
return out;
|
|
3971
|
-
}
|
|
3972
|
-
/**
|
|
3973
|
-
* Group item-targeted links (`target.itemKey` set) by itemKey and upsert each
|
|
3974
|
-
* group as ONE item into the status' item array (see
|
|
3975
|
-
* `CapabilityStatusItemArray`). A NEW item is seeded from `emptyItem` with
|
|
3976
|
-
* `keyField` (and `labelField`, when declared) set to the itemKey; an
|
|
3977
|
-
* EXISTING item (matched by `keyField`) is cloned and overlaid in place, so
|
|
3978
|
-
* native fields a link doesn't touch are preserved. Each upserted item is
|
|
3979
|
-
* validated against `itemSchema` — an invalid item is skipped WITHOUT
|
|
3980
|
-
* discarding its valid siblings. Returns the new draft and whether anything
|
|
3981
|
-
* was applied. Pure + immutable (base array/items are never mutated).
|
|
3982
|
-
*/
|
|
3983
|
-
function upsertItemArrayLinks(draft, grouped, itemArray) {
|
|
3984
|
-
const arrRaw = require_dist.getByPath(draft, itemArray.path);
|
|
3985
|
-
const arr = Array.isArray(arrRaw) ? [...arrRaw] : [];
|
|
3986
|
-
let touched = false;
|
|
3987
|
-
for (const [itemKey, group] of grouped) {
|
|
3988
|
-
const idx = arr.findIndex((el) => isRecord(el) && el[itemArray.keyField] === itemKey);
|
|
3989
|
-
const existing = idx >= 0 ? arr[idx] : null;
|
|
3990
|
-
let item = isRecord(existing) ? { ...existing } : {
|
|
3991
|
-
...itemArray.emptyItem,
|
|
3992
|
-
[itemArray.keyField]: itemKey,
|
|
3993
|
-
...itemArray.labelField !== void 0 ? { [itemArray.labelField]: itemKey } : {}
|
|
3994
|
-
};
|
|
3995
|
-
let itemTouched = false;
|
|
3996
|
-
for (const { link, sourceValue } of group) {
|
|
3997
|
-
if (sourceValue === void 0) continue;
|
|
3998
|
-
item = require_dist.setByPath(item, link.target.fieldPath, sourceValue);
|
|
3999
|
-
itemTouched = true;
|
|
4000
|
-
}
|
|
4001
|
-
if (!itemTouched) continue;
|
|
4002
|
-
const repaired = fillNullableDefaults(itemArray.itemSchema, item);
|
|
4003
|
-
const parsed = itemArray.itemSchema.safeParse(repaired);
|
|
4004
|
-
if (!parsed.success) continue;
|
|
4005
|
-
if (isRecord(parsed.data)) item = parsed.data;
|
|
4006
|
-
if (idx >= 0) arr[idx] = item;
|
|
4007
|
-
else arr.push(item);
|
|
4008
|
-
touched = true;
|
|
4009
|
-
}
|
|
4010
|
-
if (!touched) return {
|
|
4011
|
-
draft,
|
|
4012
|
-
touched: false
|
|
4013
|
-
};
|
|
4014
|
-
return {
|
|
4015
|
-
draft: require_dist.setByPath(draft, itemArray.path, arr),
|
|
4016
|
-
touched: true
|
|
4017
|
-
};
|
|
4018
|
-
}
|
|
4019
|
-
/**
|
|
4020
|
-
* Overlay transformed source values onto `base` by dot-path, then validate the
|
|
4021
|
-
* result against the target cap's `statusSchema`. On validation failure the
|
|
4022
|
-
* overlay is discarded and `base` is returned unchanged (a misconfigured link
|
|
4023
|
-
* must never corrupt a cap response). Pure — all I/O happens in the caller.
|
|
4024
|
-
*
|
|
4025
|
-
* Item-array grouping (P2b): when the cap declares `status.itemArray`, links
|
|
4026
|
-
* carrying a `target.itemKey` are grouped per key and upserted as items into
|
|
4027
|
-
* the array (see `upsertItemArrayLinks`); their `fieldPath` is relative to
|
|
4028
|
-
* ONE item ('level', 'status', 'label'). Links WITHOUT an itemKey keep the
|
|
4029
|
-
* scalar dot-path behavior unchanged. A link with an itemKey on a cap that
|
|
4030
|
-
* declares NO `itemArray` is dropped (its item-relative path must never be
|
|
4031
|
-
* scalar-applied to the status root).
|
|
4032
|
-
*/
|
|
4033
|
-
function mergeLinkedStatus(base, resolved, statusSchema, itemArray) {
|
|
4034
|
-
let draft = base;
|
|
4035
|
-
let touched = false;
|
|
4036
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
4037
|
-
for (const entry of resolved) {
|
|
4038
|
-
const itemKey = entry.link.target.itemKey;
|
|
4039
|
-
if (itemKey !== void 0) {
|
|
4040
|
-
if (!itemArray) continue;
|
|
4041
|
-
const group = grouped.get(itemKey);
|
|
4042
|
-
if (group) group.push(entry);
|
|
4043
|
-
else grouped.set(itemKey, [entry]);
|
|
4044
|
-
continue;
|
|
4045
|
-
}
|
|
4046
|
-
if (entry.sourceValue === void 0) continue;
|
|
4047
|
-
draft = require_dist.setByPath(draft, entry.link.target.fieldPath, entry.sourceValue);
|
|
4048
|
-
touched = true;
|
|
4049
|
-
}
|
|
4050
|
-
if (itemArray && grouped.size > 0) {
|
|
4051
|
-
const upserted = upsertItemArrayLinks(draft, grouped, itemArray);
|
|
4052
|
-
draft = upserted.draft;
|
|
4053
|
-
touched = touched || upserted.touched;
|
|
4054
|
-
}
|
|
4055
|
-
if (!touched) return base;
|
|
4056
|
-
if (!statusSchema) return draft;
|
|
4057
|
-
const repaired = fillNullableDefaults(statusSchema, draft);
|
|
4058
|
-
const parsed = statusSchema.safeParse(repaired);
|
|
4059
|
-
if (!parsed.success) return base;
|
|
4060
|
-
return isRecord(parsed.data) ? parsed.data : base;
|
|
4061
|
-
}
|
|
4062
|
-
//#endregion
|
|
4063
3583
|
//#region src/builtins/device-manager/device-state-mirror.ts
|
|
4064
3584
|
/**
|
|
4065
3585
|
* Hub-side runtime-state mirror for the device-manager addon.
|
|
4066
3586
|
*
|
|
4067
3587
|
* `DeviceStateMirror` owns the per-device cap-keyed slice mirror and the
|
|
4068
|
-
* debounced disk-write coalescer
|
|
4069
|
-
*
|
|
4070
|
-
* `
|
|
4071
|
-
*
|
|
4072
|
-
*
|
|
4073
|
-
*
|
|
4074
|
-
*
|
|
3588
|
+
* debounced disk-write coalescer. It mirrors every `deviceState.setCapSlice`
|
|
3589
|
+
* write, emits `DeviceStateChanged`, and coalesces frequent writes into one
|
|
3590
|
+
* `writeDeviceRuntimeState` per debounce window.
|
|
3591
|
+
*
|
|
3592
|
+
* It used to do one more thing: overlay each emitted slice with cross-device
|
|
3593
|
+
* LINKED values, walk a `linkDependents` reverse-index and re-emit for every
|
|
3594
|
+
* dependent target, guarded by a `lastEmittedOverlay` churn map. Wiring was
|
|
3595
|
+
* deleted on 2026-08-08 (0 links across 302 devices), so the mirror emits what
|
|
3596
|
+
* the provider wrote — one writer per field, which is what D62 asks for
|
|
3597
|
+
* everywhere else.
|
|
4075
3598
|
*/
|
|
4076
3599
|
var DeviceStateMirror = class DeviceStateMirror {
|
|
4077
3600
|
ctx;
|
|
4078
|
-
linkHost;
|
|
4079
3601
|
/**
|
|
4080
3602
|
* Hub-side mirror of every device's cap-keyed runtime state.
|
|
4081
3603
|
* Key: deviceId. Value: per-cap slice map. Empty by default —
|
|
@@ -4089,11 +3611,8 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4089
3611
|
*/
|
|
4090
3612
|
runtimeStateDebounce = /* @__PURE__ */ new Map();
|
|
4091
3613
|
static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
|
|
4092
|
-
|
|
4093
|
-
lastEmittedOverlay = /* @__PURE__ */ new Map();
|
|
4094
|
-
constructor(ctx, linkHost) {
|
|
3614
|
+
constructor(ctx) {
|
|
4095
3615
|
this.ctx = ctx;
|
|
4096
|
-
this.linkHost = linkHost;
|
|
4097
3616
|
}
|
|
4098
3617
|
/**
|
|
4099
3618
|
* Single-cap mirror update — diff against the current mirror,
|
|
@@ -4110,9 +3629,7 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4110
3629
|
const prior = perCap.get(capName);
|
|
4111
3630
|
if (prior && shallowEqual(prior, slice)) return;
|
|
4112
3631
|
perCap.set(capName, { ...slice });
|
|
4113
|
-
this.
|
|
4114
|
-
const deps = this.linkHost.linkDependents.get(`${deviceId}:${capName}`);
|
|
4115
|
-
if (deps) for (const d of deps) this.emitOverlayed(d.targetDeviceId, d.targetCap);
|
|
3632
|
+
this.emitStateChanged(deviceId, capName, { ...slice });
|
|
4116
3633
|
}
|
|
4117
3634
|
/**
|
|
4118
3635
|
* Debounced disk writer. Coalesces frequent writes (motion phase
|
|
@@ -4244,68 +3761,17 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4244
3761
|
for (const [k, v] of perCap) out[k] = { ...v };
|
|
4245
3762
|
return out;
|
|
4246
3763
|
}
|
|
4247
|
-
/**
|
|
4248
|
-
*
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
* target, the slice is SYNTHESIZED: the merge base is seeded from the
|
|
4253
|
-
* cap's declared `status.empty` default; a failed/empty synthesize
|
|
4254
|
-
* returns null (no phantom slices). The disk writer must NOT use this
|
|
4255
|
-
* method; it must persist raw provider truth via snapshotForDevice.
|
|
4256
|
-
*/
|
|
4257
|
-
overlayedSlice(deviceId, cap) {
|
|
4258
|
-
const raw = this.stateMirror.get(deviceId)?.get(cap) ?? null;
|
|
4259
|
-
const links = this.linkHost.linkTargets.get(`${deviceId}:${cap}`);
|
|
4260
|
-
if (!links || links.length === 0) return raw ? { ...raw } : null;
|
|
4261
|
-
const now = Date.now();
|
|
4262
|
-
const resolved = links.map((rl) => ({
|
|
4263
|
-
link: rl.link,
|
|
4264
|
-
sourceValue: rl.link.source.kind === "expression" ? resolveExpressionLinkValue(rl, (srcDeviceId, srcCap, fieldPath) => require_dist.getByPath(this.stateMirror.get(srcDeviceId)?.get(srcCap), fieldPath), now) : resolveLinkValue(rl.link, (srcCap, fieldPath) => require_dist.getByPath(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
4265
|
-
}));
|
|
4266
|
-
const capStatus = this.linkHost.capabilityRegistry?.getDefinition(cap)?.status;
|
|
4267
|
-
const schema = capStatus?.schema;
|
|
4268
|
-
const empty = capStatus?.empty;
|
|
4269
|
-
const base = raw ? { ...raw } : empty !== null && typeof empty === "object" && !Array.isArray(empty) ? { ...empty } : {};
|
|
4270
|
-
const merged = mergeLinkedStatus(base, resolved, schema, capStatus?.itemArray);
|
|
4271
|
-
if (!raw && merged === base) return null;
|
|
4272
|
-
return merged;
|
|
3764
|
+
/** One cap's mirrored slice, cloned. Null when the device has never written
|
|
3765
|
+
* it — callers read that as "the cap has no state", never as an empty slice. */
|
|
3766
|
+
capSlice(deviceId, cap) {
|
|
3767
|
+
const raw = this.stateMirror.get(deviceId)?.get(cap);
|
|
3768
|
+
return raw ? { ...raw } : null;
|
|
4273
3769
|
}
|
|
4274
|
-
/**
|
|
4275
|
-
*
|
|
4276
|
-
|
|
4277
|
-
* getAllSnapshots) so callers see overlayed values. SYNTHESIZE-only
|
|
4278
|
-
* link-target caps (no base mirror slice) are unioned into the iterated
|
|
4279
|
-
* key set so they appear at warm-load — `overlayedSlice` builds them from
|
|
4280
|
-
* the cap's `status.empty` (and drops them when nothing resolves). The
|
|
4281
|
-
* debounced disk writer must continue to call snapshotForDevice (raw truth).
|
|
4282
|
-
*/
|
|
4283
|
-
snapshotForDeviceOverlayed(deviceId) {
|
|
4284
|
-
const perCap = this.stateMirror.get(deviceId);
|
|
4285
|
-
const capNames = new Set(perCap ? perCap.keys() : []);
|
|
4286
|
-
const prefix = `${deviceId}:`;
|
|
4287
|
-
for (const key of this.linkHost.linkTargets.keys()) if (key.startsWith(prefix)) capNames.add(key.slice(prefix.length));
|
|
3770
|
+
/** Whole-system mirror dump. Backs `deviceState.getAllSnapshots` — one
|
|
3771
|
+
* round-trip for a warm boot instead of N per-device calls. */
|
|
3772
|
+
allSnapshots() {
|
|
4288
3773
|
const out = {};
|
|
4289
|
-
for (const
|
|
4290
|
-
const s = this.overlayedSlice(deviceId, capName);
|
|
4291
|
-
if (s) out[capName] = s;
|
|
4292
|
-
}
|
|
4293
|
-
return out;
|
|
4294
|
-
}
|
|
4295
|
-
/** Whole-system mirror dump (overlaid). Backs `deviceState.getAllSnapshots`.
|
|
4296
|
-
* Devices that only exist as link TARGETS (no mirror slice yet) are
|
|
4297
|
-
* included via the link reverse-index so their synthesized caps appear. */
|
|
4298
|
-
allSnapshotsOverlayed() {
|
|
4299
|
-
const deviceIds = new Set(this.stateMirror.keys());
|
|
4300
|
-
for (const key of this.linkHost.linkTargets.keys()) {
|
|
4301
|
-
const id = Number(key.slice(0, key.indexOf(":")));
|
|
4302
|
-
if (Number.isFinite(id)) deviceIds.add(id);
|
|
4303
|
-
}
|
|
4304
|
-
const out = {};
|
|
4305
|
-
for (const deviceId of deviceIds) {
|
|
4306
|
-
const dev = this.snapshotForDeviceOverlayed(deviceId);
|
|
4307
|
-
if (this.stateMirror.has(deviceId) || Object.keys(dev).length > 0) out[String(deviceId)] = dev;
|
|
4308
|
-
}
|
|
3774
|
+
for (const deviceId of this.stateMirror.keys()) out[String(deviceId)] = this.snapshotForDevice(deviceId);
|
|
4309
3775
|
return out;
|
|
4310
3776
|
}
|
|
4311
3777
|
emitStateChanged(deviceId, capName, slice) {
|
|
@@ -4324,24 +3790,6 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4324
3790
|
}
|
|
4325
3791
|
});
|
|
4326
3792
|
}
|
|
4327
|
-
/** Emit DeviceStateChanged for (deviceId, cap) using the OVERLAID slice,
|
|
4328
|
-
* skipping when the overlay is unchanged since the last emit (loop/churn
|
|
4329
|
-
* guard). Used for the written pair AND its dependent targets. */
|
|
4330
|
-
emitOverlayed(deviceId, cap) {
|
|
4331
|
-
const slice = this.overlayedSlice(deviceId, cap);
|
|
4332
|
-
if (!slice) return;
|
|
4333
|
-
const key = `${deviceId}:${cap}`;
|
|
4334
|
-
const prev = this.lastEmittedOverlay.get(key);
|
|
4335
|
-
if (prev && shallowEqual(prev, slice)) return;
|
|
4336
|
-
this.lastEmittedOverlay.set(key, slice);
|
|
4337
|
-
this.emitStateChanged(deviceId, cap, slice);
|
|
4338
|
-
}
|
|
4339
|
-
/** Drop a removed device's overlay-emit guard entries (keyed
|
|
4340
|
-
* `${deviceId}:${cap}`) so the map doesn't retain rows for a removed device.
|
|
4341
|
-
* Called from `removeDevice`. */
|
|
4342
|
-
dropDeviceOverlays(deviceId) {
|
|
4343
|
-
for (const overlayKey of this.lastEmittedOverlay.keys()) if (overlayKey.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(overlayKey);
|
|
4344
|
-
}
|
|
4345
3793
|
/** Flush every pending debounced disk write (graceful shutdown). Clears the
|
|
4346
3794
|
* debounce slots after awaiting in-flight + scheduled writes so shutdown is
|
|
4347
3795
|
* lossless. */
|
|
@@ -4370,11 +3818,15 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4370
3818
|
//#endregion
|
|
4371
3819
|
//#region src/builtins/device-manager/device-sync-resolvers.ts
|
|
4372
3820
|
/**
|
|
4373
|
-
* Synchronous ownership / native-cap resolvers
|
|
4374
|
-
*
|
|
4375
|
-
*
|
|
4376
|
-
*
|
|
4377
|
-
* the
|
|
3821
|
+
* Synchronous ownership / native-cap resolvers for the device-manager provider.
|
|
3822
|
+
* Free functions over a `ProviderContext` (DI); these back
|
|
3823
|
+
* `CapabilityRegistry`'s native fallback.
|
|
3824
|
+
*
|
|
3825
|
+
* This file also held `resolveLinkedStatus`, the read-time cross-device link
|
|
3826
|
+
* overlay. Wiring was deleted on 2026-08-08 — 0 links across 302 devices, a
|
|
3827
|
+
* month after the tab shipped — so the overlay, its re-entrancy guard and its
|
|
3828
|
+
* per-call source-read memo went with it. A cross-device derivation now
|
|
3829
|
+
* produces a DEVICE, not a field on somebody else's cap.
|
|
4378
3830
|
*/
|
|
4379
3831
|
/**
|
|
4380
3832
|
* Sync ownership lookup backing persistence fallbacks (e.g. remove()
|
|
@@ -4416,133 +3868,6 @@ function resolveNativeCapOwnerSync(pctx, capName, deviceId) {
|
|
|
4416
3868
|
};
|
|
4417
3869
|
return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
|
|
4418
3870
|
}
|
|
4419
|
-
/**
|
|
4420
|
-
* Read-time overlay: merge cross-device linked values onto a target cap's
|
|
4421
|
-
* status. Returns null when the device has no links for `cap` (caller uses
|
|
4422
|
-
* the base status untouched). Reads each source cap via the hub registry's
|
|
4423
|
-
* `getProviderForDevice` (routes cross-process); merge is pure.
|
|
4424
|
-
*/
|
|
4425
|
-
async function resolveLinkedStatus(pctx, input) {
|
|
4426
|
-
const { deviceId, cap } = input;
|
|
4427
|
-
if (!pctx.host.devicesWithLinks.has(deviceId)) return null;
|
|
4428
|
-
const inFlightKey = `${deviceId}:${cap}`;
|
|
4429
|
-
if (pctx.host.linkResolveInFlight.has(inFlightKey)) return null;
|
|
4430
|
-
pctx.host.linkResolveInFlight.add(inFlightKey);
|
|
4431
|
-
try {
|
|
4432
|
-
return await resolveLinkedStatusInner(pctx, input);
|
|
4433
|
-
} finally {
|
|
4434
|
-
pctx.host.linkResolveInFlight.delete(inFlightKey);
|
|
4435
|
-
}
|
|
4436
|
-
}
|
|
4437
|
-
/** Body of `resolveLinkedStatus` — see the wrapper for the re-entrancy guard. */
|
|
4438
|
-
async function resolveLinkedStatusInner(pctx, input) {
|
|
4439
|
-
const { deviceId, cap, baseStatus } = input;
|
|
4440
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
4441
|
-
if (!persisted) return null;
|
|
4442
|
-
const links = (persisted.meta.deviceLinks ?? []).filter((l) => l.target.cap === cap);
|
|
4443
|
-
if (links.length === 0) return null;
|
|
4444
|
-
const capRegistry = pctx.host.capabilityRegistry;
|
|
4445
|
-
const capStatus = capRegistry?.getDefinition(cap)?.status;
|
|
4446
|
-
const schema = capStatus?.schema;
|
|
4447
|
-
if (!schema) return null;
|
|
4448
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
4449
|
-
let containerStableId = persisted.stableId;
|
|
4450
|
-
const parentId = persisted.meta.parentDeviceId;
|
|
4451
|
-
if (parentId !== null) {
|
|
4452
|
-
const parentMeta = allMeta[String(parentId)];
|
|
4453
|
-
if (parentMeta) containerStableId = parentMeta.stableId;
|
|
4454
|
-
}
|
|
4455
|
-
const now = Date.now();
|
|
4456
|
-
const statusMemo = /* @__PURE__ */ new Map();
|
|
4457
|
-
const readSourceStatus = async (srcId, srcCap) => {
|
|
4458
|
-
const key = `${srcId}:${srcCap}`;
|
|
4459
|
-
const cached = statusMemo.get(key);
|
|
4460
|
-
if (cached !== void 0) return cached;
|
|
4461
|
-
let result;
|
|
4462
|
-
try {
|
|
4463
|
-
const srcProvider = capRegistry?.getProviderForDevice(srcCap, srcId);
|
|
4464
|
-
result = {
|
|
4465
|
-
status: typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0,
|
|
4466
|
-
ok: true
|
|
4467
|
-
};
|
|
4468
|
-
} catch (err) {
|
|
4469
|
-
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
4470
|
-
tags: {
|
|
4471
|
-
deviceId,
|
|
4472
|
-
capName: cap
|
|
4473
|
-
},
|
|
4474
|
-
meta: {
|
|
4475
|
-
sourceCap: srcCap,
|
|
4476
|
-
sourceId: srcId,
|
|
4477
|
-
error: err instanceof Error ? err.message : String(err)
|
|
4478
|
-
}
|
|
4479
|
-
});
|
|
4480
|
-
result = {
|
|
4481
|
-
status: void 0,
|
|
4482
|
-
ok: false
|
|
4483
|
-
};
|
|
4484
|
-
}
|
|
4485
|
-
statusMemo.set(key, result);
|
|
4486
|
-
return result;
|
|
4487
|
-
};
|
|
4488
|
-
const resolved = [];
|
|
4489
|
-
for (const link of links) {
|
|
4490
|
-
const src = link.source;
|
|
4491
|
-
if (src.kind === "literal") {
|
|
4492
|
-
resolved.push({
|
|
4493
|
-
link,
|
|
4494
|
-
sourceValue: require_dist.applyTransform(src.value, link.transform)
|
|
4495
|
-
});
|
|
4496
|
-
continue;
|
|
4497
|
-
}
|
|
4498
|
-
if (src.kind === "expression") {
|
|
4499
|
-
const bindingValues = {};
|
|
4500
|
-
for (const [name, b] of Object.entries(src.bindings)) {
|
|
4501
|
-
if (b.kind === "literal") {
|
|
4502
|
-
bindingValues[name] = b.value;
|
|
4503
|
-
continue;
|
|
4504
|
-
}
|
|
4505
|
-
const bId = b.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(b.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, b.sourceKey, allMeta);
|
|
4506
|
-
if (bId === null) {
|
|
4507
|
-
bindingValues[name] = null;
|
|
4508
|
-
continue;
|
|
4509
|
-
}
|
|
4510
|
-
bindingValues[name] = require_dist.toExpressionValue(require_dist.getByPath((await readSourceStatus(bId, b.cap)).status, b.fieldPath)) ?? null;
|
|
4511
|
-
}
|
|
4512
|
-
const result = require_dist.evaluateLinkExpression(src.expr, bindingValues, now);
|
|
4513
|
-
if (result.ok) resolved.push({
|
|
4514
|
-
link,
|
|
4515
|
-
sourceValue: require_dist.applyTransform(result.value, link.transform)
|
|
4516
|
-
});
|
|
4517
|
-
else pctx.host.ctx.logger.warn("resolveLinkedStatus: expression skipped", {
|
|
4518
|
-
tags: {
|
|
4519
|
-
deviceId,
|
|
4520
|
-
capName: cap
|
|
4521
|
-
},
|
|
4522
|
-
meta: {
|
|
4523
|
-
linkId: link.id,
|
|
4524
|
-
error: result.error
|
|
4525
|
-
}
|
|
4526
|
-
});
|
|
4527
|
-
continue;
|
|
4528
|
-
}
|
|
4529
|
-
const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
|
|
4530
|
-
if (srcId === null) continue;
|
|
4531
|
-
const read = await readSourceStatus(srcId, src.cap);
|
|
4532
|
-
if (!read.ok) continue;
|
|
4533
|
-
const raw = require_dist.getByPath(read.status, src.fieldPath);
|
|
4534
|
-
resolved.push({
|
|
4535
|
-
link,
|
|
4536
|
-
sourceValue: require_dist.applyTransform(raw, link.transform)
|
|
4537
|
-
});
|
|
4538
|
-
}
|
|
4539
|
-
if (resolved.length === 0) return null;
|
|
4540
|
-
const synthesizing = !isRecord$1(baseStatus);
|
|
4541
|
-
const base = isRecord$1(baseStatus) ? baseStatus : isRecord$1(capStatus.empty) ? { ...capStatus.empty } : {};
|
|
4542
|
-
const merged = mergeLinkedStatus(base, resolved, schema, capStatus.itemArray);
|
|
4543
|
-
if (synthesizing && merged === base) return null;
|
|
4544
|
-
return merged;
|
|
4545
|
-
}
|
|
4546
3871
|
//#endregion
|
|
4547
3872
|
//#region src/builtins/device-manager/device-manager.addon.ts
|
|
4548
3873
|
var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
@@ -4595,100 +3920,24 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
4595
3920
|
* the D3 re-handshake after device restore is the reliable replacement.
|
|
4596
3921
|
*/
|
|
4597
3922
|
remoteNativeCaps = /* @__PURE__ */ new Map();
|
|
4598
|
-
/** Device ids that currently have at least one `deviceLinks` entry. O(1) gate
|
|
4599
|
-
* consulted by `resolveLinkedStatus` so the cap-mount overlay is a no-op for
|
|
4600
|
-
* the vast majority of devices that have no links. Rebuilt on restore. */
|
|
4601
|
-
devicesWithLinks = /* @__PURE__ */ new Set();
|
|
4602
|
-
/** Whether a device currently has cross-device links. */
|
|
4603
|
-
devicesWithLinksHas(deviceId) {
|
|
4604
|
-
return this.devicesWithLinks.has(deviceId);
|
|
4605
|
-
}
|
|
4606
|
-
/** `${targetDeviceId}:${targetCap}` → resolved links feeding that cap's slice. */
|
|
4607
|
-
linkTargets = /* @__PURE__ */ new Map();
|
|
4608
|
-
/** `${sourceDeviceId}:${sourceCap}` → targets to recompute when that source changes. */
|
|
4609
|
-
linkDependents = /* @__PURE__ */ new Map();
|
|
4610
|
-
/** Expected source `stableId`s (`${container}-${sourceKey}` for sibling
|
|
4611
|
-
* sources, the full `sourceStableId` for global sources) across all links,
|
|
4612
|
-
* resolved or not — gates the `registerDevice` rebuild so only a registering
|
|
4613
|
-
* device that IS a link source triggers a reindex (not every boot restore). */
|
|
4614
|
-
expectedSourceStableIds = /* @__PURE__ */ new Set();
|
|
4615
|
-
/** In-flight `${deviceId}:${cap}` pairs of `resolveLinkedStatus` — the P2e
|
|
4616
|
-
* defensive re-entrancy guard bounding link cycles at resolve time. Mutated
|
|
4617
|
-
* in place (never reassigned), so the host exposes a direct reference. */
|
|
4618
|
-
linkResolveInFlight = /* @__PURE__ */ new Set();
|
|
4619
|
-
/** Test/diagnostic accessors. */
|
|
4620
|
-
linkTargetKeys() {
|
|
4621
|
-
return [...this.linkTargets.keys()];
|
|
4622
|
-
}
|
|
4623
|
-
linkDependentsKeys() {
|
|
4624
|
-
return [...this.linkDependents.keys()];
|
|
4625
|
-
}
|
|
4626
|
-
/** Replace the cross-device link reverse-index maps in one atomic swap.
|
|
4627
|
-
* Sole writer of `linkTargets` / `linkDependents` / `expectedSourceStableIds`;
|
|
4628
|
-
* called by `DeviceMetaStore.rebuildLinkDependents` via the `LinkIndexHost`. */
|
|
4629
|
-
setLinkIndexes(targets, dependents, expectedSourceStableIds) {
|
|
4630
|
-
this.linkTargets = targets;
|
|
4631
|
-
this.linkDependents = dependents;
|
|
4632
|
-
this.expectedSourceStableIds = expectedSourceStableIds;
|
|
4633
|
-
}
|
|
4634
|
-
/** Build the `LinkIndexHost` the meta store mutates — exposes this addon's
|
|
4635
|
-
* `devicesWithLinks` gate (read) + `setLinkIndexes` (write) without leaking
|
|
4636
|
-
* the private maps. */
|
|
4637
|
-
get linkIndexHost() {
|
|
4638
|
-
return {
|
|
4639
|
-
devicesWithLinks: this.devicesWithLinks,
|
|
4640
|
-
setLinkIndexes: (targets, dependents, expected) => this.setLinkIndexes(targets, dependents, expected)
|
|
4641
|
-
};
|
|
4642
|
-
}
|
|
4643
3923
|
/** The runtime-state mirror, asserted present (constructed in onInitialize). */
|
|
4644
3924
|
get stateMirror() {
|
|
4645
3925
|
if (!this.stateMirrorImpl) throw new Error("DeviceManagerAddon: state mirror accessed before onInitialize");
|
|
4646
3926
|
return this.stateMirrorImpl;
|
|
4647
3927
|
}
|
|
4648
|
-
/** Build the `LinkOverlayHost` the state mirror reads when overlaying linked
|
|
4649
|
-
* values — exposes the link reverse-index maps + registry via LIVE thunks
|
|
4650
|
-
* (the maps are reassigned on every `rebuildLinkDependents`). Arrow thunks
|
|
4651
|
-
* capture `this` lexically so the getters never go stale. */
|
|
4652
|
-
get linkOverlayHost() {
|
|
4653
|
-
const linkTargets = () => this.linkTargets;
|
|
4654
|
-
const linkDependents = () => this.linkDependents;
|
|
4655
|
-
const capabilityRegistry = () => this.capabilityRegistry;
|
|
4656
|
-
return {
|
|
4657
|
-
get linkTargets() {
|
|
4658
|
-
return linkTargets();
|
|
4659
|
-
},
|
|
4660
|
-
get linkDependents() {
|
|
4661
|
-
return linkDependents();
|
|
4662
|
-
},
|
|
4663
|
-
get capabilityRegistry() {
|
|
4664
|
-
return capabilityRegistry();
|
|
4665
|
-
}
|
|
4666
|
-
};
|
|
4667
|
-
}
|
|
4668
3928
|
/** Build the `ProviderHost` the extracted provider-method modules
|
|
4669
3929
|
* (`device-meta-actions`, `device-queries`) reach — exposes the addon's
|
|
4670
3930
|
* private state/methods they need without leaking the class internals.
|
|
4671
3931
|
*
|
|
4672
|
-
* `
|
|
4673
|
-
*
|
|
4674
|
-
* reference would go stale. `devicesWithLinks` / `remoteNativeCaps` are
|
|
4675
|
-
* mutated in place (never reassigned) so a direct reference preserves the
|
|
4676
|
-
* original live semantics. The overlay-emit guard now lives in the state
|
|
4677
|
-
* mirror — pruning is routed through `dropDeviceOverlays`. */
|
|
3932
|
+
* `remoteNativeCaps` is mutated in place (never reassigned) so a direct
|
|
3933
|
+
* reference preserves the live semantics. */
|
|
4678
3934
|
get providerHost() {
|
|
4679
3935
|
const capabilityRegistry = () => this.capabilityRegistry;
|
|
4680
|
-
const expectedSourceStableIds = () => this.expectedSourceStableIds;
|
|
4681
3936
|
return {
|
|
4682
3937
|
ctx: this.ctx,
|
|
4683
3938
|
get capabilityRegistry() {
|
|
4684
3939
|
return capabilityRegistry();
|
|
4685
3940
|
},
|
|
4686
|
-
devicesWithLinks: this.devicesWithLinks,
|
|
4687
|
-
get expectedSourceStableIds() {
|
|
4688
|
-
return expectedSourceStableIds();
|
|
4689
|
-
},
|
|
4690
|
-
linkResolveInFlight: this.linkResolveInFlight,
|
|
4691
|
-
dropDeviceOverlays: (deviceId) => this.stateMirror.dropDeviceOverlays(deviceId),
|
|
4692
3941
|
remoteNativeCaps: this.remoteNativeCaps,
|
|
4693
3942
|
seedMirror: (deviceId, blob) => this.stateMirror.seedMirror(deviceId, blob),
|
|
4694
3943
|
withResetSessionProbe: (blob) => this.stateMirror.withResetSessionProbe(blob),
|
|
@@ -4736,15 +3985,12 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
4736
3985
|
}
|
|
4737
3986
|
}))).filter((id) => id !== null);
|
|
4738
3987
|
}
|
|
4739
|
-
/** Build the dependency context the extracted binding resolvers consume.
|
|
4740
|
-
* `devicesWithLinks` is mutated in place (never reassigned) so the direct
|
|
4741
|
-
* reference stays live — it gates the virtual `linked` binding step. */
|
|
3988
|
+
/** Build the dependency context the extracted binding resolvers consume. */
|
|
4742
3989
|
get bindingsDeps() {
|
|
4743
3990
|
return {
|
|
4744
3991
|
ctx: this.ctx,
|
|
4745
3992
|
capabilityRegistry: this.capabilityRegistry,
|
|
4746
|
-
remoteNativeCaps: this.remoteNativeCaps
|
|
4747
|
-
devicesWithLinks: this.devicesWithLinks
|
|
3993
|
+
remoteNativeCaps: this.remoteNativeCaps
|
|
4748
3994
|
};
|
|
4749
3995
|
}
|
|
4750
3996
|
async getBindings(input) {
|
|
@@ -4800,21 +4046,16 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
4800
4046
|
if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
|
|
4801
4047
|
return ops;
|
|
4802
4048
|
};
|
|
4803
|
-
const metaStore = new DeviceMetaStore(settings, registry
|
|
4804
|
-
this.stateMirrorImpl = new DeviceStateMirror(this.ctx
|
|
4049
|
+
const metaStore = new DeviceMetaStore(settings, registry);
|
|
4050
|
+
this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
|
|
4805
4051
|
const stateMirror = this.stateMirrorImpl;
|
|
4806
4052
|
const readMeta = metaStore.readMeta;
|
|
4807
4053
|
const resolvePersistedById = metaStore.resolvePersistedById;
|
|
4808
|
-
const rebuildLinkDependents = metaStore.rebuildLinkDependents;
|
|
4809
4054
|
const idToAddonId = metaStore.idToAddonId;
|
|
4810
4055
|
{
|
|
4811
4056
|
const meta = await readMeta();
|
|
4812
|
-
for (const m of Object.values(meta))
|
|
4813
|
-
idToAddonId.set(m.id, m.addonId);
|
|
4814
|
-
if (Array.isArray(m.deviceLinks) && m.deviceLinks.length > 0) this.devicesWithLinks.add(m.id);
|
|
4815
|
-
}
|
|
4057
|
+
for (const m of Object.values(meta)) idToAddonId.set(m.id, m.addonId);
|
|
4816
4058
|
}
|
|
4817
|
-
await rebuildLinkDependents();
|
|
4818
4059
|
const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, settings, this.ctx, deviceId, integrationId);
|
|
4819
4060
|
const pctx = {
|
|
4820
4061
|
host: this.providerHost,
|
|
@@ -4831,7 +4072,6 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
4831
4072
|
const provider = {
|
|
4832
4073
|
resolveDeviceOwnerSync: (deviceId) => resolveDeviceOwnerSync(pctx, deviceId),
|
|
4833
4074
|
resolveNativeCapOwnerSync: (capName, deviceId) => resolveNativeCapOwnerSync(pctx, capName, deviceId),
|
|
4834
|
-
resolveLinkedStatus: (input) => resolveLinkedStatus(pctx, input),
|
|
4835
4075
|
allocateDeviceId: (input) => allocateDeviceId(pctx, input),
|
|
4836
4076
|
registerDevice: (input) => registerDevice(pctx, input),
|
|
4837
4077
|
removeDevice: (input) => removeDevice(pctx, input),
|
|
@@ -4845,7 +4085,6 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
4845
4085
|
setLinkDeviceId: (input) => setLinkDeviceId(pctx, input),
|
|
4846
4086
|
setPrimaryChildEntityId: (input) => setPrimaryChildEntityId(pctx, input),
|
|
4847
4087
|
setChildLayout: (input) => setChildLayout(pctx, input),
|
|
4848
|
-
setDeviceLinks: (input) => setDeviceLinks(pctx, input),
|
|
4849
4088
|
setRole: (input) => setRole(pctx, input),
|
|
4850
4089
|
setDisplay: (input) => setDisplay(pctx, input),
|
|
4851
4090
|
getRoleDisplayDefaults: (input) => getRoleDisplayDefaults(pctx, input),
|
|
@@ -5005,13 +4244,13 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
5005
4244
|
capability: require_dist.deviceStateCapability,
|
|
5006
4245
|
provider: {
|
|
5007
4246
|
getSnapshot: async (input) => {
|
|
5008
|
-
return stateMirror.
|
|
4247
|
+
return stateMirror.snapshotForDevice(input.deviceId);
|
|
5009
4248
|
},
|
|
5010
4249
|
getCapSlice: async (input) => {
|
|
5011
|
-
return stateMirror.
|
|
4250
|
+
return stateMirror.capSlice(input.deviceId, input.capName);
|
|
5012
4251
|
},
|
|
5013
4252
|
getAllSnapshots: async () => {
|
|
5014
|
-
return stateMirror.
|
|
4253
|
+
return stateMirror.allSnapshots();
|
|
5015
4254
|
},
|
|
5016
4255
|
setCapSlice: async (input) => {
|
|
5017
4256
|
const { deviceId, capName, slice } = input;
|