@camstack/system 1.2.315 → 1.2.317
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/auth/auth-manager.d.ts +11 -0
- 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/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +347 -181
- package/dist/builtins/composer/composer.addon.mjs +347 -181
- package/dist/builtins/composer/composition-evaluation.d.ts +37 -0
- package/dist/builtins/composer/composition-runtime.d.ts +11 -52
- package/dist/builtins/composer/composition-target.d.ts +50 -0
- package/dist/builtins/composer/existing-target.d.ts +19 -1
- package/dist/builtins/composer/own-field-reads.d.ts +19 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +265 -29
- package/dist/builtins/device-manager/device-manager.addon.mjs +265 -29
- package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
- package/dist/builtins/device-manager/device-source-activation.d.ts +26 -1
- package/dist/builtins/device-manager/device-state-mirror.d.ts +41 -9
- package/dist/builtins/device-manager/mirror-row-writer.d.ts +59 -1
- package/dist/builtins/doorbell/binding-mirror.d.ts +45 -81
- package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +4 -3
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +53 -11
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -82
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -82
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +47 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +46 -4
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -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 +4 -4
- package/dist/builtins/system-config/system-config.addon.mjs +4 -4
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{child-cap-dispatch-AAltfBUH.mjs → child-cap-dispatch-C2VINHu1.mjs} +70 -18
- package/dist/{child-cap-dispatch-DzcUd_ct.js → child-cap-dispatch-C8uts1P4.js} +70 -18
- package/dist/{composition-sources-yonsRneP.mjs → composition-sources-DVClu006.mjs} +1 -1
- package/dist/{composition-sources-DYqQLRzF.js → composition-sources-Dep7nBwu.js} +1 -1
- package/dist/{dist-MzYJVCLE.mjs → dist-Dori4pf3.mjs} +99 -11
- package/dist/{dist-DVKv5i-e.js → dist-a6p0V-IL.js} +99 -11
- package/dist/index.js +739 -350
- package/dist/index.mjs +737 -352
- package/dist/kernel/bootstrap-config.d.ts +8 -0
- package/dist/kernel/capability-registry.d.ts +30 -0
- package/dist/kernel/config-manager.d.ts +75 -129
- package/dist/kernel/index.d.ts +5 -2
- package/dist/kernel/moleculer/runtime-state-load.d.ts +19 -0
- package/dist/kernel/settings-door-view.d.ts +24 -1
- package/dist/kernel/settings-read-error.d.ts +37 -0
- package/dist/kernel/settings-view-types.d.ts +105 -0
- package/dist/kernel/store-settings-view.d.ts +17 -0
- package/dist/kernel/system-settings-mirror.d.ts +34 -0
- package/dist/kernel/system-settings-policy.d.ts +47 -0
- package/dist/{retired-settings-keys-iY5I0nKM.js → retired-settings-keys-DC1uHWPZ.js} +1 -1
- package/dist/{retired-settings-keys-8yY2sTc-.mjs → retired-settings-keys-DVcKRgzm.mjs} +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { An as cloneStatefulExpressionMemory, Bn as toExpressionValue, Fn as compositionSliceKey, Gn as BaseAddon, In as compositionSourceKey, K as buildCapabilityLookup, Ln as createStatefulExpressionMemory, Mn as composedDeviceStableId, Nn as compositionFeatureEnabled, Pn as compositionItemFieldPath, Un as DeviceType, Wn as errMsg, X as composerCapability, Xt as planComposition, Z as compositionFieldKey, c as COMPOSED_ITEM_FIXED_FIELDS, h as DeclaredDevices, hr as EventCategory, i as BaseDevice, t as ALL_CAPABILITY_DEFINITIONS, tr as createEvent, vt as evaluateCompositionField, wn as COMPOSER_ADDON_ID } from "../../dist-
|
|
1
|
+
import { An as cloneStatefulExpressionMemory, Bn as toExpressionValue, Fn as compositionSliceKey, Gn as BaseAddon, In as compositionSourceKey, K as buildCapabilityLookup, Ln as createStatefulExpressionMemory, Mn as composedDeviceStableId, Nn as compositionFeatureEnabled, Pn as compositionItemFieldPath, Un as DeviceType, Wn as errMsg, X as composerCapability, Xt as planComposition, Z as compositionFieldKey, c as COMPOSED_ITEM_FIXED_FIELDS, h as DeclaredDevices, hr as EventCategory, i as BaseDevice, jn as composedDeviceRef, t as ALL_CAPABILITY_DEFINITIONS, tr as createEvent, vt as evaluateCompositionField, wn as COMPOSER_ADDON_ID } from "../../dist-Dori4pf3.mjs";
|
|
2
2
|
import { n as parseDeviceStateChanged, t as isRecord$2 } from "../../device-state-changed-DoBbkyEI.mjs";
|
|
3
|
-
import { n as normalizedNativeOwner } from "../../composition-sources-
|
|
3
|
+
import { n as normalizedNativeOwner } from "../../composition-sources-DVClu006.mjs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
//#region src/builtins/composer/composed-device.ts
|
|
6
6
|
/**
|
|
@@ -626,88 +626,6 @@ function logPlanTransition(logger, previous, p, liveTags) {
|
|
|
626
626
|
else logger.warn(p.declaration === null ? "composition does not plan — no device is declared for it" : "composition no longer plans — its device is kept offline with no capabilities", extras);
|
|
627
627
|
}
|
|
628
628
|
//#endregion
|
|
629
|
-
//#region src/builtins/composer/slice-assembler.ts
|
|
630
|
-
/**
|
|
631
|
-
* From the runtime's flat field values to the slice a target takes (D663).
|
|
632
|
-
*
|
|
633
|
-
* A top-level field is written under its path. An item leaf
|
|
634
|
-
* (`consumables.items[desiccant].remaining.value`) is one entry of an ARRAY:
|
|
635
|
-
* every leaf of one item is grouped by `(cap, arrayPath, key)` and set, by
|
|
636
|
-
* path segment, onto the cap's OWN `status.itemArray.emptyItem` — so an
|
|
637
|
-
* unsourced nullable leaf is `null` because the cap says so, not because this
|
|
638
|
-
* module guessed. The key, the label and {@link COMPOSED_ITEM_FIXED_FIELDS}
|
|
639
|
-
* are the composer's; items are sorted by key so two writes of the same
|
|
640
|
-
* values are one slice.
|
|
641
|
-
*
|
|
642
|
-
* A cap the lookup does not know as an item array cannot be assembled: that
|
|
643
|
-
* is thrown by name, and the runtime's write path turns a throw into a named,
|
|
644
|
-
* retried failure — never a half-built array.
|
|
645
|
-
*/
|
|
646
|
-
function isRecord$1(value) {
|
|
647
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
648
|
-
}
|
|
649
|
-
/** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
|
|
650
|
-
function setPath(record, segments, value) {
|
|
651
|
-
const [head, ...rest] = segments;
|
|
652
|
-
if (head === void 0) return record;
|
|
653
|
-
if (rest.length === 0) return {
|
|
654
|
-
...record,
|
|
655
|
-
[head]: value
|
|
656
|
-
};
|
|
657
|
-
const current = record[head];
|
|
658
|
-
return {
|
|
659
|
-
...record,
|
|
660
|
-
[head]: setPath(isRecord$1(current) ? current : {}, rest, value)
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
function itemArrayOf(group, lookupCap) {
|
|
664
|
-
const itemArray = lookupCap(group.cap)?.status?.itemArray;
|
|
665
|
-
if (itemArray === void 0 || itemArray.path !== group.arrayPath) throw new Error(`\`${group.cap}\` has no item array at \`${group.arrayPath}\` — \`${compositionItemFieldPath(group.arrayPath, group.key, "*")}\` cannot be assembled`);
|
|
666
|
-
return itemArray;
|
|
667
|
-
}
|
|
668
|
-
function assembleItem(group, lookupCap) {
|
|
669
|
-
const itemArray = itemArrayOf(group, lookupCap);
|
|
670
|
-
const base = {
|
|
671
|
-
...itemArray.emptyItem,
|
|
672
|
-
[itemArray.keyField]: group.key,
|
|
673
|
-
...itemArray.labelField === void 0 ? {} : { [itemArray.labelField]: group.label },
|
|
674
|
-
...COMPOSED_ITEM_FIXED_FIELDS
|
|
675
|
-
};
|
|
676
|
-
return group.leaves.reduce((item, leaf) => setPath(item, leaf.plan.path.split("."), leaf.value), base);
|
|
677
|
-
}
|
|
678
|
-
function groupItems(entries) {
|
|
679
|
-
const groups = /* @__PURE__ */ new Map();
|
|
680
|
-
for (const entry of entries) {
|
|
681
|
-
const item = entry.plan.item;
|
|
682
|
-
if (item === void 0) continue;
|
|
683
|
-
const id = `${entry.plan.cap}\u0000${item.arrayPath}\u0000${item.key}`;
|
|
684
|
-
const group = groups.get(id) ?? {
|
|
685
|
-
cap: entry.plan.cap,
|
|
686
|
-
arrayPath: item.arrayPath,
|
|
687
|
-
key: item.key,
|
|
688
|
-
label: item.label,
|
|
689
|
-
leaves: []
|
|
690
|
-
};
|
|
691
|
-
groups.set(id, {
|
|
692
|
-
...group,
|
|
693
|
-
leaves: [...group.leaves, entry]
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
return [...groups.values()];
|
|
697
|
-
}
|
|
698
|
-
function compareKeys(a, b) {
|
|
699
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
700
|
-
}
|
|
701
|
-
function assembleSlice(entries, lookupCap) {
|
|
702
|
-
const top = Object.fromEntries(entries.filter((e) => e.plan.item === void 0).map((e) => [e.plan.path, e.value]));
|
|
703
|
-
const groups = groupItems(entries);
|
|
704
|
-
const arrays = [...new Set(groups.map((g) => g.arrayPath))].map((arrayPath) => [arrayPath, groups.filter((g) => g.arrayPath === arrayPath).toSorted((a, b) => compareKeys(a.key, b.key)).map((g) => assembleItem(g, lookupCap))]);
|
|
705
|
-
return {
|
|
706
|
-
...top,
|
|
707
|
-
...Object.fromEntries(arrays)
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
//#endregion
|
|
711
629
|
//#region src/builtins/composer/field-record.ts
|
|
712
630
|
/**
|
|
713
631
|
* One composed field's record and the pure judgements over it (D659, D663):
|
|
@@ -720,11 +638,11 @@ var NO_FIELDS = /* @__PURE__ */ new Set();
|
|
|
720
638
|
function fieldKey(plan) {
|
|
721
639
|
return compositionFieldKey(plan);
|
|
722
640
|
}
|
|
723
|
-
function isRecord(value) {
|
|
641
|
+
function isRecord$1(value) {
|
|
724
642
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
725
643
|
}
|
|
726
644
|
function valueAtPath$1(value, path) {
|
|
727
|
-
return path.split(".").reduce((node, segment) => isRecord(node) ? node[segment] : void 0, value);
|
|
645
|
+
return path.split(".").reduce((node, segment) => isRecord$1(node) ? node[segment] : void 0, value);
|
|
728
646
|
}
|
|
729
647
|
function initialValue(plan, byCap, lookupCap) {
|
|
730
648
|
const slice = byCap?.get(plan.cap);
|
|
@@ -735,7 +653,7 @@ function initialValue(plan, byCap, lookupCap) {
|
|
|
735
653
|
if (itemArray === void 0 || itemArray.path !== item.arrayPath) return void 0;
|
|
736
654
|
const entries = valueAtPath$1(slice, item.arrayPath);
|
|
737
655
|
if (!Array.isArray(entries)) return void 0;
|
|
738
|
-
return valueAtPath$1(entries.find((entry) => isRecord(entry) && entry[itemArray.keyField] === item.key), plan.path);
|
|
656
|
+
return valueAtPath$1(entries.find((entry) => isRecord$1(entry) && entry[itemArray.keyField] === item.key), plan.path);
|
|
739
657
|
}
|
|
740
658
|
function createFieldRecord(plan, byCap, lookupCap) {
|
|
741
659
|
const initial = initialValue(plan, byCap, lookupCap);
|
|
@@ -816,6 +734,283 @@ function pendingReason(cap, pending) {
|
|
|
816
734
|
return pending.kind === "refused" ? `the \`${cap}\` capability refused the value: ${pending.error}` : `writing the \`${cap}\` capability threw: ${pending.error}`;
|
|
817
735
|
}
|
|
818
736
|
//#endregion
|
|
737
|
+
//#region src/builtins/composer/own-field-reads.ts
|
|
738
|
+
/** Derived fields before accumulators: a count reads the value its own pass just computed. */
|
|
739
|
+
var EVALUATION_ORDER = {
|
|
740
|
+
derived: 0,
|
|
741
|
+
accumulator: 1
|
|
742
|
+
};
|
|
743
|
+
/** `keys` in evaluation order: derived, then accumulators, then the rest; stable within a rank. */
|
|
744
|
+
function orderForEvaluation(keys, records) {
|
|
745
|
+
const rank = (k) => EVALUATION_ORDER[records.get(k)?.plan.role ?? ""] ?? 2;
|
|
746
|
+
return [...keys].toSorted((a, b) => rank(a) - rank(b));
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* The own field `key` as the runtime holds it right now (`records`: the pass in
|
|
750
|
+
* progress) — exactly what the runtime PUBLISHES for it, which is what the hub
|
|
751
|
+
* answered before (review H1). An unavailable field under the `null` or
|
|
752
|
+
* `unknown` policy is published as `null` / `'unknown'`, so a reader sees that
|
|
753
|
+
* value; only a field nothing is written for (`release`, `offline`) reads as
|
|
754
|
+
* unavailable. Answering those as unavailable left `count` unprimed while a
|
|
755
|
+
* source was late at boot, and it then swallowed the first press as its seed.
|
|
756
|
+
*
|
|
757
|
+
* Not evaluated yet is unavailable: nothing is published, and unknown is never
|
|
758
|
+
* `null` (D393). The evaluation order makes this unreachable for a derived
|
|
759
|
+
* reader of a derived field. Never fatal here: a gone source fails the block
|
|
760
|
+
* through the field that reads it directly.
|
|
761
|
+
*/
|
|
762
|
+
function ownFieldReading(records, key) {
|
|
763
|
+
const record = records.get(key);
|
|
764
|
+
const outcome = record?.outcome ?? null;
|
|
765
|
+
if (record === void 0 || outcome === null) return {
|
|
766
|
+
state: "unavailable",
|
|
767
|
+
reason: `own field \`${key}\` not evaluated yet`,
|
|
768
|
+
fatal: false
|
|
769
|
+
};
|
|
770
|
+
if (outcome.state === "unavailable") {
|
|
771
|
+
const published = writable(record);
|
|
772
|
+
if (published !== void 0) return {
|
|
773
|
+
state: "fresh",
|
|
774
|
+
value: published
|
|
775
|
+
};
|
|
776
|
+
return {
|
|
777
|
+
state: "unavailable",
|
|
778
|
+
reason: `own field \`${key}\`: ${outcome.reason}`,
|
|
779
|
+
fatal: false,
|
|
780
|
+
...outcome.confirmed === true ? { confirmed: true } : {}
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (outcome.state === "stale") return {
|
|
784
|
+
state: "stale",
|
|
785
|
+
value: outcome.value,
|
|
786
|
+
reason: `own field \`${key}\`: ${outcome.reason ?? "stale"}`
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
state: "fresh",
|
|
790
|
+
value: outcome.value
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
//#endregion
|
|
794
|
+
//#region src/builtins/composer/composition-evaluation.ts
|
|
795
|
+
/**
|
|
796
|
+
* Evaluating a composition's fields (D659, D670): one pass over the keys a
|
|
797
|
+
* change affects, the own-field readers it must re-run (D670 amendment), and
|
|
798
|
+
* the state-derived events it emits. Split from `composition-runtime.ts`,
|
|
799
|
+
* which owns the records and hands them in through {@link EvaluationHost}.
|
|
800
|
+
*
|
|
801
|
+
* - Stateful expression memory commits only when the whole expression
|
|
802
|
+
* succeeds.
|
|
803
|
+
* - A declared state-derived event advances its high-water and is emitted
|
|
804
|
+
* BEFORE the corresponding target write; the committed records are visible
|
|
805
|
+
* to a synchronous subscriber first.
|
|
806
|
+
* - Derived fields are evaluated before accumulators, and every field reading
|
|
807
|
+
* an own field that changed is re-evaluated in the same pass.
|
|
808
|
+
*/
|
|
809
|
+
function strictFiniteAdvance(previous, next) {
|
|
810
|
+
return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
|
|
811
|
+
}
|
|
812
|
+
/** The record key an own read is answered from; null when `read` is a source's. */
|
|
813
|
+
function ownKeyOf(host, read) {
|
|
814
|
+
if (host.isOwnRead === void 0 || !host.isOwnRead(read)) return null;
|
|
815
|
+
const key = `${read.cap}.${read.fieldPath}`;
|
|
816
|
+
return host.records().has(key) ? key : null;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* `keys`, derived before accumulators, then — in the same pass — every
|
|
820
|
+
* derived/accumulator field that reads an own field that changed. Bounded by
|
|
821
|
+
* the record count: the validator refuses a cycle, so a chain is at most that
|
|
822
|
+
* long (D6).
|
|
823
|
+
*/
|
|
824
|
+
function evaluateWithOwnReaders(host, keys, now) {
|
|
825
|
+
const changed = /* @__PURE__ */ new Set();
|
|
826
|
+
const valueCaps = /* @__PURE__ */ new Set();
|
|
827
|
+
let round = orderForEvaluation(keys, host.records());
|
|
828
|
+
for (let hop = 0; round.length > 0 && hop <= host.records().size; hop++) {
|
|
829
|
+
const delta = evaluateFields(host, round, now);
|
|
830
|
+
for (const k of delta.keys) changed.add(k);
|
|
831
|
+
for (const c of delta.valueCaps) valueCaps.add(c);
|
|
832
|
+
round = orderForEvaluation([...host.records().values()].filter((r) => (r.plan.role === "derived" || r.plan.role === "accumulator") && r.plan.reads.some((read) => {
|
|
833
|
+
const own = ownKeyOf(host, read);
|
|
834
|
+
return own !== null && delta.keys.has(own);
|
|
835
|
+
})).map((r) => fieldKey(r.plan)), host.records());
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
keys: changed,
|
|
839
|
+
valueCaps
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/** Evaluate `keys` in order against the records in progress; commit whatever was evaluated, even on a throw. */
|
|
843
|
+
function evaluateFields(host, keys, now) {
|
|
844
|
+
const changed = /* @__PURE__ */ new Set();
|
|
845
|
+
const valueCaps = /* @__PURE__ */ new Set();
|
|
846
|
+
const next = new Map(host.records());
|
|
847
|
+
const read = (r) => {
|
|
848
|
+
const own = ownKeyOf(host, r);
|
|
849
|
+
return own === null ? host.read(r) : ownFieldReading(next, own);
|
|
850
|
+
};
|
|
851
|
+
try {
|
|
852
|
+
for (const key of keys) {
|
|
853
|
+
const record = next.get(key);
|
|
854
|
+
if (record === void 0) continue;
|
|
855
|
+
const candidateMemory = cloneStatefulExpressionMemory(record.expressionMemory);
|
|
856
|
+
const outcome = evaluateCompositionField({
|
|
857
|
+
source: record.plan.source,
|
|
858
|
+
read,
|
|
859
|
+
now,
|
|
860
|
+
stateful: {
|
|
861
|
+
currentOutput: currentOutput(record),
|
|
862
|
+
expressionMemory: candidateMemory,
|
|
863
|
+
startedAtMs: host.startedAtMs,
|
|
864
|
+
callPathPrefix: fieldKey(record.plan)
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
const expressionSucceeded = outcome.state !== "unavailable";
|
|
868
|
+
let evaluated = {
|
|
869
|
+
...record,
|
|
870
|
+
outcome,
|
|
871
|
+
...expressionSucceeded ? { expressionMemory: candidateMemory } : {}
|
|
872
|
+
};
|
|
873
|
+
const eventPlans = host.eventPlans.filter((eventPlan) => eventPlan.fieldKey === fieldKey(record.plan));
|
|
874
|
+
if (expressionSucceeded && eventPlans.length > 0 && strictFiniteAdvance(record.eventHighWater, outcome.value)) {
|
|
875
|
+
const timestamp = outcome.value;
|
|
876
|
+
evaluated = {
|
|
877
|
+
...evaluated,
|
|
878
|
+
eventHighWater: timestamp
|
|
879
|
+
};
|
|
880
|
+
next.set(key, evaluated);
|
|
881
|
+
host.commit(next);
|
|
882
|
+
for (const eventPlan of eventPlans) host.emit(eventPlan, timestamp);
|
|
883
|
+
} else next.set(key, evaluated);
|
|
884
|
+
if (sameOutcome(record.outcome, outcome)) continue;
|
|
885
|
+
if (record.outcome?.state !== outcome.state) host.logTransition(record, outcome);
|
|
886
|
+
if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
|
|
887
|
+
changed.add(key);
|
|
888
|
+
}
|
|
889
|
+
} finally {
|
|
890
|
+
host.commit(next);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
keys: changed,
|
|
894
|
+
valueCaps
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
/** Emit one state-derived event; a delivery failure is logged, never thrown (D670). */
|
|
898
|
+
function emitStateDerivedEvent(ctx, plan, timestamp) {
|
|
899
|
+
const extras = {
|
|
900
|
+
tags: { deviceId: ctx.deviceId },
|
|
901
|
+
meta: {
|
|
902
|
+
blockId: ctx.blockId,
|
|
903
|
+
cap: plan.cap,
|
|
904
|
+
eventName: plan.eventName,
|
|
905
|
+
category: plan.category,
|
|
906
|
+
timestamp
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
try {
|
|
910
|
+
if (plan.category !== EventCategory.DoorbellOnPressed) throw new Error(`unsupported state-derived event category \`${plan.category}\``);
|
|
911
|
+
const event = createEvent(EventCategory.DoorbellOnPressed, {
|
|
912
|
+
type: "device",
|
|
913
|
+
id: ctx.deviceId,
|
|
914
|
+
deviceId: ctx.deviceId
|
|
915
|
+
}, {
|
|
916
|
+
deviceId: ctx.deviceId,
|
|
917
|
+
timestamp
|
|
918
|
+
});
|
|
919
|
+
ctx.events.emit(event);
|
|
920
|
+
ctx.logger.debug("composition: state-derived event emitted", extras);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
ctx.logger.warn("composition: state-derived event delivery failed", {
|
|
923
|
+
...extras,
|
|
924
|
+
meta: {
|
|
925
|
+
...extras.meta,
|
|
926
|
+
error: errorText(error)
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/builtins/composer/slice-assembler.ts
|
|
933
|
+
/**
|
|
934
|
+
* From the runtime's flat field values to the slice a target takes (D663).
|
|
935
|
+
*
|
|
936
|
+
* A top-level field is written under its path. An item leaf
|
|
937
|
+
* (`consumables.items[desiccant].remaining.value`) is one entry of an ARRAY:
|
|
938
|
+
* every leaf of one item is grouped by `(cap, arrayPath, key)` and set, by
|
|
939
|
+
* path segment, onto the cap's OWN `status.itemArray.emptyItem` — so an
|
|
940
|
+
* unsourced nullable leaf is `null` because the cap says so, not because this
|
|
941
|
+
* module guessed. The key, the label and {@link COMPOSED_ITEM_FIXED_FIELDS}
|
|
942
|
+
* are the composer's; items are sorted by key so two writes of the same
|
|
943
|
+
* values are one slice.
|
|
944
|
+
*
|
|
945
|
+
* A cap the lookup does not know as an item array cannot be assembled: that
|
|
946
|
+
* is thrown by name, and the runtime's write path turns a throw into a named,
|
|
947
|
+
* retried failure — never a half-built array.
|
|
948
|
+
*/
|
|
949
|
+
function isRecord(value) {
|
|
950
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
951
|
+
}
|
|
952
|
+
/** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
|
|
953
|
+
function setPath(record, segments, value) {
|
|
954
|
+
const [head, ...rest] = segments;
|
|
955
|
+
if (head === void 0) return record;
|
|
956
|
+
if (rest.length === 0) return {
|
|
957
|
+
...record,
|
|
958
|
+
[head]: value
|
|
959
|
+
};
|
|
960
|
+
const current = record[head];
|
|
961
|
+
return {
|
|
962
|
+
...record,
|
|
963
|
+
[head]: setPath(isRecord(current) ? current : {}, rest, value)
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function itemArrayOf(group, lookupCap) {
|
|
967
|
+
const itemArray = lookupCap(group.cap)?.status?.itemArray;
|
|
968
|
+
if (itemArray === void 0 || itemArray.path !== group.arrayPath) throw new Error(`\`${group.cap}\` has no item array at \`${group.arrayPath}\` — \`${compositionItemFieldPath(group.arrayPath, group.key, "*")}\` cannot be assembled`);
|
|
969
|
+
return itemArray;
|
|
970
|
+
}
|
|
971
|
+
function assembleItem(group, lookupCap) {
|
|
972
|
+
const itemArray = itemArrayOf(group, lookupCap);
|
|
973
|
+
const base = {
|
|
974
|
+
...itemArray.emptyItem,
|
|
975
|
+
[itemArray.keyField]: group.key,
|
|
976
|
+
...itemArray.labelField === void 0 ? {} : { [itemArray.labelField]: group.label },
|
|
977
|
+
...COMPOSED_ITEM_FIXED_FIELDS
|
|
978
|
+
};
|
|
979
|
+
return group.leaves.reduce((item, leaf) => setPath(item, leaf.plan.path.split("."), leaf.value), base);
|
|
980
|
+
}
|
|
981
|
+
function groupItems(entries) {
|
|
982
|
+
const groups = /* @__PURE__ */ new Map();
|
|
983
|
+
for (const entry of entries) {
|
|
984
|
+
const item = entry.plan.item;
|
|
985
|
+
if (item === void 0) continue;
|
|
986
|
+
const id = `${entry.plan.cap}\u0000${item.arrayPath}\u0000${item.key}`;
|
|
987
|
+
const group = groups.get(id) ?? {
|
|
988
|
+
cap: entry.plan.cap,
|
|
989
|
+
arrayPath: item.arrayPath,
|
|
990
|
+
key: item.key,
|
|
991
|
+
label: item.label,
|
|
992
|
+
leaves: []
|
|
993
|
+
};
|
|
994
|
+
groups.set(id, {
|
|
995
|
+
...group,
|
|
996
|
+
leaves: [...group.leaves, entry]
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
return [...groups.values()];
|
|
1000
|
+
}
|
|
1001
|
+
function compareKeys(a, b) {
|
|
1002
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1003
|
+
}
|
|
1004
|
+
function assembleSlice(entries, lookupCap) {
|
|
1005
|
+
const top = Object.fromEntries(entries.filter((e) => e.plan.item === void 0).map((e) => [e.plan.path, e.value]));
|
|
1006
|
+
const groups = groupItems(entries);
|
|
1007
|
+
const arrays = [...new Set(groups.map((g) => g.arrayPath))].map((arrayPath) => [arrayPath, groups.filter((g) => g.arrayPath === arrayPath).toSorted((a, b) => compareKeys(a.key, b.key)).map((g) => assembleItem(g, lookupCap))]);
|
|
1008
|
+
return {
|
|
1009
|
+
...top,
|
|
1010
|
+
...Object.fromEntries(arrays)
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
//#endregion
|
|
819
1014
|
//#region src/builtins/composer/composition-runtime.ts
|
|
820
1015
|
/**
|
|
821
1016
|
* One running composition: per-field state, writes to the composed device, and
|
|
@@ -864,10 +1059,11 @@ function pendingReason(cap, pending) {
|
|
|
864
1059
|
* not land through `onAsyncFailure`: the cap is marked write-pending by name
|
|
865
1060
|
* and re-written whole on the next pass — no retry timer (D3).
|
|
866
1061
|
*
|
|
867
|
-
* Slice 2b (D670): stateful
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* event-bus failure nor a refused write can replay
|
|
1062
|
+
* Slice 2b (D670): evaluation — stateful memory, state-derived events before
|
|
1063
|
+
* their write, and reads of the composition's OWN fields answered from the
|
|
1064
|
+
* records rather than the hub (D670 amendment) — is `composition-evaluation.ts`.
|
|
1065
|
+
* Neither an event-bus failure nor a refused write can replay an event, and the
|
|
1066
|
+
* hub's echo of the runtime's own write re-runs nothing.
|
|
871
1067
|
*/
|
|
872
1068
|
/**
|
|
873
1069
|
* A refusal nothing on this side can retry away: another owner holds the
|
|
@@ -880,9 +1076,6 @@ var TERMINAL_REFUSALS = new Set([
|
|
|
880
1076
|
"invalid-slice",
|
|
881
1077
|
"unknown-device"
|
|
882
1078
|
]);
|
|
883
|
-
function strictFiniteAdvance(previous, next) {
|
|
884
|
-
return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
|
|
885
|
-
}
|
|
886
1079
|
var CompositionRuntime = class {
|
|
887
1080
|
deps;
|
|
888
1081
|
records;
|
|
@@ -906,11 +1099,31 @@ var CompositionRuntime = class {
|
|
|
906
1099
|
releaseFlaps = /* @__PURE__ */ new Map();
|
|
907
1100
|
/** Caps the target refused for good (`TERMINAL_REFUSALS`): never written again by this runtime. */
|
|
908
1101
|
terminal = /* @__PURE__ */ new Map();
|
|
1102
|
+
/** What `composition-evaluation.ts` reads and commits through. */
|
|
1103
|
+
evaluation;
|
|
909
1104
|
constructor(deps) {
|
|
910
1105
|
this.deps = deps;
|
|
911
1106
|
this.startedAtMs = deps.now();
|
|
912
1107
|
this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
|
|
913
1108
|
deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
|
|
1109
|
+
const eventContext = {
|
|
1110
|
+
deviceId: deps.target.id,
|
|
1111
|
+
blockId: deps.blockId,
|
|
1112
|
+
events: deps.events,
|
|
1113
|
+
logger: deps.logger
|
|
1114
|
+
};
|
|
1115
|
+
this.evaluation = {
|
|
1116
|
+
records: () => this.records,
|
|
1117
|
+
commit: (records) => {
|
|
1118
|
+
this.records = records;
|
|
1119
|
+
},
|
|
1120
|
+
read: deps.read,
|
|
1121
|
+
...deps.isOwnRead === void 0 ? {} : { isOwnRead: deps.isOwnRead },
|
|
1122
|
+
eventPlans: deps.eventPlans,
|
|
1123
|
+
startedAtMs: this.startedAtMs,
|
|
1124
|
+
emit: (plan, timestamp) => emitStateDerivedEvent(eventContext, plan, timestamp),
|
|
1125
|
+
logTransition: (record, outcome) => this.logTransition(record, outcome)
|
|
1126
|
+
};
|
|
914
1127
|
}
|
|
915
1128
|
start() {
|
|
916
1129
|
if (this.disabledReason !== null) return;
|
|
@@ -919,7 +1132,7 @@ var CompositionRuntime = class {
|
|
|
919
1132
|
onSourcesChanged(sliceKeys) {
|
|
920
1133
|
if (this.disabledReason !== null) return;
|
|
921
1134
|
this.guarded("source", true, () => {
|
|
922
|
-
const affected = [...this.records.values()].filter((r) => r.plan.reads.some((read) => sliceKeys.has(compositionSliceKey(read.source, read.cap)))).map((r) => fieldKey(r.plan));
|
|
1135
|
+
const affected = [...this.records.values()].filter((r) => r.plan.reads.some((read) => ownKeyOf(this.evaluation, read) === null && sliceKeys.has(compositionSliceKey(read.source, read.cap)))).map((r) => fieldKey(r.plan));
|
|
923
1136
|
if (affected.length > 0) this.pass(affected, "source");
|
|
924
1137
|
});
|
|
925
1138
|
}
|
|
@@ -1068,12 +1281,12 @@ var CompositionRuntime = class {
|
|
|
1068
1281
|
pass(keys, trigger) {
|
|
1069
1282
|
const initial = trigger === "start";
|
|
1070
1283
|
const now = this.deps.now();
|
|
1071
|
-
const calculated = this.
|
|
1284
|
+
const calculated = evaluateWithOwnReaders(this.evaluation, keys.filter((k) => {
|
|
1072
1285
|
const role = this.records.get(k)?.plan.role;
|
|
1073
1286
|
return role === "derived" || role === "accumulator";
|
|
1074
1287
|
}), now);
|
|
1075
1288
|
const followers = [...this.records.values()].filter((r) => r.plan.role === "clock" ? initial || calculated.valueCaps.has(r.plan.cap) : r.plan.role === "constant" && initial).map((r) => fieldKey(r.plan));
|
|
1076
|
-
const extra = this.
|
|
1289
|
+
const extra = evaluateFields(this.evaluation, followers, now);
|
|
1077
1290
|
const evaluatedCaps = new Set([...keys, ...followers].flatMap((k) => {
|
|
1078
1291
|
const cap = this.records.get(k)?.plan.cap;
|
|
1079
1292
|
return cap === void 0 ? [] : [cap];
|
|
@@ -1083,89 +1296,6 @@ var CompositionRuntime = class {
|
|
|
1083
1296
|
this.updateFailure();
|
|
1084
1297
|
this.updateAvailability();
|
|
1085
1298
|
}
|
|
1086
|
-
evaluate(keys, now) {
|
|
1087
|
-
const changed = /* @__PURE__ */ new Set();
|
|
1088
|
-
const valueCaps = /* @__PURE__ */ new Set();
|
|
1089
|
-
const next = new Map(this.records);
|
|
1090
|
-
try {
|
|
1091
|
-
for (const key of keys) {
|
|
1092
|
-
const record = next.get(key);
|
|
1093
|
-
if (record === void 0) continue;
|
|
1094
|
-
const candidateMemory = cloneStatefulExpressionMemory(record.expressionMemory);
|
|
1095
|
-
const outcome = evaluateCompositionField({
|
|
1096
|
-
source: record.plan.source,
|
|
1097
|
-
read: this.deps.read,
|
|
1098
|
-
now,
|
|
1099
|
-
stateful: {
|
|
1100
|
-
currentOutput: currentOutput(record),
|
|
1101
|
-
expressionMemory: candidateMemory,
|
|
1102
|
-
startedAtMs: this.startedAtMs,
|
|
1103
|
-
callPathPrefix: fieldKey(record.plan)
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1106
|
-
const expressionSucceeded = outcome.state !== "unavailable";
|
|
1107
|
-
let evaluated = {
|
|
1108
|
-
...record,
|
|
1109
|
-
outcome,
|
|
1110
|
-
...expressionSucceeded ? { expressionMemory: candidateMemory } : {}
|
|
1111
|
-
};
|
|
1112
|
-
const eventPlans = this.deps.eventPlans.filter((eventPlan) => eventPlan.fieldKey === fieldKey(record.plan));
|
|
1113
|
-
if (expressionSucceeded && eventPlans.length > 0 && strictFiniteAdvance(record.eventHighWater, outcome.value)) {
|
|
1114
|
-
const timestamp = outcome.value;
|
|
1115
|
-
evaluated = {
|
|
1116
|
-
...evaluated,
|
|
1117
|
-
eventHighWater: timestamp
|
|
1118
|
-
};
|
|
1119
|
-
next.set(key, evaluated);
|
|
1120
|
-
this.records = next;
|
|
1121
|
-
for (const eventPlan of eventPlans) this.emitStateEvent(eventPlan, timestamp);
|
|
1122
|
-
} else next.set(key, evaluated);
|
|
1123
|
-
if (sameOutcome(record.outcome, outcome)) continue;
|
|
1124
|
-
if (record.outcome?.state !== outcome.state) this.logTransition(record, outcome);
|
|
1125
|
-
if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
|
|
1126
|
-
changed.add(key);
|
|
1127
|
-
}
|
|
1128
|
-
} finally {
|
|
1129
|
-
this.records = next;
|
|
1130
|
-
}
|
|
1131
|
-
return {
|
|
1132
|
-
keys: changed,
|
|
1133
|
-
valueCaps
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
emitStateEvent(plan, timestamp) {
|
|
1137
|
-
const extras = {
|
|
1138
|
-
tags: { deviceId: this.deps.target.id },
|
|
1139
|
-
meta: {
|
|
1140
|
-
blockId: this.deps.blockId,
|
|
1141
|
-
cap: plan.cap,
|
|
1142
|
-
eventName: plan.eventName,
|
|
1143
|
-
category: plan.category,
|
|
1144
|
-
timestamp
|
|
1145
|
-
}
|
|
1146
|
-
};
|
|
1147
|
-
try {
|
|
1148
|
-
if (plan.category !== EventCategory.DoorbellOnPressed) throw new Error(`unsupported state-derived event category \`${plan.category}\``);
|
|
1149
|
-
const event = createEvent(EventCategory.DoorbellOnPressed, {
|
|
1150
|
-
type: "device",
|
|
1151
|
-
id: this.deps.target.id,
|
|
1152
|
-
deviceId: this.deps.target.id
|
|
1153
|
-
}, {
|
|
1154
|
-
deviceId: this.deps.target.id,
|
|
1155
|
-
timestamp
|
|
1156
|
-
});
|
|
1157
|
-
this.deps.events.emit(event);
|
|
1158
|
-
this.deps.logger.debug("composition: state-derived event emitted", extras);
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
this.deps.logger.warn("composition: state-derived event delivery failed", {
|
|
1161
|
-
...extras,
|
|
1162
|
-
meta: {
|
|
1163
|
-
...extras.meta,
|
|
1164
|
-
error: errorText(error)
|
|
1165
|
-
}
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
1299
|
recordsOf(cap) {
|
|
1170
1300
|
return [...this.records.values()].filter((r) => r.plan.cap === cap);
|
|
1171
1301
|
}
|
|
@@ -1529,6 +1659,29 @@ var ConfirmedSeed = class {
|
|
|
1529
1659
|
* (`HeldClaims`, C-1).
|
|
1530
1660
|
*/
|
|
1531
1661
|
/**
|
|
1662
|
+
* Review M3: an ADDED `restored` cap seeded from NOTHING says so, once per
|
|
1663
|
+
* runtime build, on its device. After a restart its state comes from the
|
|
1664
|
+
* target's runtime-state row; a row that lost the slice (D677: 615's doorbell,
|
|
1665
|
+
* dropped at a hub boot) used to surface as "not seeded yet" and now seeds a
|
|
1666
|
+
* clean `{ null, 0 }` — a lifetime count silently reset unless this line
|
|
1667
|
+
* names it. A first composition of a device that never had the slice logs it
|
|
1668
|
+
* too: the line cannot tell the two apart, and says both.
|
|
1669
|
+
*/
|
|
1670
|
+
function reportSeedlessRestoredCaps(input) {
|
|
1671
|
+
for (const cap of addedCapsOf(input.features)) {
|
|
1672
|
+
if (input.lookupCap(cap)?.durability !== "restored") continue;
|
|
1673
|
+
if ((input.seed.byCap.get(cap) ?? null) !== null) continue;
|
|
1674
|
+
input.logger.info("composed restored capability seeded from nothing — the target had no slice for it (a first composition, or a lost runtime-state row)", {
|
|
1675
|
+
tags: { deviceId: input.deviceId },
|
|
1676
|
+
meta: {
|
|
1677
|
+
blockId: input.blockId,
|
|
1678
|
+
cap,
|
|
1679
|
+
seededFrom: "none"
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1532
1685
|
* Read every affected effective capability exactly once, before the composer
|
|
1533
1686
|
* grafts or claims any of them. A null slice is a successful empty seed; a
|
|
1534
1687
|
* rejection is deliberately left to the caller so ownership cannot begin.
|
|
@@ -2492,6 +2645,14 @@ var Composer = class {
|
|
|
2492
2645
|
let targetSeed;
|
|
2493
2646
|
if (p.target.kind === "existing" && p.target.deviceId !== null && p.target.waiting === null && p.target.gone === null && p.block.enabled && p.fields.length > 0) try {
|
|
2494
2647
|
targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
|
|
2648
|
+
reportSeedlessRestoredCaps({
|
|
2649
|
+
logger: this.deps.logger,
|
|
2650
|
+
blockId: p.block.id,
|
|
2651
|
+
deviceId: p.target.deviceId,
|
|
2652
|
+
features: p.target.features,
|
|
2653
|
+
seed: targetSeed,
|
|
2654
|
+
lookupCap: this.deps.lookupCap
|
|
2655
|
+
});
|
|
2495
2656
|
} catch (err) {
|
|
2496
2657
|
const reason = `the target device state could not be read before ownership (${errMsg(err)}) — nothing is grafted or claimed until it is; retried on the next apply`;
|
|
2497
2658
|
this.deps.logger.warn(previous?.runtime === null || previous === void 0 ? "customization target state could not be read — ownership is held" : "customization edit target state could not be read — the previous runtime stays installed", {
|
|
@@ -2604,6 +2765,7 @@ var Composer = class {
|
|
|
2604
2765
|
const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
|
|
2605
2766
|
const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
|
|
2606
2767
|
const nativeCaps = new Set(self?.native ?? []);
|
|
2768
|
+
const ownKey = targetKey ?? compositionSourceKey(composedDeviceRef(p.block.id));
|
|
2607
2769
|
return new CompositionRuntime({
|
|
2608
2770
|
blockId: p.block.id,
|
|
2609
2771
|
fields: p.fields,
|
|
@@ -2613,6 +2775,7 @@ var Composer = class {
|
|
|
2613
2775
|
lookupCap: this.deps.lookupCap,
|
|
2614
2776
|
...seed === void 0 ? {} : { initialByCap: seed.byCap },
|
|
2615
2777
|
read: (read) => this.deps.tracker.read(read, compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
|
|
2778
|
+
isOwnRead: (read) => compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
|
|
2616
2779
|
now: this.deps.now,
|
|
2617
2780
|
timer: this.deps.newTimer(),
|
|
2618
2781
|
logger: this.deps.logger.child("runtime")
|
|
@@ -2838,7 +3001,10 @@ var NodeDeadlineTimer = class {
|
|
|
2838
3001
|
* this runner must never answer `{}`.
|
|
2839
3002
|
* - The context is `createContext(stableId, id)` on a FOREIGN device id: the
|
|
2840
3003
|
* second use of the forked-runner native path D659 lists as unverified live
|
|
2841
|
-
* (D659 live item 1). Task 16 step 4.1 is its live proof.
|
|
3004
|
+
* (D659 live item 1). Task 16 step 4.1 is its live proof. Its EMPTY runtime
|
|
3005
|
+
* state is correct and deliberate (D677): no device is built from it and no
|
|
3006
|
+
* slice is written through it — the graft only registers method-serving
|
|
3007
|
+
* natives, and `getStatus` reads the hub's merged slice.
|
|
2842
3008
|
* - `ungraft` removes only what THIS host registered, through the context's
|
|
2843
3009
|
* optional `unregisterNativeCap`. An older kernel without it leaves the graft
|
|
2844
3010
|
* routable until the composer respawns — warned once per device, never
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CompositionEventPlan, CompositionFieldRead, FieldOutcome, IScopedLogger, SourceReader } from '@camstack/types';
|
|
2
|
+
import { CompositionEventEmitter } from './composition-target.js';
|
|
3
|
+
import { FieldRecord } from './field-record.js';
|
|
4
|
+
export interface EvaluationDelta {
|
|
5
|
+
readonly keys: ReadonlySet<string>;
|
|
6
|
+
readonly valueCaps: ReadonlySet<string>;
|
|
7
|
+
}
|
|
8
|
+
/** What an evaluation reads from, and writes back to, the runtime that owns the records. */
|
|
9
|
+
export interface EvaluationHost {
|
|
10
|
+
records(): ReadonlyMap<string, FieldRecord>;
|
|
11
|
+
commit(records: ReadonlyMap<string, FieldRecord>): void;
|
|
12
|
+
readonly read: SourceReader;
|
|
13
|
+
readonly isOwnRead?: (read: CompositionFieldRead) => boolean;
|
|
14
|
+
readonly eventPlans: readonly CompositionEventPlan[];
|
|
15
|
+
readonly startedAtMs: number;
|
|
16
|
+
emit(plan: CompositionEventPlan, timestamp: number): void;
|
|
17
|
+
logTransition(record: FieldRecord, outcome: FieldOutcome): void;
|
|
18
|
+
}
|
|
19
|
+
/** The record key an own read is answered from; null when `read` is a source's. */
|
|
20
|
+
export declare function ownKeyOf(host: EvaluationHost, read: CompositionFieldRead): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* `keys`, derived before accumulators, then — in the same pass — every
|
|
23
|
+
* derived/accumulator field that reads an own field that changed. Bounded by
|
|
24
|
+
* the record count: the validator refuses a cycle, so a chain is at most that
|
|
25
|
+
* long (D6).
|
|
26
|
+
*/
|
|
27
|
+
export declare function evaluateWithOwnReaders(host: EvaluationHost, keys: readonly string[], now: number): EvaluationDelta;
|
|
28
|
+
/** Evaluate `keys` in order against the records in progress; commit whatever was evaluated, even on a throw. */
|
|
29
|
+
export declare function evaluateFields(host: EvaluationHost, keys: readonly string[], now: number): EvaluationDelta;
|
|
30
|
+
export interface StateEventContext {
|
|
31
|
+
readonly deviceId: number;
|
|
32
|
+
readonly blockId: string;
|
|
33
|
+
readonly events: CompositionEventEmitter;
|
|
34
|
+
readonly logger: IScopedLogger;
|
|
35
|
+
}
|
|
36
|
+
/** Emit one state-derived event; a delivery failure is logged, never thrown (D670). */
|
|
37
|
+
export declare function emitStateDerivedEvent(ctx: StateEventContext, plan: CompositionEventPlan, timestamp: number): void;
|