@camstack/system 1.2.314 → 1.2.315
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/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-plan.d.ts +8 -1
- package/dist/builtins/composer/composer.addon.js +191 -29
- package/dist/builtins/composer/composer.addon.mjs +195 -33
- package/dist/builtins/composer/composer.d.ts +5 -1
- package/dist/builtins/composer/composition-runtime.d.ts +10 -1
- package/dist/builtins/composer/existing-target.d.ts +9 -0
- package/dist/builtins/composer/field-record.d.ts +12 -3
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/composition-sources.d.ts +3 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +3 -103
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -102
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/binding-mirror.d.ts +1 -0
- package/dist/builtins/doorbell/doorbell-composition-migration.d.ts +59 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +3 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +315 -8
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +315 -8
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/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 +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/composition-sources-DYqQLRzF.js +122 -0
- package/dist/composition-sources-yonsRneP.mjs +111 -0
- package/dist/{dist-BtNkk3Qz.js → dist-DVKv5i-e.js} +611 -299
- package/dist/{dist-cH4t1ZEm.mjs → dist-MzYJVCLE.mjs} +582 -300
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-DQYHfZ0S.mjs → retired-settings-keys-8yY2sTc-.mjs} +1 -1
- package/dist/{retired-settings-keys-B7imb-qH.js → retired-settings-keys-iY5I0nKM.js} +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as parseDeviceStateChanged, t as isRecord$
|
|
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-MzYJVCLE.mjs";
|
|
2
|
+
import { n as parseDeviceStateChanged, t as isRecord$2 } from "../../device-state-changed-DoBbkyEI.mjs";
|
|
3
|
+
import { n as normalizedNativeOwner } from "../../composition-sources-yonsRneP.mjs";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
//#region src/builtins/composer/composed-device.ts
|
|
5
6
|
/**
|
|
@@ -419,6 +420,7 @@ var WithdrawalGate = class {
|
|
|
419
420
|
var PLACEHOLDER_SHAPE = "placeholder";
|
|
420
421
|
var NO_PLAN = {
|
|
421
422
|
fields: [],
|
|
423
|
+
events: [],
|
|
422
424
|
problems: [],
|
|
423
425
|
declaration: null,
|
|
424
426
|
shape: null,
|
|
@@ -465,6 +467,7 @@ function planNew(block, target, lookupCap, rows) {
|
|
|
465
467
|
};
|
|
466
468
|
}
|
|
467
469
|
const caps = block.composition.features.flatMap((f) => {
|
|
470
|
+
if (!compositionFeatureEnabled(f)) return [];
|
|
468
471
|
const def = lookupCap(f.cap);
|
|
469
472
|
return def === null ? [] : [def];
|
|
470
473
|
});
|
|
@@ -477,6 +480,7 @@ function planNew(block, target, lookupCap, rows) {
|
|
|
477
480
|
return {
|
|
478
481
|
block,
|
|
479
482
|
fields: plan.fields,
|
|
483
|
+
events: plan.events,
|
|
480
484
|
problems: [],
|
|
481
485
|
declaration: declarationFor(declarationInput),
|
|
482
486
|
shape: shapeOf(declarationInput),
|
|
@@ -524,6 +528,7 @@ async function planExisting(input) {
|
|
|
524
528
|
...NO_PLAN,
|
|
525
529
|
block,
|
|
526
530
|
fields: plan.problems.length > 0 ? [] : plan.fields,
|
|
531
|
+
events: plan.problems.length > 0 ? [] : plan.events,
|
|
527
532
|
problems: plan.problems.map((p) => `${p.path}: ${p.message}`),
|
|
528
533
|
target: {
|
|
529
534
|
...existing,
|
|
@@ -547,6 +552,20 @@ function waitingPlan(block, ref, deviceId, waiting) {
|
|
|
547
552
|
}
|
|
548
553
|
};
|
|
549
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Runtime preparation can still fail after a complete existing-target plan.
|
|
557
|
+
* Preserve its fields and description for retry, but mark it waiting so claim
|
|
558
|
+
* reconciliation cannot treat the unprepared candidate as live.
|
|
559
|
+
*/
|
|
560
|
+
function withExistingTargetWait(p, waiting) {
|
|
561
|
+
return p.target.kind === "existing" ? {
|
|
562
|
+
...p,
|
|
563
|
+
target: {
|
|
564
|
+
...p.target,
|
|
565
|
+
waiting
|
|
566
|
+
}
|
|
567
|
+
} : p;
|
|
568
|
+
}
|
|
550
569
|
/** What `logPlanTransition` compares: problems, and for an existing target its wait or its loss. */
|
|
551
570
|
function planSummary(p) {
|
|
552
571
|
const target = p.target;
|
|
@@ -624,7 +643,7 @@ function logPlanTransition(logger, previous, p, liveTags) {
|
|
|
624
643
|
* is thrown by name, and the runtime's write path turns a throw into a named,
|
|
625
644
|
* retried failure — never a half-built array.
|
|
626
645
|
*/
|
|
627
|
-
function isRecord(value) {
|
|
646
|
+
function isRecord$1(value) {
|
|
628
647
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
629
648
|
}
|
|
630
649
|
/** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
|
|
@@ -638,7 +657,7 @@ function setPath(record, segments, value) {
|
|
|
638
657
|
const current = record[head];
|
|
639
658
|
return {
|
|
640
659
|
...record,
|
|
641
|
-
[head]: setPath(isRecord(current) ? current : {}, rest, value)
|
|
660
|
+
[head]: setPath(isRecord$1(current) ? current : {}, rest, value)
|
|
642
661
|
};
|
|
643
662
|
}
|
|
644
663
|
function itemArrayOf(group, lookupCap) {
|
|
@@ -701,6 +720,38 @@ var NO_FIELDS = /* @__PURE__ */ new Set();
|
|
|
701
720
|
function fieldKey(plan) {
|
|
702
721
|
return compositionFieldKey(plan);
|
|
703
722
|
}
|
|
723
|
+
function isRecord(value) {
|
|
724
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
725
|
+
}
|
|
726
|
+
function valueAtPath$1(value, path) {
|
|
727
|
+
return path.split(".").reduce((node, segment) => isRecord(node) ? node[segment] : void 0, value);
|
|
728
|
+
}
|
|
729
|
+
function initialValue(plan, byCap, lookupCap) {
|
|
730
|
+
const slice = byCap?.get(plan.cap);
|
|
731
|
+
if (slice === void 0 || slice === null) return void 0;
|
|
732
|
+
const item = plan.item;
|
|
733
|
+
if (item === void 0) return valueAtPath$1(slice, plan.path);
|
|
734
|
+
const itemArray = lookupCap(plan.cap)?.status?.itemArray;
|
|
735
|
+
if (itemArray === void 0 || itemArray.path !== item.arrayPath) return void 0;
|
|
736
|
+
const entries = valueAtPath$1(slice, item.arrayPath);
|
|
737
|
+
if (!Array.isArray(entries)) return void 0;
|
|
738
|
+
return valueAtPath$1(entries.find((entry) => isRecord(entry) && entry[itemArray.keyField] === item.key), plan.path);
|
|
739
|
+
}
|
|
740
|
+
function createFieldRecord(plan, byCap, lookupCap) {
|
|
741
|
+
const initial = initialValue(plan, byCap, lookupCap);
|
|
742
|
+
return {
|
|
743
|
+
plan,
|
|
744
|
+
outcome: null,
|
|
745
|
+
initial,
|
|
746
|
+
hasWritten: false,
|
|
747
|
+
written: void 0,
|
|
748
|
+
expressionMemory: createStatefulExpressionMemory(),
|
|
749
|
+
eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function currentOutput(record) {
|
|
753
|
+
return record.hasWritten ? record.written : record.initial;
|
|
754
|
+
}
|
|
704
755
|
/** The top-level field a record writes: an item leaf writes its whole array. */
|
|
705
756
|
function topLevelField(plan) {
|
|
706
757
|
return plan.item === void 0 ? plan.path : plan.item.arrayPath;
|
|
@@ -812,6 +863,11 @@ function pendingReason(cap, pending) {
|
|
|
812
863
|
* - A target whose writes are asynchronous (claims on the hub) reports what did
|
|
813
864
|
* not land through `onAsyncFailure`: the cap is marked write-pending by name
|
|
814
865
|
* and re-written whole on the next pass — no retry timer (D3).
|
|
866
|
+
*
|
|
867
|
+
* Slice 2b (D670): stateful expression memory commits only when the whole
|
|
868
|
+
* expression succeeds. A declared state-derived event advances its seeded
|
|
869
|
+
* high-water and is emitted before the corresponding target write; neither an
|
|
870
|
+
* event-bus failure nor a refused write can replay the event.
|
|
815
871
|
*/
|
|
816
872
|
/**
|
|
817
873
|
* A refusal nothing on this side can retry away: another owner holds the
|
|
@@ -824,9 +880,13 @@ var TERMINAL_REFUSALS = new Set([
|
|
|
824
880
|
"invalid-slice",
|
|
825
881
|
"unknown-device"
|
|
826
882
|
]);
|
|
883
|
+
function strictFiniteAdvance(previous, next) {
|
|
884
|
+
return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
|
|
885
|
+
}
|
|
827
886
|
var CompositionRuntime = class {
|
|
828
887
|
deps;
|
|
829
888
|
records;
|
|
889
|
+
startedAtMs;
|
|
830
890
|
seeded = /* @__PURE__ */ new Set();
|
|
831
891
|
available = null;
|
|
832
892
|
failure = null;
|
|
@@ -848,11 +908,8 @@ var CompositionRuntime = class {
|
|
|
848
908
|
terminal = /* @__PURE__ */ new Map();
|
|
849
909
|
constructor(deps) {
|
|
850
910
|
this.deps = deps;
|
|
851
|
-
this.
|
|
852
|
-
|
|
853
|
-
outcome: null,
|
|
854
|
-
written: void 0
|
|
855
|
-
}]));
|
|
911
|
+
this.startedAtMs = deps.now();
|
|
912
|
+
this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
|
|
856
913
|
deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
|
|
857
914
|
}
|
|
858
915
|
start() {
|
|
@@ -1011,15 +1068,18 @@ var CompositionRuntime = class {
|
|
|
1011
1068
|
pass(keys, trigger) {
|
|
1012
1069
|
const initial = trigger === "start";
|
|
1013
1070
|
const now = this.deps.now();
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1071
|
+
const calculated = this.evaluate(keys.filter((k) => {
|
|
1072
|
+
const role = this.records.get(k)?.plan.role;
|
|
1073
|
+
return role === "derived" || role === "accumulator";
|
|
1074
|
+
}), now);
|
|
1075
|
+
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));
|
|
1016
1076
|
const extra = this.evaluate(followers, now);
|
|
1017
1077
|
const evaluatedCaps = new Set([...keys, ...followers].flatMap((k) => {
|
|
1018
1078
|
const cap = this.records.get(k)?.plan.cap;
|
|
1019
1079
|
return cap === void 0 ? [] : [cap];
|
|
1020
1080
|
}));
|
|
1021
1081
|
const retry = new Set([...this.pending.keys()].filter((cap) => trigger === "deadline" || evaluatedCaps.has(cap)));
|
|
1022
|
-
this.writeCaps(new Set([...
|
|
1082
|
+
this.writeCaps(new Set([...calculated.keys, ...extra.keys]), retry);
|
|
1023
1083
|
this.updateFailure();
|
|
1024
1084
|
this.updateAvailability();
|
|
1025
1085
|
}
|
|
@@ -1031,15 +1091,35 @@ var CompositionRuntime = class {
|
|
|
1031
1091
|
for (const key of keys) {
|
|
1032
1092
|
const record = next.get(key);
|
|
1033
1093
|
if (record === void 0) continue;
|
|
1094
|
+
const candidateMemory = cloneStatefulExpressionMemory(record.expressionMemory);
|
|
1034
1095
|
const outcome = evaluateCompositionField({
|
|
1035
1096
|
source: record.plan.source,
|
|
1036
1097
|
read: this.deps.read,
|
|
1037
|
-
now
|
|
1098
|
+
now,
|
|
1099
|
+
stateful: {
|
|
1100
|
+
currentOutput: currentOutput(record),
|
|
1101
|
+
expressionMemory: candidateMemory,
|
|
1102
|
+
startedAtMs: this.startedAtMs,
|
|
1103
|
+
callPathPrefix: fieldKey(record.plan)
|
|
1104
|
+
}
|
|
1038
1105
|
});
|
|
1039
|
-
|
|
1106
|
+
const expressionSucceeded = outcome.state !== "unavailable";
|
|
1107
|
+
let evaluated = {
|
|
1040
1108
|
...record,
|
|
1041
|
-
outcome
|
|
1042
|
-
|
|
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);
|
|
1043
1123
|
if (sameOutcome(record.outcome, outcome)) continue;
|
|
1044
1124
|
if (record.outcome?.state !== outcome.state) this.logTransition(record, outcome);
|
|
1045
1125
|
if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
|
|
@@ -1053,6 +1133,39 @@ var CompositionRuntime = class {
|
|
|
1053
1133
|
valueCaps
|
|
1054
1134
|
};
|
|
1055
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
|
+
}
|
|
1056
1169
|
recordsOf(cap) {
|
|
1057
1170
|
return [...this.records.values()].filter((r) => r.plan.cap === cap);
|
|
1058
1171
|
}
|
|
@@ -1081,8 +1194,9 @@ var CompositionRuntime = class {
|
|
|
1081
1194
|
const records = this.recordsOf(cap);
|
|
1082
1195
|
this.logReleaseTransitions(cap, records);
|
|
1083
1196
|
const releasedRecords = records.filter(isReleased);
|
|
1084
|
-
this.records = new Map([...this.records].map(([k, r]) => releasedRecords.includes(r) && r.
|
|
1197
|
+
this.records = new Map([...this.records].map(([k, r]) => releasedRecords.includes(r) && r.hasWritten ? [k, {
|
|
1085
1198
|
...r,
|
|
1199
|
+
hasWritten: false,
|
|
1086
1200
|
written: void 0
|
|
1087
1201
|
}] : [k, r]));
|
|
1088
1202
|
const withhold = this.deps.target.withhold;
|
|
@@ -1144,7 +1258,7 @@ var CompositionRuntime = class {
|
|
|
1144
1258
|
this.markWritten(cap, entries);
|
|
1145
1259
|
}
|
|
1146
1260
|
patchCap(cap, touched) {
|
|
1147
|
-
const entries = this.entriesFor(cap, this.recordsOf(cap).filter((r) => touched.has(fieldKey(r.plan)) && writable(r) !== r.written));
|
|
1261
|
+
const entries = this.entriesFor(cap, this.recordsOf(cap).filter((r) => touched.has(fieldKey(r.plan)) && (!r.hasWritten || writable(r) !== r.written)));
|
|
1148
1262
|
if (entries.length === 0) return;
|
|
1149
1263
|
if (this.write(cap, "patch", entries)) this.markWritten(cap, entries);
|
|
1150
1264
|
}
|
|
@@ -1276,6 +1390,7 @@ var CompositionRuntime = class {
|
|
|
1276
1390
|
const written = new Map(entries.map((e) => [fieldKey(e.plan), e.value]));
|
|
1277
1391
|
this.records = new Map([...this.records].map(([k, r]) => r.plan.cap === cap && written.has(k) ? [k, {
|
|
1278
1392
|
...r,
|
|
1393
|
+
hasWritten: true,
|
|
1279
1394
|
written: written.get(k)
|
|
1280
1395
|
}] : [k, r]));
|
|
1281
1396
|
}
|
|
@@ -1413,6 +1528,16 @@ var ConfirmedSeed = class {
|
|
|
1413
1528
|
* is NOT decided here from the plan: it follows whether the claim has landed
|
|
1414
1529
|
* (`HeldClaims`, C-1).
|
|
1415
1530
|
*/
|
|
1531
|
+
/**
|
|
1532
|
+
* Read every affected effective capability exactly once, before the composer
|
|
1533
|
+
* grafts or claims any of them. A null slice is a successful empty seed; a
|
|
1534
|
+
* rejection is deliberately left to the caller so ownership cannot begin.
|
|
1535
|
+
*/
|
|
1536
|
+
async function readExistingTargetSeeds(targetDeviceId, caps, read) {
|
|
1537
|
+
const byCap = /* @__PURE__ */ new Map();
|
|
1538
|
+
for (const cap of new Set(caps)) byCap.set(cap, await read(targetDeviceId, cap));
|
|
1539
|
+
return { byCap };
|
|
1540
|
+
}
|
|
1416
1541
|
/** The reason every ungraft of a gone target carries (Task 9 rule 5 / Task 10 rule 2). */
|
|
1417
1542
|
var TARGET_GONE_UNGRAFT_REASON = "target device gone";
|
|
1418
1543
|
/**
|
|
@@ -1746,14 +1871,17 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
|
|
|
1746
1871
|
reason: "the composed device has not been constructed yet"
|
|
1747
1872
|
};
|
|
1748
1873
|
const runtime = entry.runtime;
|
|
1749
|
-
if (runtime === null)
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1874
|
+
if (runtime === null) {
|
|
1875
|
+
if (target.kind !== "existing" || target.features.length === 0) return {
|
|
1876
|
+
blockId,
|
|
1877
|
+
status: "running"
|
|
1878
|
+
};
|
|
1879
|
+
return {
|
|
1880
|
+
blockId,
|
|
1881
|
+
status: "starting",
|
|
1882
|
+
reason: NOT_STARTED
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1757
1885
|
if (unsynced) return {
|
|
1758
1886
|
blockId,
|
|
1759
1887
|
status: "starting",
|
|
@@ -2361,8 +2489,30 @@ var Composer = class {
|
|
|
2361
2489
|
});
|
|
2362
2490
|
continue;
|
|
2363
2491
|
}
|
|
2492
|
+
let targetSeed;
|
|
2493
|
+
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
|
+
targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
|
|
2495
|
+
} catch (err) {
|
|
2496
|
+
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
|
+
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", {
|
|
2498
|
+
tags: { deviceId: p.target.deviceId },
|
|
2499
|
+
meta: {
|
|
2500
|
+
blockId: p.block.id,
|
|
2501
|
+
error: errMsg(err)
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2504
|
+
if (previous !== void 0) next.set(p.block.id, previous);
|
|
2505
|
+
else next.set(p.block.id, {
|
|
2506
|
+
planned: withExistingTargetWait(p, reason),
|
|
2507
|
+
device: null,
|
|
2508
|
+
runtime: null,
|
|
2509
|
+
appliedName: null,
|
|
2510
|
+
rebuildError: null,
|
|
2511
|
+
heldFields: []
|
|
2512
|
+
});
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2364
2515
|
const heldFields = this.heldFieldsFor(p, previous);
|
|
2365
|
-
previous?.runtime?.stop();
|
|
2366
2516
|
const operatorAction = previous === void 0 || restart.has(p.block.id) || previous.planned.block.updatedAt !== p.block.updatedAt;
|
|
2367
2517
|
const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
|
|
2368
2518
|
if (operatorAction) for (const f of p.fields) for (const r of f.reads) {
|
|
@@ -2370,6 +2520,7 @@ var Composer = class {
|
|
|
2370
2520
|
if (key !== targetKey) reset.add(key);
|
|
2371
2521
|
}
|
|
2372
2522
|
const writer = this.writerFor(p, device);
|
|
2523
|
+
const runtime = writer === null || p.fields.length === 0 ? null : this.newRuntime(p, writer, targetSeed);
|
|
2373
2524
|
if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
|
|
2374
2525
|
deviceId: p.target.deviceId,
|
|
2375
2526
|
ref: p.target.ref,
|
|
@@ -2378,7 +2529,7 @@ var Composer = class {
|
|
|
2378
2529
|
enabled: p.block.enabled,
|
|
2379
2530
|
settled: p.problems.length === 0
|
|
2380
2531
|
});
|
|
2381
|
-
|
|
2532
|
+
previous?.runtime?.stop();
|
|
2382
2533
|
if (runtime !== null) fresh.add(runtime);
|
|
2383
2534
|
next.set(p.block.id, {
|
|
2384
2535
|
planned: p,
|
|
@@ -2449,15 +2600,18 @@ var Composer = class {
|
|
|
2449
2600
|
if (previous.runtime === null) return previous.heldFields;
|
|
2450
2601
|
return heldFieldsOf(previous.runtime.state().fields, `the composition no longer plans — the field keeps its last value on the device: ${p.problems.join("; ")}`);
|
|
2451
2602
|
}
|
|
2452
|
-
newRuntime(p, target) {
|
|
2603
|
+
newRuntime(p, target, seed) {
|
|
2453
2604
|
const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
|
|
2454
2605
|
const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
|
|
2455
2606
|
const nativeCaps = new Set(self?.native ?? []);
|
|
2456
2607
|
return new CompositionRuntime({
|
|
2457
2608
|
blockId: p.block.id,
|
|
2458
2609
|
fields: p.fields,
|
|
2610
|
+
eventPlans: p.events,
|
|
2611
|
+
events: this.deps.events,
|
|
2459
2612
|
target,
|
|
2460
2613
|
lookupCap: this.deps.lookupCap,
|
|
2614
|
+
...seed === void 0 ? {} : { initialByCap: seed.byCap },
|
|
2461
2615
|
read: (read) => this.deps.tracker.read(read, compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
|
|
2462
2616
|
now: this.deps.now,
|
|
2463
2617
|
timer: this.deps.newTimer(),
|
|
@@ -2890,7 +3044,7 @@ function entriesOf(request) {
|
|
|
2890
3044
|
}))];
|
|
2891
3045
|
}
|
|
2892
3046
|
function valueAtPath(slice, path) {
|
|
2893
|
-
return path.split(".").reduce((node, segment) => isRecord$
|
|
3047
|
+
return path.split(".").reduce((node, segment) => isRecord$2(node) ? node[segment] : void 0, slice);
|
|
2894
3048
|
}
|
|
2895
3049
|
function onlineOf(slice) {
|
|
2896
3050
|
const online = slice["online"];
|
|
@@ -3555,12 +3709,12 @@ var SourceTracker = class {
|
|
|
3555
3709
|
var DECLARED_INTEGRATION_NAME_UNUSED = "Blocks";
|
|
3556
3710
|
/** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
|
|
3557
3711
|
function parseNativeShadowChanged(data) {
|
|
3558
|
-
if (!isRecord$
|
|
3712
|
+
if (!isRecord$2(data)) return null;
|
|
3559
3713
|
const deviceId = data["deviceId"];
|
|
3560
3714
|
const capName = data["capName"];
|
|
3561
3715
|
const native = data["native"];
|
|
3562
3716
|
if (typeof deviceId !== "number" || typeof capName !== "string") return null;
|
|
3563
|
-
if (!isRecord$
|
|
3717
|
+
if (!isRecord$2(native)) return null;
|
|
3564
3718
|
return {
|
|
3565
3719
|
deviceId,
|
|
3566
3720
|
capName,
|
|
@@ -3581,7 +3735,10 @@ var ComposerAddon = class extends BaseAddon {
|
|
|
3581
3735
|
const bindings = await api.deviceManager.getBindings.query({ deviceId });
|
|
3582
3736
|
return {
|
|
3583
3737
|
kind: "caps",
|
|
3584
|
-
caps: new Map(bindings.entries.
|
|
3738
|
+
caps: new Map(bindings.entries.flatMap((entry) => {
|
|
3739
|
+
const owner = normalizedNativeOwner(entry.nativeAddonId);
|
|
3740
|
+
return owner === null ? [] : [[entry.capName, owner]];
|
|
3741
|
+
}))
|
|
3585
3742
|
};
|
|
3586
3743
|
} catch (err) {
|
|
3587
3744
|
return {
|
|
@@ -3693,6 +3850,11 @@ var ComposerAddon = class extends BaseAddon {
|
|
|
3693
3850
|
releaseClaim: (input) => api.deviceState.releaseClaim.mutate(input),
|
|
3694
3851
|
listClaims: (input) => api.deviceState.listClaims.query(input)
|
|
3695
3852
|
},
|
|
3853
|
+
events: this.ctx.eventBus,
|
|
3854
|
+
readExistingTargetCap: async (deviceId, capName) => await api.deviceState.getCapSlice.query({
|
|
3855
|
+
deviceId,
|
|
3856
|
+
capName
|
|
3857
|
+
}) ?? null,
|
|
3696
3858
|
describeTarget: readBoundCaps,
|
|
3697
3859
|
grafts,
|
|
3698
3860
|
now: () => Date.now(),
|
|
@@ -2,7 +2,7 @@ import { CapabilityLookup, ComposerApplyInput, ComposerApplyResult, CompositionB
|
|
|
2
2
|
import { DeviceStateChangedData } from '../shared/device-state-changed.js';
|
|
3
3
|
import { ComposerDeclare, ComposerDevicePorts } from './composed-device-set.js';
|
|
4
4
|
import { PersistedComposedRow } from './composition-declarations.js';
|
|
5
|
-
import { DeadlineTimer } from './composition-runtime.js';
|
|
5
|
+
import { CompositionEventEmitter, DeadlineTimer } from './composition-runtime.js';
|
|
6
6
|
import { ComposerClaimsApi, ComposerGraftPorts, DescribeTarget } from './existing-target.js';
|
|
7
7
|
import { SourceBindingsChangeReason, SourceTracker } from './source-tracker.js';
|
|
8
8
|
export type { ComposerDerivedSpec } from './composer-plan.js';
|
|
@@ -31,6 +31,10 @@ export interface ComposerDeps {
|
|
|
31
31
|
readonly devices: ComposerDevicePorts;
|
|
32
32
|
/** The hub's per-field claims (D663): the write target's methods and the boot seed. */
|
|
33
33
|
readonly claims: ComposerClaimsApi;
|
|
34
|
+
/** Typed state-derived events emitted by composed capability fields. */
|
|
35
|
+
readonly events: CompositionEventEmitter;
|
|
36
|
+
/** Effective target state, read before an existing-target runtime owns any field. */
|
|
37
|
+
readonly readExistingTargetCap: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
|
|
34
38
|
/** ONE `getBindings` per target description read (rule 1). */
|
|
35
39
|
readonly describeTarget: DescribeTarget;
|
|
36
40
|
/** `GraftHost` (Task 10): a method-serving native for each ADDED cap. Absent, nothing is grafted. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CapabilityLookup, ClaimOutcome, CompositionFieldPlan, CompositionFieldState, IScopedLogger, SourceReader } from '@camstack/types';
|
|
1
|
+
import { CapabilityLookup, ClaimOutcome, CompositionEventPlan, CompositionFieldPlan, CompositionFieldState, IScopedLogger, SourceReader, SystemEvent } from '@camstack/types';
|
|
2
2
|
import { SlicePatch } from './slice-assembler.js';
|
|
3
3
|
import { ReleasedField } from './field-record.js';
|
|
4
4
|
export type { ReleasedField } from './field-record.js';
|
|
@@ -47,11 +47,18 @@ export interface DeadlineTimer {
|
|
|
47
47
|
arm(at: number, fire: () => void): void;
|
|
48
48
|
disarm(): void;
|
|
49
49
|
}
|
|
50
|
+
export interface CompositionEventEmitter {
|
|
51
|
+
emit(event: SystemEvent): void;
|
|
52
|
+
}
|
|
50
53
|
export interface CompositionRuntimeDeps {
|
|
51
54
|
readonly blockId: string;
|
|
52
55
|
readonly fields: readonly CompositionFieldPlan[];
|
|
56
|
+
readonly eventPlans: readonly CompositionEventPlan[];
|
|
57
|
+
readonly events: CompositionEventEmitter;
|
|
53
58
|
readonly target: ComposedWriteTarget;
|
|
54
59
|
readonly lookupCap: CapabilityLookup;
|
|
60
|
+
/** Effective target slices captured before an existing-target runtime takes ownership. */
|
|
61
|
+
readonly initialByCap?: ReadonlyMap<string, Readonly<Record<string, unknown>> | null>;
|
|
55
62
|
readonly read: SourceReader;
|
|
56
63
|
readonly now: () => number;
|
|
57
64
|
readonly timer: DeadlineTimer;
|
|
@@ -68,6 +75,7 @@ export interface CompositionRuntimeState {
|
|
|
68
75
|
export declare class CompositionRuntime {
|
|
69
76
|
private readonly deps;
|
|
70
77
|
private records;
|
|
78
|
+
private readonly startedAtMs;
|
|
71
79
|
private seeded;
|
|
72
80
|
private available;
|
|
73
81
|
private failure;
|
|
@@ -117,6 +125,7 @@ export declare class CompositionRuntime {
|
|
|
117
125
|
private logPassError;
|
|
118
126
|
private pass;
|
|
119
127
|
private evaluate;
|
|
128
|
+
private emitStateEvent;
|
|
120
129
|
private recordsOf;
|
|
121
130
|
/** `retry`: write-pending caps this pass re-writes WHOLE, whether or not a value changed. */
|
|
122
131
|
private writeCaps;
|
|
@@ -9,6 +9,15 @@ export type ListClaimsInput = z.infer<typeof deviceStateCapability.methods.listC
|
|
|
9
9
|
export interface ComposerClaimsApi extends OwnedFieldsApi {
|
|
10
10
|
listClaims(input: ListClaimsInput): Promise<ClaimsListing>;
|
|
11
11
|
}
|
|
12
|
+
export interface ExistingTargetSeedRead {
|
|
13
|
+
readonly byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read every affected effective capability exactly once, before the composer
|
|
17
|
+
* grafts or claims any of them. A null slice is a successful empty seed; a
|
|
18
|
+
* rejection is deliberately left to the caller so ownership cannot begin.
|
|
19
|
+
*/
|
|
20
|
+
export declare function readExistingTargetSeeds(targetDeviceId: number, caps: readonly string[], read: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>): Promise<ExistingTargetSeedRead>;
|
|
12
21
|
/**
|
|
13
22
|
* The composer's grafts (`GraftHost`, Task 10): a method-serving native for each
|
|
14
23
|
* cap a customization ADDS. Grafted at PLAN time only; removed when a foreign
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { CompositionFieldItem, CompositionFieldPlan, CompositionFieldState, ExpressionValue, FieldOutcome } from '@camstack/types';
|
|
1
|
+
import { CapabilityLookup, CompositionFieldItem, CompositionFieldPlan, CompositionFieldState, ExpressionValue, FieldOutcome, StatefulExpressionMemory } from '@camstack/types';
|
|
2
2
|
export declare const NO_FIELDS: ReadonlySet<string>;
|
|
3
3
|
export interface FieldRecord {
|
|
4
4
|
readonly plan: CompositionFieldPlan;
|
|
5
5
|
readonly outcome: FieldOutcome | null;
|
|
6
|
-
/**
|
|
7
|
-
readonly
|
|
6
|
+
/** The effective target value observed before this runtime took ownership. Never changes. */
|
|
7
|
+
readonly initial: unknown;
|
|
8
|
+
/** Whether `written` is an accepted runtime answer; kept separate because every falsey value is valid. */
|
|
9
|
+
readonly hasWritten: boolean;
|
|
10
|
+
/** What the target last accepted from this runtime. */
|
|
11
|
+
readonly written: unknown;
|
|
12
|
+
readonly expressionMemory: StatefulExpressionMemory;
|
|
13
|
+
/** Last finite state value already represented by a derived event. */
|
|
14
|
+
readonly eventHighWater: number | null;
|
|
8
15
|
}
|
|
9
16
|
export declare function fieldKey(plan: CompositionFieldPlan): string;
|
|
17
|
+
export declare function createFieldRecord(plan: CompositionFieldPlan, byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null> | undefined, lookupCap: CapabilityLookup): FieldRecord;
|
|
18
|
+
export declare function currentOutput(record: FieldRecord): unknown;
|
|
10
19
|
/** The top-level field a record writes: an item leaf writes its whole array. */
|
|
11
20
|
export declare function topLevelField(plan: CompositionFieldPlan): string;
|
|
12
21
|
/** The field as a report or a log line names it: `items[desiccant].status` for an item leaf. */
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-DVKv5i-e.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
//#region src/builtins/console-logging/console-destination.ts
|
|
9
9
|
var LEVEL_RANK = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Gn as BaseAddon, Rt as logDestinationCapability } from "../../dist-MzYJVCLE.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
//#region src/builtins/console-logging/console-destination.ts
|
|
4
4
|
var LEVEL_RANK = {
|
|
@@ -7,7 +7,7 @@ export interface SourceRowView {
|
|
|
7
7
|
/** One `getBindings` entry: the cap, and the addon serving it natively. */
|
|
8
8
|
export interface BoundCapView {
|
|
9
9
|
readonly capName: string;
|
|
10
|
-
readonly nativeAddonId
|
|
10
|
+
readonly nativeAddonId?: string | null;
|
|
11
11
|
}
|
|
12
12
|
export interface CompositionSourcePorts {
|
|
13
13
|
readonly listByAddon: (addonId: string) => Promise<readonly SourceRowView[]>;
|
|
@@ -18,4 +18,6 @@ export interface CompositionDescriptions {
|
|
|
18
18
|
readonly sources: ReadonlyMap<string, CompositionSourceDescription>;
|
|
19
19
|
readonly targets: ReadonlyMap<string, CompositionTargetDescription>;
|
|
20
20
|
}
|
|
21
|
+
/** Blank binding owners are not native providers and must not turn ADD into REPLACE. */
|
|
22
|
+
export declare function normalizedNativeOwner(owner: string | null | undefined): string | null;
|
|
21
23
|
export declare function describeCompositionSources(ports: CompositionSourcePorts, sources: readonly CompositionSourceRef[], targets?: readonly CompositionSourceRef[]): Promise<CompositionDescriptions>;
|