@camstack/system 1.1.23 → 1.1.25
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/builtins/device-manager/device-link-overlay.d.ts +15 -5
- package/dist/builtins/device-manager/device-link-resolver.d.ts +11 -0
- package/dist/builtins/device-manager/device-manager.addon.js +255 -33
- package/dist/builtins/device-manager/device-manager.addon.mjs +256 -34
- package/dist/builtins/device-manager/device-meta-actions.d.ts +21 -0
- package/dist/builtins/device-manager/device-meta-types.d.ts +12 -1
- package/dist/builtins/device-manager/device-projection.d.ts +1 -0
- package/dist/builtins/platform-probe/index.d.ts +22 -0
- package/dist/builtins/platform-probe/index.js +83 -2
- package/dist/builtins/platform-probe/index.mjs +84 -3
- package/dist/index.js +1 -2
- package/dist/index.mjs +2 -2
- package/dist/kernel/index.d.ts +0 -2
- package/dist/kernel/transport/parent-unowned-call.d.ts +20 -0
- package/dist/{manifest-python-deps-D7iR07uA.js → manifest-python-deps-BqXCckob.js} +13 -36
- package/dist/{manifest-python-deps-hDWMDe_b.mjs → manifest-python-deps-Dxl4twM3.mjs} +17 -34
- package/package.json +1 -1
- package/dist/kernel/hwaccel/hwaccel-service.d.ts +0 -4
package/dist/addon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
2
|
+
const require_manifest_python_deps = require("./manifest-python-deps-BqXCckob.js");
|
|
3
3
|
const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
|
|
4
4
|
let node_fs = require("node:fs");
|
|
5
5
|
node_fs = require_chunk.__toESM(node_fs);
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { F as createUdsLoggerWithControl, I as LocalChildClient, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps, yt as resolveAddonClass } from "./manifest-python-deps-Dxl4twM3.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import * as fs from "node:fs";
|
|
@@ -2,7 +2,12 @@ import { DeviceLink } from '@camstack/types';
|
|
|
2
2
|
/** A target link with its source already resolved to a live numeric id. */
|
|
3
3
|
export interface ResolvedTargetLink {
|
|
4
4
|
readonly link: DeviceLink;
|
|
5
|
+
/** Resolved source device id for scalar sources; `-1` sentinel for a literal
|
|
6
|
+
* source AND for an expression source (whose per-binding ids live below). */
|
|
5
7
|
readonly sourceDeviceId: number;
|
|
8
|
+
/** Expression links only: binding name → resolved source device id. Literal
|
|
9
|
+
* bindings and unresolved bindings are ABSENT from the map. */
|
|
10
|
+
readonly bindingSourceIds?: Readonly<Record<string, number>>;
|
|
6
11
|
}
|
|
7
12
|
/** A target that depends on a given source slice (for reactive re-emit). */
|
|
8
13
|
export interface DependentLink {
|
|
@@ -15,9 +20,14 @@ export interface LinkIndexes {
|
|
|
15
20
|
/** `${sourceDeviceId}:${sourceCap}` → targets to recompute on a source change. */
|
|
16
21
|
readonly dependents: Map<string, DependentLink[]>;
|
|
17
22
|
}
|
|
23
|
+
/** One resolved link entry fed to `buildLinkIndexes`. */
|
|
24
|
+
export interface LinkIndexEntry {
|
|
25
|
+
readonly targetDeviceId: number;
|
|
26
|
+
readonly link: DeviceLink;
|
|
27
|
+
/** Resolved scalar source id; `-1` for literal/expression sources. */
|
|
28
|
+
readonly sourceDeviceId: number;
|
|
29
|
+
/** Expression links: binding name → resolved source device id. */
|
|
30
|
+
readonly bindingSourceIds?: Readonly<Record<string, number>>;
|
|
31
|
+
}
|
|
18
32
|
/** Build both lookup maps from resolved link entries. Pure — order-preserving. */
|
|
19
|
-
export declare function buildLinkIndexes(entries: ReadonlyArray<
|
|
20
|
-
targetDeviceId: number;
|
|
21
|
-
link: DeviceLink;
|
|
22
|
-
sourceDeviceId: number;
|
|
23
|
-
}>): LinkIndexes;
|
|
33
|
+
export declare function buildLinkIndexes(entries: ReadonlyArray<LinkIndexEntry>): LinkIndexes;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CapabilityStatusItemArray, DeviceLink } from '@camstack/types';
|
|
3
|
+
import { ResolvedTargetLink } from './device-link-overlay.js';
|
|
3
4
|
/** One resolved source reading paired with the link that produced it. The
|
|
4
5
|
* `sourceValue` is already transformed; `undefined` skips the overlay. */
|
|
5
6
|
export interface ResolvedLinkValue {
|
|
@@ -16,6 +17,16 @@ export interface ResolvedLinkValue {
|
|
|
16
17
|
* `source.kind` directly.
|
|
17
18
|
*/
|
|
18
19
|
export declare function resolveLinkValue(link: DeviceLink, readField: (cap: string, fieldPath: string) => unknown): unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Sync-channel expression resolution: bindings read from the mirror via
|
|
22
|
+
* `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
|
|
23
|
+
* on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
|
|
24
|
+
* binding id (dangling source) or a non-primitive read resolves to `null`.
|
|
25
|
+
* Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
|
|
26
|
+
* the mirror path is hot and churn-free; the async channel logs. Compile
|
|
27
|
+
* failures cost one parse ever thanks to the negative LRU compile cache. Pure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveExpressionLinkValue(rl: ResolvedTargetLink, readField: (deviceId: number, cap: string, fieldPath: string) => unknown, now: number): unknown;
|
|
19
30
|
/**
|
|
20
31
|
* Overlay transformed source values onto `base` by dot-path, then validate the
|
|
21
32
|
* result against the target cap's `statusSchema`. On validation failure the
|
|
@@ -300,7 +300,8 @@ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
|
|
|
300
300
|
...metaRow?.linkDeviceId !== void 0 ? { linkDeviceId: metaRow.linkDeviceId } : {},
|
|
301
301
|
...metaRow?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: metaRow.primaryChildEntityId } : {},
|
|
302
302
|
...metaRow?.childLayout !== void 0 ? { childLayout: metaRow.childLayout } : {},
|
|
303
|
-
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {}
|
|
303
|
+
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {},
|
|
304
|
+
...metaRow?.display !== void 0 ? { display: metaRow.display } : {}
|
|
304
305
|
};
|
|
305
306
|
}
|
|
306
307
|
function resolveDeviceById(registry, deviceId) {
|
|
@@ -1371,20 +1372,41 @@ var DeviceEventPropagator = class {
|
|
|
1371
1372
|
function nodeKey(deviceId, cap) {
|
|
1372
1373
|
return `${deviceId}:${cap}`;
|
|
1373
1374
|
}
|
|
1375
|
+
/** The target's container stableId — its parent's (falling back to its own for
|
|
1376
|
+
* a top-level target). Sibling FIELD sources resolve relative to this, the same
|
|
1377
|
+
* rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
|
|
1378
|
+
function containerStableId(targetRow, rowById) {
|
|
1379
|
+
return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
|
|
1380
|
+
}
|
|
1381
|
+
/** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
|
|
1382
|
+
* null when it is a literal or resolves to no known device. */
|
|
1383
|
+
function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
|
|
1384
|
+
if (binding.kind === "literal") return null;
|
|
1385
|
+
const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
|
|
1386
|
+
const srcId = idByStableId.get(wantedStableId);
|
|
1387
|
+
if (srcId === void 0) return null;
|
|
1388
|
+
return nodeKey(srcId, binding.cap);
|
|
1389
|
+
}
|
|
1374
1390
|
/**
|
|
1375
|
-
* Resolve a link source to
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1391
|
+
* Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
|
|
1392
|
+
* A literal source depends on nothing; a field/global source depends on exactly
|
|
1393
|
+
* one node; an EXPRESSION source depends on one node per resolvable non-literal
|
|
1394
|
+
* binding. Unresolvable (dangling) bindings contribute no edge — they cannot
|
|
1395
|
+
* loop while dangling; a cycle that only becomes resolvable later is bounded by
|
|
1396
|
+
* the resolve-time re-entrancy guard.
|
|
1380
1397
|
*/
|
|
1381
|
-
function
|
|
1398
|
+
function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
|
|
1382
1399
|
const src = link.source;
|
|
1383
|
-
if (src.kind === "
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1400
|
+
if (src.kind === "expression") {
|
|
1401
|
+
const nodes = [];
|
|
1402
|
+
for (const binding of Object.values(src.bindings)) {
|
|
1403
|
+
const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
|
|
1404
|
+
if (node !== null) nodes.push(node);
|
|
1405
|
+
}
|
|
1406
|
+
return nodes;
|
|
1407
|
+
}
|
|
1408
|
+
const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
|
|
1409
|
+
return node !== null ? [node] : [];
|
|
1388
1410
|
}
|
|
1389
1411
|
/**
|
|
1390
1412
|
* Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
|
|
@@ -1402,11 +1424,11 @@ function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
|
|
|
1402
1424
|
const dependsOn = /* @__PURE__ */ new Map();
|
|
1403
1425
|
const addEdges = (row, links) => {
|
|
1404
1426
|
for (const link of links) {
|
|
1405
|
-
const
|
|
1406
|
-
if (
|
|
1427
|
+
const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
|
|
1428
|
+
if (srcNodes.length === 0) continue;
|
|
1407
1429
|
const tNode = nodeKey(row.id, link.target.cap);
|
|
1408
1430
|
const list = dependsOn.get(tNode) ?? [];
|
|
1409
|
-
list.push(srcNode);
|
|
1431
|
+
for (const srcNode of srcNodes) list.push(srcNode);
|
|
1410
1432
|
dependsOn.set(tNode, list);
|
|
1411
1433
|
}
|
|
1412
1434
|
};
|
|
@@ -1473,7 +1495,7 @@ function dfsFindCycle(root, dependsOn) {
|
|
|
1473
1495
|
* removeDevice), config persistence (persistConfig, loadConfig), the meta
|
|
1474
1496
|
* surface load (loadMeta, loadRuntimeState), every meta setter (setName,
|
|
1475
1497
|
* setLocation, setType, setIntegrationId, setLinkDeviceId,
|
|
1476
|
-
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole,
|
|
1498
|
+
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole, setDisplay,
|
|
1477
1499
|
* applyInitialMeta, setMetadata, setDisabled), and the location registry
|
|
1478
1500
|
* (listLocations, addLocation, removeLocation).
|
|
1479
1501
|
*
|
|
@@ -1582,6 +1604,7 @@ async function registerDevice(pctx, input) {
|
|
|
1582
1604
|
...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
|
|
1583
1605
|
...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
|
|
1584
1606
|
...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
|
|
1607
|
+
...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
|
|
1585
1608
|
parentDeviceId,
|
|
1586
1609
|
id,
|
|
1587
1610
|
features: featuresArr,
|
|
@@ -2023,6 +2046,11 @@ async function setDeviceLinks(pctx, input) {
|
|
|
2023
2046
|
const { deviceId, deviceLinks } = input;
|
|
2024
2047
|
const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
|
|
2025
2048
|
if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
|
|
2049
|
+
for (const link of deviceLinks) {
|
|
2050
|
+
if (link.source.kind !== "expression") continue;
|
|
2051
|
+
const err = (0, _camstack_types.validateExpressionSource)(link.source);
|
|
2052
|
+
if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
|
|
2053
|
+
}
|
|
2026
2054
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2027
2055
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2028
2056
|
if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
|
|
@@ -2093,6 +2121,84 @@ async function setRole(pctx, input) {
|
|
|
2093
2121
|
});
|
|
2094
2122
|
}
|
|
2095
2123
|
/**
|
|
2124
|
+
* Normalize an operator-authored display override at write time so the render
|
|
2125
|
+
* path's `UNIT_TABLE` lookups always hit canonical spellings — maps `unit` and
|
|
2126
|
+
* every `perCap[*].unit` through `normalizeUnit`. Pure: rebuilds new objects,
|
|
2127
|
+
* never mutates the input. A unit `normalizeUnit` cannot canonicalize is left
|
|
2128
|
+
* as-is (the render path refuses conversion for unknown spellings).
|
|
2129
|
+
*/
|
|
2130
|
+
function normalizeDisplayOverride(display) {
|
|
2131
|
+
return {
|
|
2132
|
+
...display,
|
|
2133
|
+
...display.unit !== void 0 ? { unit: (0, _camstack_types.normalizeUnit)(display.unit) ?? display.unit } : {},
|
|
2134
|
+
...display.perCap !== void 0 ? { perCap: Object.fromEntries(Object.entries(display.perCap).map(([cap, refine]) => [cap, refine.unit !== void 0 ? {
|
|
2135
|
+
...refine,
|
|
2136
|
+
unit: (0, _camstack_types.normalizeUnit)(refine.unit) ?? refine.unit
|
|
2137
|
+
} : refine])) } : {}
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Set (or clear) the per-device display override on a device's meta row.
|
|
2142
|
+
* Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
|
|
2143
|
+
* entirely (immutable rest-destructure — matches the "absent ⇒ no override"
|
|
2144
|
+
* projection contract, never persists `display: undefined`). Override units are
|
|
2145
|
+
* normalized at write so the render path always looks up canonical spellings.
|
|
2146
|
+
*/
|
|
2147
|
+
async function setDisplay(pctx, input) {
|
|
2148
|
+
const { deviceId, display } = input;
|
|
2149
|
+
const normalized = display === null ? null : normalizeDisplayOverride(display);
|
|
2150
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2151
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2152
|
+
if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
|
|
2153
|
+
const { meta: m } = persisted;
|
|
2154
|
+
const key = String(deviceId);
|
|
2155
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
2156
|
+
const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
|
|
2157
|
+
...m,
|
|
2158
|
+
display: normalized
|
|
2159
|
+
};
|
|
2160
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2161
|
+
...allMeta,
|
|
2162
|
+
[key]: nextRow
|
|
2163
|
+
} });
|
|
2164
|
+
});
|
|
2165
|
+
pctx.host.ctx.eventBus.emit({
|
|
2166
|
+
id: (0, node_crypto.randomUUID)(),
|
|
2167
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2168
|
+
source: {
|
|
2169
|
+
type: "device",
|
|
2170
|
+
id: deviceId
|
|
2171
|
+
},
|
|
2172
|
+
category: _camstack_types.EventCategory.DeviceMetaChanged,
|
|
2173
|
+
data: {
|
|
2174
|
+
deviceId,
|
|
2175
|
+
field: "display",
|
|
2176
|
+
value: normalized
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Read the operator-authored per-role display defaults. Empty record when none
|
|
2182
|
+
* set. Not per-device — a plain top-level-key read.
|
|
2183
|
+
*/
|
|
2184
|
+
async function getRoleDisplayDefaults(pctx, _input) {
|
|
2185
|
+
return { defaults: (await pctx.metaStore.readStore()).roleDisplayDefaults ?? {} };
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Replace the per-role display defaults whole-record (full replace). Override
|
|
2189
|
+
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
2190
|
+
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
2191
|
+
* no interaction with the `deviceMeta` write lock. Not per-device, so no event
|
|
2192
|
+
* is emitted; the UI invalidates its own query on mutate.
|
|
2193
|
+
*/
|
|
2194
|
+
async function setRoleDisplayDefaults(pctx, input) {
|
|
2195
|
+
const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
|
|
2196
|
+
...def,
|
|
2197
|
+
unit: (0, _camstack_types.normalizeUnit)(def.unit) ?? def.unit
|
|
2198
|
+
} : def]));
|
|
2199
|
+
await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2096
2202
|
* Batched meta pre-seed. Applies every provided field to the
|
|
2097
2203
|
* device's meta row in ONE read-modify-write under a single
|
|
2098
2204
|
* `withMetaWriteLock` acquisition (one `deviceMeta` blob write),
|
|
@@ -2337,23 +2443,33 @@ async function removeLocation(pctx, input) {
|
|
|
2337
2443
|
function buildLinkIndexes(entries) {
|
|
2338
2444
|
const targets = /* @__PURE__ */ new Map();
|
|
2339
2445
|
const dependents = /* @__PURE__ */ new Map();
|
|
2340
|
-
for (const { targetDeviceId, link, sourceDeviceId } of entries) {
|
|
2446
|
+
for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
|
|
2341
2447
|
const tKey = `${targetDeviceId}:${link.target.cap}`;
|
|
2342
2448
|
const tList = targets.get(tKey) ?? [];
|
|
2343
2449
|
tList.push({
|
|
2344
2450
|
link,
|
|
2345
|
-
sourceDeviceId
|
|
2451
|
+
sourceDeviceId,
|
|
2452
|
+
...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
|
|
2346
2453
|
});
|
|
2347
2454
|
targets.set(tKey, tList);
|
|
2348
|
-
|
|
2349
|
-
const sKey = `${
|
|
2455
|
+
const addDependent = (sourceId, sourceCap) => {
|
|
2456
|
+
const sKey = `${sourceId}:${sourceCap}`;
|
|
2350
2457
|
const sList = dependents.get(sKey) ?? [];
|
|
2351
2458
|
sList.push({
|
|
2352
2459
|
targetDeviceId,
|
|
2353
2460
|
targetCap: link.target.cap
|
|
2354
2461
|
});
|
|
2355
2462
|
dependents.set(sKey, sList);
|
|
2463
|
+
};
|
|
2464
|
+
if (link.source.kind === "expression") {
|
|
2465
|
+
if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
|
|
2466
|
+
const binding = link.source.bindings[name];
|
|
2467
|
+
if (binding === void 0 || binding.kind === "literal") continue;
|
|
2468
|
+
addDependent(bindingDeviceId, binding.cap);
|
|
2469
|
+
}
|
|
2470
|
+
continue;
|
|
2356
2471
|
}
|
|
2472
|
+
if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
|
|
2357
2473
|
}
|
|
2358
2474
|
return {
|
|
2359
2475
|
targets,
|
|
@@ -2508,6 +2624,23 @@ var DeviceMetaStore = class {
|
|
|
2508
2624
|
if (container === void 0) continue;
|
|
2509
2625
|
for (const link of targetMeta.deviceLinks ?? []) {
|
|
2510
2626
|
const src = link.source;
|
|
2627
|
+
if (src.kind === "expression") {
|
|
2628
|
+
const bindingSourceIds = {};
|
|
2629
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
2630
|
+
if (b.kind === "literal") continue;
|
|
2631
|
+
const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
|
|
2632
|
+
expectedSources.add(wanted);
|
|
2633
|
+
const bid = idByStableId.get(wanted);
|
|
2634
|
+
if (bid !== void 0) bindingSourceIds[name] = bid;
|
|
2635
|
+
}
|
|
2636
|
+
entries.push({
|
|
2637
|
+
targetDeviceId: targetId,
|
|
2638
|
+
link,
|
|
2639
|
+
sourceDeviceId: -1,
|
|
2640
|
+
bindingSourceIds
|
|
2641
|
+
});
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2511
2644
|
if (src.kind === "literal") {
|
|
2512
2645
|
entries.push({
|
|
2513
2646
|
targetDeviceId: targetId,
|
|
@@ -2625,6 +2758,7 @@ async function listAll(pctx, input) {
|
|
|
2625
2758
|
...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2626
2759
|
...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2627
2760
|
...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2761
|
+
...m?.display !== void 0 ? { display: m.display } : {},
|
|
2628
2762
|
...(() => {
|
|
2629
2763
|
const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
2630
2764
|
return si !== void 0 ? { sourceInfo: si } : {};
|
|
@@ -2673,6 +2807,7 @@ async function getDevice(pctx, input) {
|
|
|
2673
2807
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2674
2808
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2675
2809
|
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2810
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
2676
2811
|
...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
|
|
2677
2812
|
};
|
|
2678
2813
|
}
|
|
@@ -2735,6 +2870,7 @@ async function getChildren(pctx, input) {
|
|
|
2735
2870
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2736
2871
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2737
2872
|
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2873
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
2738
2874
|
...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
|
|
2739
2875
|
});
|
|
2740
2876
|
}
|
|
@@ -3074,7 +3210,37 @@ function isRecord(x) {
|
|
|
3074
3210
|
* `source.kind` directly.
|
|
3075
3211
|
*/
|
|
3076
3212
|
function resolveLinkValue(link, readField) {
|
|
3077
|
-
|
|
3213
|
+
const src = link.source;
|
|
3214
|
+
if (src.kind === "expression") return void 0;
|
|
3215
|
+
return (0, _camstack_types.applyTransform)(src.kind === "literal" ? src.value : readField(src.cap, src.fieldPath), link.transform);
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Sync-channel expression resolution: bindings read from the mirror via
|
|
3219
|
+
* `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
|
|
3220
|
+
* on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
|
|
3221
|
+
* binding id (dangling source) or a non-primitive read resolves to `null`.
|
|
3222
|
+
* Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
|
|
3223
|
+
* the mirror path is hot and churn-free; the async channel logs. Compile
|
|
3224
|
+
* failures cost one parse ever thanks to the negative LRU compile cache. Pure.
|
|
3225
|
+
*/
|
|
3226
|
+
function resolveExpressionLinkValue(rl, readField, now) {
|
|
3227
|
+
const src = rl.link.source;
|
|
3228
|
+
if (src.kind !== "expression") return void 0;
|
|
3229
|
+
const bindingValues = {};
|
|
3230
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3231
|
+
if (b.kind === "literal") {
|
|
3232
|
+
bindingValues[name] = b.value;
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
const bId = rl.bindingSourceIds?.[name];
|
|
3236
|
+
if (bId === void 0) {
|
|
3237
|
+
bindingValues[name] = null;
|
|
3238
|
+
continue;
|
|
3239
|
+
}
|
|
3240
|
+
bindingValues[name] = (0, _camstack_types.toExpressionValue)(readField(bId, b.cap, b.fieldPath)) ?? null;
|
|
3241
|
+
}
|
|
3242
|
+
const result = (0, _camstack_types.evaluateLinkExpression)(src.expr, bindingValues, now);
|
|
3243
|
+
return result.ok ? (0, _camstack_types.applyTransform)(result.value, rl.link.transform) : void 0;
|
|
3078
3244
|
}
|
|
3079
3245
|
/** Narrow Zod v4's structural `$ZodType` (returned by `.unwrap()`) back to the
|
|
3080
3246
|
* concrete classic `z.ZodType`. Every runtime schema is a `z.ZodType`, so this
|
|
@@ -3395,9 +3561,10 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
3395
3561
|
const raw = this.stateMirror.get(deviceId)?.get(cap) ?? null;
|
|
3396
3562
|
const links = this.linkHost.linkTargets.get(`${deviceId}:${cap}`);
|
|
3397
3563
|
if (!links || links.length === 0) return raw ? { ...raw } : null;
|
|
3564
|
+
const now = Date.now();
|
|
3398
3565
|
const resolved = links.map((rl) => ({
|
|
3399
3566
|
link: rl.link,
|
|
3400
|
-
sourceValue: resolveLinkValue(rl.link, (srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
3567
|
+
sourceValue: rl.link.source.kind === "expression" ? resolveExpressionLinkValue(rl, (srcDeviceId, srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(srcDeviceId)?.get(srcCap), fieldPath), now) : resolveLinkValue(rl.link, (srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
3401
3568
|
}));
|
|
3402
3569
|
const capStatus = this.linkHost.capabilityRegistry?.getDefinition(cap)?.status;
|
|
3403
3570
|
const schema = capStatus?.schema;
|
|
@@ -3588,6 +3755,39 @@ async function resolveLinkedStatusInner(pctx, input) {
|
|
|
3588
3755
|
const parentMeta = allMeta[String(parentId)];
|
|
3589
3756
|
if (parentMeta) containerStableId = parentMeta.stableId;
|
|
3590
3757
|
}
|
|
3758
|
+
const now = Date.now();
|
|
3759
|
+
const statusMemo = /* @__PURE__ */ new Map();
|
|
3760
|
+
const readSourceStatus = async (srcId, srcCap) => {
|
|
3761
|
+
const key = `${srcId}:${srcCap}`;
|
|
3762
|
+
const cached = statusMemo.get(key);
|
|
3763
|
+
if (cached !== void 0) return cached;
|
|
3764
|
+
let result;
|
|
3765
|
+
try {
|
|
3766
|
+
const srcProvider = capRegistry?.getProviderForDevice(srcCap, srcId);
|
|
3767
|
+
result = {
|
|
3768
|
+
status: typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0,
|
|
3769
|
+
ok: true
|
|
3770
|
+
};
|
|
3771
|
+
} catch (err) {
|
|
3772
|
+
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
3773
|
+
tags: {
|
|
3774
|
+
deviceId,
|
|
3775
|
+
capName: cap
|
|
3776
|
+
},
|
|
3777
|
+
meta: {
|
|
3778
|
+
sourceCap: srcCap,
|
|
3779
|
+
sourceId: srcId,
|
|
3780
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3781
|
+
}
|
|
3782
|
+
});
|
|
3783
|
+
result = {
|
|
3784
|
+
status: void 0,
|
|
3785
|
+
ok: false
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
statusMemo.set(key, result);
|
|
3789
|
+
return result;
|
|
3790
|
+
};
|
|
3591
3791
|
const resolved = [];
|
|
3592
3792
|
for (const link of links) {
|
|
3593
3793
|
const src = link.source;
|
|
@@ -3598,27 +3798,46 @@ async function resolveLinkedStatusInner(pctx, input) {
|
|
|
3598
3798
|
});
|
|
3599
3799
|
continue;
|
|
3600
3800
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3801
|
+
if (src.kind === "expression") {
|
|
3802
|
+
const bindingValues = {};
|
|
3803
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3804
|
+
if (b.kind === "literal") {
|
|
3805
|
+
bindingValues[name] = b.value;
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
const bId = b.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(b.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, b.sourceKey, allMeta);
|
|
3809
|
+
if (bId === null) {
|
|
3810
|
+
bindingValues[name] = null;
|
|
3811
|
+
continue;
|
|
3812
|
+
}
|
|
3813
|
+
bindingValues[name] = (0, _camstack_types.toExpressionValue)((0, _camstack_types.getByPath)((await readSourceStatus(bId, b.cap)).status, b.fieldPath)) ?? null;
|
|
3814
|
+
}
|
|
3815
|
+
const result = (0, _camstack_types.evaluateLinkExpression)(src.expr, bindingValues, now);
|
|
3816
|
+
if (result.ok) resolved.push({
|
|
3607
3817
|
link,
|
|
3608
|
-
sourceValue: (0, _camstack_types.applyTransform)(
|
|
3818
|
+
sourceValue: (0, _camstack_types.applyTransform)(result.value, link.transform)
|
|
3609
3819
|
});
|
|
3610
|
-
|
|
3611
|
-
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
3820
|
+
else pctx.host.ctx.logger.warn("resolveLinkedStatus: expression skipped", {
|
|
3612
3821
|
tags: {
|
|
3613
3822
|
deviceId,
|
|
3614
3823
|
capName: cap
|
|
3615
3824
|
},
|
|
3616
3825
|
meta: {
|
|
3617
|
-
|
|
3618
|
-
error:
|
|
3826
|
+
linkId: link.id,
|
|
3827
|
+
error: result.error
|
|
3619
3828
|
}
|
|
3620
3829
|
});
|
|
3830
|
+
continue;
|
|
3621
3831
|
}
|
|
3832
|
+
const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
|
|
3833
|
+
if (srcId === null) continue;
|
|
3834
|
+
const read = await readSourceStatus(srcId, src.cap);
|
|
3835
|
+
if (!read.ok) continue;
|
|
3836
|
+
const raw = (0, _camstack_types.getByPath)(read.status, src.fieldPath);
|
|
3837
|
+
resolved.push({
|
|
3838
|
+
link,
|
|
3839
|
+
sourceValue: (0, _camstack_types.applyTransform)(raw, link.transform)
|
|
3840
|
+
});
|
|
3622
3841
|
}
|
|
3623
3842
|
if (resolved.length === 0) return null;
|
|
3624
3843
|
const synthesizing = !isRecord$1(baseStatus);
|
|
@@ -3931,6 +4150,9 @@ var DeviceManagerAddon = class extends _camstack_types.BaseAddon {
|
|
|
3931
4150
|
setChildLayout: (input) => setChildLayout(pctx, input),
|
|
3932
4151
|
setDeviceLinks: (input) => setDeviceLinks(pctx, input),
|
|
3933
4152
|
setRole: (input) => setRole(pctx, input),
|
|
4153
|
+
setDisplay: (input) => setDisplay(pctx, input),
|
|
4154
|
+
getRoleDisplayDefaults: (input) => getRoleDisplayDefaults(pctx, input),
|
|
4155
|
+
setRoleDisplayDefaults: (input) => setRoleDisplayDefaults(pctx, input),
|
|
3934
4156
|
applyInitialMeta: (input) => applyInitialMeta(pctx, input),
|
|
3935
4157
|
setMetadata: (input) => setMetadata(pctx, input),
|
|
3936
4158
|
setDisabled: (input) => setDisabled(pctx, input),
|