@camstack/system 1.2.476 → 1.2.477
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/composer/composer-runtime-factory.d.ts +2 -1
- package/dist/builtins/composer/composer.addon.js +451 -4
- package/dist/builtins/composer/composer.addon.mjs +451 -4
- package/dist/builtins/composer/composer.d.ts +6 -0
- package/dist/builtins/composer/composition-runtime.d.ts +3 -0
- package/dist/builtins/composer/field-record.d.ts +7 -0
- package/dist/builtins/composer/pulse-seed-resolve.d.ts +59 -0
- package/dist/builtins/composer/pulse-seed.d.ts +75 -0
- package/dist/builtins/device-history/device-history.addon.js +62 -952
- package/dist/builtins/device-history/device-history.addon.mjs +2 -892
- package/dist/retention-1QA8e4o0.js +1024 -0
- package/dist/retention-lPgwLane.mjs +893 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { $n as planComposition, $r as DeviceType, Gr as compositionItemFieldPath, Hr as composedDeviceRef, It as composerCapability, Jr as createStatefulExpressionMemory, Kr as compositionSliceKey, Lt as compositionFieldKey, N as DeclaredDevices, Oa as EventCategory, Ot as buildCapabilityLookup, Ur as composedDeviceStableId, Vr as cloneStatefulExpressionMemory, Wi as createEvent, Wr as compositionFeatureEnabled, Xr as toExpressionValue, Yr as disarmRoseMemory, a as BaseDevice, cn as evaluateCompositionField, d as COMPOSED_ITEM_FIXED_FIELDS, dn as expressionContainsBuiltinCall, ei as errMsg, qr as compositionSourceKey, r as ALL_CAPABILITY_DEFINITIONS, ti as BaseAddon, zr as COMPOSER_ADDON_ID } from "../../dist-B_V9r61Z.mjs";
|
|
2
2
|
import { n as normalizedNativeOwner } from "../../composition-sources-B5nK7pHt.mjs";
|
|
3
|
+
import { i as DAY_MS, m as capSeriesKey, t as DEFAULT_RETENTION } from "../../retention-lPgwLane.mjs";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
//#region src/builtins/shared/device-state-changed.ts
|
|
5
6
|
/** Structural narrowing helper for untrusted bus payloads. */
|
|
@@ -1196,9 +1197,33 @@ function createFieldRecord(plan, byCap, lookupCap) {
|
|
|
1196
1197
|
hasWritten: false,
|
|
1197
1198
|
written: void 0,
|
|
1198
1199
|
expressionMemory: createStatefulExpressionMemory(),
|
|
1199
|
-
eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
|
|
1200
|
+
eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null,
|
|
1201
|
+
pulseHold: false
|
|
1200
1202
|
};
|
|
1201
1203
|
}
|
|
1204
|
+
/** Apply a pulse start seed (D938). `keep` leaves the slice seed in place. */
|
|
1205
|
+
function applyPulseSeed(record, seed) {
|
|
1206
|
+
if (seed === void 0 || seed.kind === "keep") return record;
|
|
1207
|
+
if (seed.kind === "hold") return {
|
|
1208
|
+
...record,
|
|
1209
|
+
initial: void 0,
|
|
1210
|
+
eventHighWater: null,
|
|
1211
|
+
pulseHold: true
|
|
1212
|
+
};
|
|
1213
|
+
const value = seed.value;
|
|
1214
|
+
return {
|
|
1215
|
+
...record,
|
|
1216
|
+
initial: value,
|
|
1217
|
+
eventHighWater: typeof value === "number" && Number.isFinite(value) ? value : null,
|
|
1218
|
+
pulseHold: false
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function fieldRecords(fields, byCap, lookupCap, pulseSeeds) {
|
|
1222
|
+
return new Map(fields.map((plan) => {
|
|
1223
|
+
const key = fieldKey(plan);
|
|
1224
|
+
return [key, applyPulseSeed(createFieldRecord(plan, byCap, lookupCap), pulseSeeds?.get(key))];
|
|
1225
|
+
}));
|
|
1226
|
+
}
|
|
1202
1227
|
function currentOutput(record) {
|
|
1203
1228
|
return record.hasWritten ? record.written : record.initial;
|
|
1204
1229
|
}
|
|
@@ -1252,6 +1277,7 @@ function namedField(r) {
|
|
|
1252
1277
|
function writable(record) {
|
|
1253
1278
|
const outcome = record.outcome;
|
|
1254
1279
|
if (outcome === null) return void 0;
|
|
1280
|
+
if (record.pulseHold && !record.hasWritten && (outcome.state === "unavailable" || outcome.value === null)) return;
|
|
1255
1281
|
if (outcome.state !== "unavailable") return outcome.value;
|
|
1256
1282
|
switch (record.plan.unavailable) {
|
|
1257
1283
|
case "null": return null;
|
|
@@ -1689,7 +1715,7 @@ var CompositionRuntime = class {
|
|
|
1689
1715
|
constructor(deps) {
|
|
1690
1716
|
this.deps = deps;
|
|
1691
1717
|
this.startedAtMs = deps.now();
|
|
1692
|
-
this.records =
|
|
1718
|
+
this.records = fieldRecords(deps.fields, deps.initialByCap, deps.lookupCap, deps.pulseSeeds);
|
|
1693
1719
|
deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
|
|
1694
1720
|
const eventContext = {
|
|
1695
1721
|
deviceId: deps.target.id,
|
|
@@ -2536,7 +2562,7 @@ function writerFor(deps, p, device, onClaimEvent) {
|
|
|
2536
2562
|
onClaim: (cap, event) => onClaimEvent(deviceId, cap, owner, event)
|
|
2537
2563
|
});
|
|
2538
2564
|
}
|
|
2539
|
-
function newCompositionRuntime(deps, p, target, seed) {
|
|
2565
|
+
function newCompositionRuntime(deps, p, target, seed, pulseSeeds) {
|
|
2540
2566
|
const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
|
|
2541
2567
|
const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
|
|
2542
2568
|
const nativeCaps = new Set(self?.native ?? []);
|
|
@@ -2549,6 +2575,7 @@ function newCompositionRuntime(deps, p, target, seed) {
|
|
|
2549
2575
|
target,
|
|
2550
2576
|
lookupCap: deps.lookupCap,
|
|
2551
2577
|
...seed === void 0 ? {} : { initialByCap: seed.byCap },
|
|
2578
|
+
...pulseSeeds === void 0 ? {} : { pulseSeeds },
|
|
2552
2579
|
read: (read) => deps.tracker.read(read, compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
|
|
2553
2580
|
isOwnRead: (read) => compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
|
|
2554
2581
|
now: deps.now,
|
|
@@ -2754,6 +2781,389 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
|
|
|
2754
2781
|
};
|
|
2755
2782
|
}
|
|
2756
2783
|
//#endregion
|
|
2784
|
+
//#region src/builtins/composer/pulse-seed.ts
|
|
2785
|
+
/** The last false→true in time order. A series that opens already on is not a transition. */
|
|
2786
|
+
function lastOffToOn(points, beforeOn) {
|
|
2787
|
+
const ordered = [...points].toSorted((a, b) => a.t - b.t);
|
|
2788
|
+
let previous = beforeOn;
|
|
2789
|
+
let rise = null;
|
|
2790
|
+
for (const point of ordered) {
|
|
2791
|
+
if (previous === false && point.on && Number.isFinite(point.t)) rise = point.t;
|
|
2792
|
+
previous = point.on;
|
|
2793
|
+
}
|
|
2794
|
+
return rise;
|
|
2795
|
+
}
|
|
2796
|
+
function finite(value) {
|
|
2797
|
+
return value !== null && Number.isFinite(value) ? value : null;
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* What the source proves. A failed read is `hold`, never `none`.
|
|
2801
|
+
* When the source records no history and it is off, `lastChangedAt` is the
|
|
2802
|
+
* end of the last ring: it bounds the ring from above (the press was at or
|
|
2803
|
+
* before that instant). It is used only when it is newer than the target's
|
|
2804
|
+
* native stamp — an older bound does not prove the native value.
|
|
2805
|
+
*/
|
|
2806
|
+
function proof(facts) {
|
|
2807
|
+
if (facts.history.kind === "unread") return {
|
|
2808
|
+
kind: "hold",
|
|
2809
|
+
reason: "history-unread"
|
|
2810
|
+
};
|
|
2811
|
+
if (facts.history.kind === "series") {
|
|
2812
|
+
const at = lastOffToOn(facts.history.points, facts.history.beforeOn);
|
|
2813
|
+
return at === null ? { kind: "none" } : {
|
|
2814
|
+
kind: "rise",
|
|
2815
|
+
at,
|
|
2816
|
+
via: "history"
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
if (facts.level.kind === "unread") return {
|
|
2820
|
+
kind: "hold",
|
|
2821
|
+
reason: "level-unread"
|
|
2822
|
+
};
|
|
2823
|
+
const changedAt = facts.level.kind === "off" ? finite(facts.level.changedAt) : null;
|
|
2824
|
+
const newerThanNative = changedAt !== null && (facts.nativeStamp === null || changedAt > facts.nativeStamp);
|
|
2825
|
+
if (changedAt !== null && newerThanNative) return {
|
|
2826
|
+
kind: "rise",
|
|
2827
|
+
at: changedAt,
|
|
2828
|
+
via: "changed-at-bound"
|
|
2829
|
+
};
|
|
2830
|
+
return { kind: "none" };
|
|
2831
|
+
}
|
|
2832
|
+
function claimed(owned) {
|
|
2833
|
+
return owned.kind === "stamp" || owned.kind === "never";
|
|
2834
|
+
}
|
|
2835
|
+
function decidePulseSeed(facts) {
|
|
2836
|
+
const proved = proof(facts);
|
|
2837
|
+
if (proved.kind === "hold") return claimed(facts.owned) ? { kind: "keep" } : proved;
|
|
2838
|
+
if (proved.kind === "rise") {
|
|
2839
|
+
if (facts.owned.kind === "stamp" && facts.owned.at >= proved.at) return { kind: "keep" };
|
|
2840
|
+
const count = facts.owned.kind === "stamp" ? facts.owned.count : 0;
|
|
2841
|
+
return {
|
|
2842
|
+
kind: "stamp",
|
|
2843
|
+
at: proved.at,
|
|
2844
|
+
count,
|
|
2845
|
+
via: proved.via
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
return claimed(facts.owned) ? { kind: "keep" } : {
|
|
2849
|
+
kind: "none",
|
|
2850
|
+
count: 0
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
/** One failed source blocks "no ring". A rise from another source still stands. */
|
|
2854
|
+
function mergePulseDecisions(decisions) {
|
|
2855
|
+
if (decisions.length === 0) return {
|
|
2856
|
+
kind: "none",
|
|
2857
|
+
count: 0
|
|
2858
|
+
};
|
|
2859
|
+
const stamps = decisions.flatMap((decision) => decision.kind === "stamp" ? [decision] : []);
|
|
2860
|
+
if (stamps.length > 0) return stamps.reduce((best, next) => next.at > best.at ? next : best);
|
|
2861
|
+
const hold = decisions.find((decision) => decision.kind === "hold");
|
|
2862
|
+
if (hold !== void 0) return hold;
|
|
2863
|
+
if (decisions.some((decision) => decision.kind === "keep")) return { kind: "keep" };
|
|
2864
|
+
return {
|
|
2865
|
+
kind: "none",
|
|
2866
|
+
count: 0
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
//#endregion
|
|
2870
|
+
//#region src/builtins/composer/pulse-seed-resolve.ts
|
|
2871
|
+
/**
|
|
2872
|
+
* Gather a pulse override's start seed (D938). The stored expression is not
|
|
2873
|
+
* changed. A failed read holds the field: no seed, and a claimed stamp is
|
|
2874
|
+
* not cleared (D49).
|
|
2875
|
+
*/
|
|
2876
|
+
var ROSE_CALL = String.raw`rose\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)`;
|
|
2877
|
+
function historySeriesKey(cap, fieldPath) {
|
|
2878
|
+
return capSeriesKey(cap, fieldPath);
|
|
2879
|
+
}
|
|
2880
|
+
async function resolvePulseSeeds(input) {
|
|
2881
|
+
try {
|
|
2882
|
+
return await resolveNow(input);
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
input.logger.warn("pulse override seed held — gathering the source facts threw, so nothing is seeded and nothing is cleared", {
|
|
2885
|
+
tags: { deviceId: input.deviceId },
|
|
2886
|
+
meta: {
|
|
2887
|
+
blockId: input.blockId,
|
|
2888
|
+
error: errMsg(err)
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
return /* @__PURE__ */ new Map();
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
async function resolveNow(input) {
|
|
2895
|
+
const roses = input.fields.filter(isPulseTimestamp);
|
|
2896
|
+
if (roses.length === 0) return /* @__PURE__ */ new Map();
|
|
2897
|
+
const listing = await readClaims(input);
|
|
2898
|
+
if (listing === null) return /* @__PURE__ */ new Map();
|
|
2899
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
2900
|
+
for (const field of roses) {
|
|
2901
|
+
const decision = await decideField(input, field, listing);
|
|
2902
|
+
logDecision(input, field, decision);
|
|
2903
|
+
for (const [key, seed] of seedsFor(input.fields, field, decision)) seeds.set(key, seed);
|
|
2904
|
+
}
|
|
2905
|
+
return seeds;
|
|
2906
|
+
}
|
|
2907
|
+
function isPulseTimestamp(field) {
|
|
2908
|
+
return field.source.kind === "expression" && expressionContainsBuiltinCall(field.source.expr, "rose");
|
|
2909
|
+
}
|
|
2910
|
+
async function readClaims(input) {
|
|
2911
|
+
try {
|
|
2912
|
+
const listing = await input.claims.listClaims({ ownerPrefix: input.blockId });
|
|
2913
|
+
if (listing.state === "loaded") return listing;
|
|
2914
|
+
input.logger.warn("pulse override seed held — the claims index has not loaded, so nothing is seeded and nothing is cleared", {
|
|
2915
|
+
tags: { deviceId: input.deviceId },
|
|
2916
|
+
meta: {
|
|
2917
|
+
blockId: input.blockId,
|
|
2918
|
+
reason: listing.reason
|
|
2919
|
+
}
|
|
2920
|
+
});
|
|
2921
|
+
return null;
|
|
2922
|
+
} catch (err) {
|
|
2923
|
+
input.logger.warn("pulse override seed held — the claims index could not be read, so nothing is seeded and nothing is cleared", {
|
|
2924
|
+
tags: { deviceId: input.deviceId },
|
|
2925
|
+
meta: {
|
|
2926
|
+
blockId: input.blockId,
|
|
2927
|
+
error: errMsg(err)
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
async function decideField(input, field, listing) {
|
|
2934
|
+
if (listing.state !== "loaded") return {
|
|
2935
|
+
kind: "hold",
|
|
2936
|
+
reason: "claims-unread"
|
|
2937
|
+
};
|
|
2938
|
+
const claim = listing.claims.find((row) => row.owner === input.blockId && row.deviceId === input.deviceId && row.capName === field.cap);
|
|
2939
|
+
const slice = input.targetSeed.byCap.get(field.cap) ?? null;
|
|
2940
|
+
const counter = counterField(input.fields, field);
|
|
2941
|
+
const owned = ownedOf(claim, slice, field.path, counter?.path ?? null);
|
|
2942
|
+
const nativeStamp = stampOf(slice, field.path);
|
|
2943
|
+
const pairs = rosePairs(field);
|
|
2944
|
+
if (pairs.length === 0) return { kind: "keep" };
|
|
2945
|
+
const decisions = [];
|
|
2946
|
+
for (const pair of pairs) decisions.push(await decidePair(input, pair, owned, nativeStamp));
|
|
2947
|
+
return mergePulseDecisions(decisions);
|
|
2948
|
+
}
|
|
2949
|
+
async function decidePair(input, pair, owned, nativeStamp) {
|
|
2950
|
+
const resolution = await input.sources.resolve(pair.source, false);
|
|
2951
|
+
if (resolution.kind === "missing") return { kind: "keep" };
|
|
2952
|
+
if (resolution.kind !== "found") return decidePulseSeed({
|
|
2953
|
+
owned,
|
|
2954
|
+
nativeStamp,
|
|
2955
|
+
history: { kind: "unread" },
|
|
2956
|
+
level: { kind: "unread" }
|
|
2957
|
+
});
|
|
2958
|
+
return decidePulseSeed({
|
|
2959
|
+
owned,
|
|
2960
|
+
nativeStamp,
|
|
2961
|
+
history: await historyOf(input, resolution.deviceId, pair),
|
|
2962
|
+
level: await levelOf(input, resolution.deviceId, pair)
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
async function historyOf(input, deviceId, pair) {
|
|
2966
|
+
if (!recordsField(input.lookupCap, pair.cap, pair.activeField)) return { kind: "absent" };
|
|
2967
|
+
const now = input.now();
|
|
2968
|
+
const fromMs = now - DEFAULT_RETENTION.rawDays * DAY_MS;
|
|
2969
|
+
try {
|
|
2970
|
+
const answer = await input.readPulseHistory(deviceId, pair.cap, pair.activeField, fromMs, now);
|
|
2971
|
+
if (answer.kind === "unread") return { kind: "unread" };
|
|
2972
|
+
return {
|
|
2973
|
+
kind: "series",
|
|
2974
|
+
beforeOn: answer.series.beforeOn,
|
|
2975
|
+
points: answer.series.points
|
|
2976
|
+
};
|
|
2977
|
+
} catch (err) {
|
|
2978
|
+
input.logger.warn("pulse override history read threw — treated as unread", {
|
|
2979
|
+
tags: { deviceId: input.deviceId },
|
|
2980
|
+
meta: {
|
|
2981
|
+
blockId: input.blockId,
|
|
2982
|
+
sourceDeviceId: deviceId,
|
|
2983
|
+
cap: pair.cap,
|
|
2984
|
+
field: pair.activeField,
|
|
2985
|
+
error: errMsg(err)
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
return { kind: "unread" };
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
async function levelOf(input, deviceId, pair) {
|
|
2992
|
+
try {
|
|
2993
|
+
return levelFromSlice(await input.readCap(deviceId, pair.cap), pair);
|
|
2994
|
+
} catch (err) {
|
|
2995
|
+
input.logger.warn("pulse override source slice could not be read — treated as unread", {
|
|
2996
|
+
tags: { deviceId: input.deviceId },
|
|
2997
|
+
meta: {
|
|
2998
|
+
blockId: input.blockId,
|
|
2999
|
+
sourceDeviceId: deviceId,
|
|
3000
|
+
cap: pair.cap,
|
|
3001
|
+
error: errMsg(err)
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
3004
|
+
return { kind: "unread" };
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
function levelFromSlice(slice, pair) {
|
|
3008
|
+
if (slice === null) return {
|
|
3009
|
+
kind: "off",
|
|
3010
|
+
changedAt: null
|
|
3011
|
+
};
|
|
3012
|
+
const active = slice[pair.activeField];
|
|
3013
|
+
if (typeof active !== "boolean") return { kind: "unread" };
|
|
3014
|
+
const changed = slice[pair.changedAtField];
|
|
3015
|
+
const changedAt = typeof changed === "number" && Number.isFinite(changed) ? changed : null;
|
|
3016
|
+
return active ? {
|
|
3017
|
+
kind: "on",
|
|
3018
|
+
changedAt
|
|
3019
|
+
} : {
|
|
3020
|
+
kind: "off",
|
|
3021
|
+
changedAt
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
function recordsField(lookup, cap, fieldPath) {
|
|
3025
|
+
const history = lookup(cap)?.history;
|
|
3026
|
+
if (history === void 0 || history === "none") return false;
|
|
3027
|
+
return Object.hasOwn(history.fields, fieldPath);
|
|
3028
|
+
}
|
|
3029
|
+
function ownedOf(claim, slice, timestampPath, counterPath) {
|
|
3030
|
+
if (claim === void 0 || slice === null || !claim.fields.includes(timestampPath)) return { kind: "none" };
|
|
3031
|
+
const at = slice[timestampPath];
|
|
3032
|
+
if (at === null) return { kind: "never" };
|
|
3033
|
+
if (typeof at !== "number" || !Number.isFinite(at)) return { kind: "none" };
|
|
3034
|
+
const rawCount = counterPath === null ? void 0 : slice[counterPath];
|
|
3035
|
+
return {
|
|
3036
|
+
kind: "stamp",
|
|
3037
|
+
at,
|
|
3038
|
+
count: typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount >= 0 ? rawCount : 0
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
function stampOf(slice, path) {
|
|
3042
|
+
const at = slice?.[path];
|
|
3043
|
+
return typeof at === "number" && Number.isFinite(at) ? at : null;
|
|
3044
|
+
}
|
|
3045
|
+
function counterField(fields, timestamp) {
|
|
3046
|
+
return fields.find((field) => {
|
|
3047
|
+
if (field.cap !== timestamp.cap || field.path === timestamp.path) return false;
|
|
3048
|
+
return field.source.kind === "expression" && field.source.expr === `count(${timestamp.path})`;
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
function rosePairs(field) {
|
|
3052
|
+
if (field.source.kind !== "expression") return [];
|
|
3053
|
+
const found = [];
|
|
3054
|
+
for (const match of field.source.expr.matchAll(new RegExp(ROSE_CALL, "g"))) {
|
|
3055
|
+
const pair = rosePair(field, match[1], match[2]);
|
|
3056
|
+
if (pair !== null) found.push(pair);
|
|
3057
|
+
}
|
|
3058
|
+
return found;
|
|
3059
|
+
}
|
|
3060
|
+
function rosePair(field, activeName, changedName) {
|
|
3061
|
+
if (field.source.kind !== "expression") return null;
|
|
3062
|
+
if (activeName === void 0 || changedName === void 0) return null;
|
|
3063
|
+
const active = field.source.bindings[activeName];
|
|
3064
|
+
const changed = field.source.bindings[changedName];
|
|
3065
|
+
if (active === void 0 || changed === void 0) return null;
|
|
3066
|
+
if (active.kind !== "from" || changed.kind !== "from") return null;
|
|
3067
|
+
if (active.cap !== changed.cap) return null;
|
|
3068
|
+
if (active.source.addonId !== changed.source.addonId) return null;
|
|
3069
|
+
if (active.source.stableId !== changed.source.stableId) return null;
|
|
3070
|
+
return {
|
|
3071
|
+
source: active.source,
|
|
3072
|
+
cap: active.cap,
|
|
3073
|
+
activeField: active.fieldPath,
|
|
3074
|
+
changedAtField: changed.fieldPath
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
function seedsFor(fields, timestamp, decision) {
|
|
3078
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
3079
|
+
if (decision.kind === "keep") return seeds;
|
|
3080
|
+
const counter = counterField(fields, timestamp);
|
|
3081
|
+
if (decision.kind === "hold") {
|
|
3082
|
+
seeds.set(fieldKey(timestamp), { kind: "hold" });
|
|
3083
|
+
if (counter !== void 0) seeds.set(fieldKey(counter), {
|
|
3084
|
+
kind: "value",
|
|
3085
|
+
value: 0
|
|
3086
|
+
});
|
|
3087
|
+
return seeds;
|
|
3088
|
+
}
|
|
3089
|
+
if (decision.kind === "none") {
|
|
3090
|
+
seeds.set(fieldKey(timestamp), {
|
|
3091
|
+
kind: "value",
|
|
3092
|
+
value: null
|
|
3093
|
+
});
|
|
3094
|
+
if (counter !== void 0) seeds.set(fieldKey(counter), {
|
|
3095
|
+
kind: "value",
|
|
3096
|
+
value: 0
|
|
3097
|
+
});
|
|
3098
|
+
return seeds;
|
|
3099
|
+
}
|
|
3100
|
+
seeds.set(fieldKey(timestamp), {
|
|
3101
|
+
kind: "value",
|
|
3102
|
+
value: decision.at
|
|
3103
|
+
});
|
|
3104
|
+
if (counter !== void 0) seeds.set(fieldKey(counter), {
|
|
3105
|
+
kind: "value",
|
|
3106
|
+
value: decision.count
|
|
3107
|
+
});
|
|
3108
|
+
return seeds;
|
|
3109
|
+
}
|
|
3110
|
+
/** Raw boolean points of the device-level series (`scope` empty). 1 is on. */
|
|
3111
|
+
function pulseHistoryFromQuery(series) {
|
|
3112
|
+
const row = series.find((item) => item.scope === "");
|
|
3113
|
+
if (row === void 0) return {
|
|
3114
|
+
beforeOn: null,
|
|
3115
|
+
points: []
|
|
3116
|
+
};
|
|
3117
|
+
return {
|
|
3118
|
+
beforeOn: levelBit(row.before?.value ?? null),
|
|
3119
|
+
points: row.points.flatMap((point) => {
|
|
3120
|
+
const on = levelBit(point.last);
|
|
3121
|
+
return on === null ? [] : [{
|
|
3122
|
+
t: point.t,
|
|
3123
|
+
on
|
|
3124
|
+
}];
|
|
3125
|
+
})
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
function levelBit(value) {
|
|
3129
|
+
if (value === 1) return true;
|
|
3130
|
+
if (value === 0) return false;
|
|
3131
|
+
return null;
|
|
3132
|
+
}
|
|
3133
|
+
function logDecision(input, field, decision) {
|
|
3134
|
+
const meta = {
|
|
3135
|
+
blockId: input.blockId,
|
|
3136
|
+
cap: field.cap,
|
|
3137
|
+
field: field.path
|
|
3138
|
+
};
|
|
3139
|
+
if (decision.kind === "stamp") {
|
|
3140
|
+
input.logger.info("pulse override seeded lastPressedAt from its source", {
|
|
3141
|
+
tags: { deviceId: input.deviceId },
|
|
3142
|
+
meta: {
|
|
3143
|
+
...meta,
|
|
3144
|
+
at: decision.at,
|
|
3145
|
+
via: decision.via,
|
|
3146
|
+
count: decision.count
|
|
3147
|
+
}
|
|
3148
|
+
});
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
if (decision.kind === "none") {
|
|
3152
|
+
input.logger.info("pulse override seeded no ring — the source proved none, so the native stamp is not used", {
|
|
3153
|
+
tags: { deviceId: input.deviceId },
|
|
3154
|
+
meta
|
|
3155
|
+
});
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
if (decision.kind === "hold") input.logger.warn("pulse override seed held — a source read failed, so nothing is seeded and nothing is cleared", {
|
|
3159
|
+
tags: { deviceId: input.deviceId },
|
|
3160
|
+
meta: {
|
|
3161
|
+
...meta,
|
|
3162
|
+
reason: decision.reason
|
|
3163
|
+
}
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
//#endregion
|
|
2757
3167
|
//#region src/builtins/composer/composer.ts
|
|
2758
3168
|
/**
|
|
2759
3169
|
* The composer: every declarative composition on this hub, in one runner
|
|
@@ -2991,6 +3401,7 @@ var Composer = class {
|
|
|
2991
3401
|
continue;
|
|
2992
3402
|
}
|
|
2993
3403
|
let targetSeed;
|
|
3404
|
+
let pulseSeeds;
|
|
2994
3405
|
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 {
|
|
2995
3406
|
targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
|
|
2996
3407
|
reportSeedlessRestoredCaps({
|
|
@@ -3001,6 +3412,20 @@ var Composer = class {
|
|
|
3001
3412
|
seed: targetSeed,
|
|
3002
3413
|
lookupCap: this.deps.lookupCap
|
|
3003
3414
|
});
|
|
3415
|
+
const readHistory = this.deps.readPulseHistory;
|
|
3416
|
+
if (readHistory !== void 0) pulseSeeds = await resolvePulseSeeds({
|
|
3417
|
+
blockId: p.block.id,
|
|
3418
|
+
deviceId: p.target.deviceId,
|
|
3419
|
+
fields: p.fields,
|
|
3420
|
+
targetSeed,
|
|
3421
|
+
lookupCap: this.deps.lookupCap,
|
|
3422
|
+
claims: this.deps.claims,
|
|
3423
|
+
sources: this.deps.tracker,
|
|
3424
|
+
readCap: this.deps.readExistingTargetCap,
|
|
3425
|
+
readPulseHistory: readHistory,
|
|
3426
|
+
now: this.deps.now,
|
|
3427
|
+
logger: this.deps.logger
|
|
3428
|
+
});
|
|
3004
3429
|
} catch (err) {
|
|
3005
3430
|
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`;
|
|
3006
3431
|
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", {
|
|
@@ -3026,7 +3451,7 @@ var Composer = class {
|
|
|
3026
3451
|
const writer = writerFor(this.deps, p, device, (deviceId, cap, owner, event) => {
|
|
3027
3452
|
this.onClaimEvent(deviceId, cap, owner, event);
|
|
3028
3453
|
});
|
|
3029
|
-
const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed);
|
|
3454
|
+
const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed, pulseSeeds);
|
|
3030
3455
|
if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
|
|
3031
3456
|
deviceId: p.target.deviceId,
|
|
3032
3457
|
ref: p.target.ref,
|
|
@@ -4159,6 +4584,8 @@ var SourceTracker = class {
|
|
|
4159
4584
|
*/
|
|
4160
4585
|
/** Ignored by `DeclaredDevices` whenever `integrationId` is supplied, which is always here. */
|
|
4161
4586
|
var DECLARED_INTEGRATION_NAME_UNUSED = "Blocks";
|
|
4587
|
+
/** The device-history query ceiling. A switch's raw tier is change-only, so this is the whole window. */
|
|
4588
|
+
var PULSE_HISTORY_MAX_POINTS = 5e3;
|
|
4162
4589
|
/** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
|
|
4163
4590
|
function parseNativeShadowChanged(data) {
|
|
4164
4591
|
if (!isRecord$2(data)) return null;
|
|
@@ -4307,6 +4734,26 @@ var ComposerAddon = class extends BaseAddon {
|
|
|
4307
4734
|
deviceId,
|
|
4308
4735
|
capName
|
|
4309
4736
|
}) ?? null,
|
|
4737
|
+
readPulseHistory: async (deviceId, cap, fieldPath, fromMs, toMs) => {
|
|
4738
|
+
try {
|
|
4739
|
+
return {
|
|
4740
|
+
kind: "series",
|
|
4741
|
+
series: pulseHistoryFromQuery((await api.deviceHistory.query.query({
|
|
4742
|
+
deviceId,
|
|
4743
|
+
key: historySeriesKey(cap, fieldPath),
|
|
4744
|
+
from: fromMs,
|
|
4745
|
+
to: toMs,
|
|
4746
|
+
resolution: "raw",
|
|
4747
|
+
maxPoints: PULSE_HISTORY_MAX_POINTS
|
|
4748
|
+
})).series)
|
|
4749
|
+
};
|
|
4750
|
+
} catch (err) {
|
|
4751
|
+
return {
|
|
4752
|
+
kind: "unread",
|
|
4753
|
+
error: errMsg(err)
|
|
4754
|
+
};
|
|
4755
|
+
}
|
|
4756
|
+
},
|
|
4310
4757
|
describeTarget: readBoundCaps,
|
|
4311
4758
|
grafts,
|
|
4312
4759
|
now: () => Date.now(),
|
|
@@ -53,6 +53,7 @@ import { type ComposerDeclare, type ComposerDevicePorts } from "./composed-devic
|
|
|
53
53
|
import { type PersistedComposedRow } from "./composition-declarations.js";
|
|
54
54
|
import type { CompositionEventEmitter, DeadlineTimer } from "./composition-runtime.js";
|
|
55
55
|
import { type ComposerClaimsApi, type ComposerGraftPorts, type DescribeTarget } from "./existing-target.js";
|
|
56
|
+
import { type ReadPulseHistory } from "./pulse-seed-resolve.js";
|
|
56
57
|
import type { SourceBindingsChangeReason, SourceTracker } from "./source-tracker.js";
|
|
57
58
|
export type { ComposerDerivedSpec } from "./composer-plan.js";
|
|
58
59
|
export type { ComposerDeclareResult, ComposerDevicePorts } from "./composed-device-set.js";
|
|
@@ -84,6 +85,11 @@ export interface ComposerDeps {
|
|
|
84
85
|
readonly events: CompositionEventEmitter;
|
|
85
86
|
/** Effective target state, read before an existing-target runtime owns any field. */
|
|
86
87
|
readonly readExistingTargetCap: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
|
|
88
|
+
/**
|
|
89
|
+
* Raw history of a source field, for a pulse override's start seed (D938).
|
|
90
|
+
* Absent (tests that predate the seed) leaves the boot slice as the initial.
|
|
91
|
+
*/
|
|
92
|
+
readonly readPulseHistory?: ReadPulseHistory;
|
|
87
93
|
/** ONE `getBindings` per target description read (rule 1). */
|
|
88
94
|
readonly describeTarget: DescribeTarget;
|
|
89
95
|
/** `GraftHost` (Task 10): a method-serving native for each ADDED cap. Absent, nothing is grafted. */
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
import { type CapabilityLookup, type CompositionEventPlan, type CompositionFieldPlan, type CompositionFieldRead, type CompositionFieldState, type IScopedLogger, type SourceReader } from "@camstack/types";
|
|
55
55
|
import type { ComposedWriteTarget, CompositionEventEmitter, DeadlineTimer } from "./composition-target.js";
|
|
56
56
|
import { type ReleasedField } from "./field-record.js";
|
|
57
|
+
import type { PulseFieldSeed } from "./pulse-seed.js";
|
|
57
58
|
export type { ReleasedField } from "./field-record.js";
|
|
58
59
|
export type { ComposedWriteTarget, CompositionEventEmitter, DeadlineTimer, TargetAsyncFailure, WriteOk, WriteOutcome, WriteRefused } from "./composition-target.js";
|
|
59
60
|
export interface CompositionRuntimeDeps {
|
|
@@ -65,6 +66,8 @@ export interface CompositionRuntimeDeps {
|
|
|
65
66
|
readonly lookupCap: CapabilityLookup;
|
|
66
67
|
/** Effective target slices captured before an existing-target runtime takes ownership. */
|
|
67
68
|
readonly initialByCap?: ReadonlyMap<string, Readonly<Record<string, unknown>> | null>;
|
|
69
|
+
/** Pulse start seed (D938). Absent keeps `initialByCap`. `hold` publishes nothing. */
|
|
70
|
+
readonly pulseSeeds?: ReadonlyMap<string, PulseFieldSeed>;
|
|
68
71
|
readonly read: SourceReader;
|
|
69
72
|
/**
|
|
70
73
|
* Whether `read` is of this composition's OWN output (an added cap on its
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* behaviour.
|
|
7
7
|
*/
|
|
8
8
|
import { type CapabilityLookup, type CompositionFieldItem, type CompositionFieldPlan, type CompositionFieldState, type ExpressionValue, type FieldOutcome, type StatefulExpressionMemory } from "@camstack/types";
|
|
9
|
+
import type { PulseFieldSeed } from "./pulse-seed.js";
|
|
9
10
|
export declare const NO_FIELDS: ReadonlySet<string>;
|
|
10
11
|
export interface FieldRecord {
|
|
11
12
|
readonly plan: CompositionFieldPlan;
|
|
@@ -19,9 +20,15 @@ export interface FieldRecord {
|
|
|
19
20
|
readonly expressionMemory: StatefulExpressionMemory;
|
|
20
21
|
/** Last finite state value already represented by a derived event. */
|
|
21
22
|
readonly eventHighWater: number | null;
|
|
23
|
+
/**
|
|
24
|
+
* A pulse timestamp whose start seed could not be read (D49, D938). A null
|
|
25
|
+
* result is not published, so a failed read is not "no ring".
|
|
26
|
+
*/
|
|
27
|
+
readonly pulseHold: boolean;
|
|
22
28
|
}
|
|
23
29
|
export declare function fieldKey(plan: CompositionFieldPlan): string;
|
|
24
30
|
export declare function createFieldRecord(plan: CompositionFieldPlan, byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null> | undefined, lookupCap: CapabilityLookup): FieldRecord;
|
|
31
|
+
export declare function fieldRecords(fields: readonly CompositionFieldPlan[], byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null> | undefined, lookupCap: CapabilityLookup, pulseSeeds: ReadonlyMap<string, PulseFieldSeed> | undefined): ReadonlyMap<string, FieldRecord>;
|
|
25
32
|
export declare function currentOutput(record: FieldRecord): unknown;
|
|
26
33
|
/** The top-level field a record writes: an item leaf writes its whole array. */
|
|
27
34
|
export declare function topLevelField(plan: CompositionFieldPlan): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gather a pulse override's start seed (D938). The stored expression is not
|
|
3
|
+
* changed. A failed read holds the field: no seed, and a claimed stamp is
|
|
4
|
+
* not cleared (D49).
|
|
5
|
+
*/
|
|
6
|
+
import { type CapabilityLookup, type ClaimsListing, type CompositionFieldPlan, type CompositionSourceRef, type IScopedLogger } from "@camstack/types";
|
|
7
|
+
import type { ExistingTargetSeedRead } from "./existing-target.js";
|
|
8
|
+
import { type PulseFieldSeed, type PulseHistoryPoint } from "./pulse-seed.js";
|
|
9
|
+
import type { SourceResolution } from "./source-readings.js";
|
|
10
|
+
export type { PulseFieldSeed } from "./pulse-seed.js";
|
|
11
|
+
export interface PulseHistorySeries {
|
|
12
|
+
readonly beforeOn: boolean | null;
|
|
13
|
+
readonly points: readonly PulseHistoryPoint[];
|
|
14
|
+
}
|
|
15
|
+
export type PulseHistoryAnswer = {
|
|
16
|
+
readonly kind: "unread";
|
|
17
|
+
readonly error: string;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "series";
|
|
20
|
+
readonly series: PulseHistorySeries;
|
|
21
|
+
};
|
|
22
|
+
export type ReadPulseHistory = (deviceId: number, cap: string, fieldPath: string, fromMs: number, toMs: number) => Promise<PulseHistoryAnswer>;
|
|
23
|
+
export interface PulseClaimListing {
|
|
24
|
+
listClaims(input: {
|
|
25
|
+
readonly ownerPrefix?: string;
|
|
26
|
+
}): Promise<ClaimsListing>;
|
|
27
|
+
}
|
|
28
|
+
export interface PulseSourceLookup {
|
|
29
|
+
resolve(ref: CompositionSourceRef, reset: boolean): Promise<SourceResolution>;
|
|
30
|
+
}
|
|
31
|
+
export interface ResolvePulseSeedsInput {
|
|
32
|
+
readonly blockId: string;
|
|
33
|
+
readonly deviceId: number;
|
|
34
|
+
readonly fields: readonly CompositionFieldPlan[];
|
|
35
|
+
readonly targetSeed: ExistingTargetSeedRead;
|
|
36
|
+
readonly lookupCap: CapabilityLookup;
|
|
37
|
+
readonly claims: PulseClaimListing;
|
|
38
|
+
readonly sources: PulseSourceLookup;
|
|
39
|
+
readonly readCap: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
|
|
40
|
+
readonly readPulseHistory: ReadPulseHistory;
|
|
41
|
+
readonly now: () => number;
|
|
42
|
+
readonly logger: IScopedLogger;
|
|
43
|
+
}
|
|
44
|
+
export declare function historySeriesKey(cap: string, fieldPath: string): string;
|
|
45
|
+
export declare function resolvePulseSeeds(input: ResolvePulseSeedsInput): Promise<ReadonlyMap<string, PulseFieldSeed>>;
|
|
46
|
+
export interface PulseHistoryQueryPoint {
|
|
47
|
+
readonly t: number;
|
|
48
|
+
readonly last: number | null;
|
|
49
|
+
}
|
|
50
|
+
export interface PulseHistoryQueryBefore {
|
|
51
|
+
readonly value: number | null;
|
|
52
|
+
}
|
|
53
|
+
export interface PulseHistoryQuerySeries {
|
|
54
|
+
readonly scope: string;
|
|
55
|
+
readonly before: PulseHistoryQueryBefore | null;
|
|
56
|
+
readonly points: readonly PulseHistoryQueryPoint[];
|
|
57
|
+
}
|
|
58
|
+
/** Raw boolean points of the device-level series (`scope` empty). 1 is on. */
|
|
59
|
+
export declare function pulseHistoryFromQuery(series: readonly PulseHistoryQuerySeries[]): PulseHistorySeries;
|