@camstack/system 1.2.475 → 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/{agent-https-BsqT0qni.js → agent-https-DkXBXEvz.js} +1 -1
- package/dist/{agent-https-wA8gGLc1.mjs → agent-https-vpf_D97z.mjs} +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.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-live-set.d.ts +9 -1
- package/dist/builtins/composer/composer-runtime-factory.d.ts +2 -1
- package/dist/builtins/composer/composer.addon.js +581 -15
- package/dist/builtins/composer/composer.addon.mjs +581 -15
- package/dist/builtins/composer/composer.d.ts +14 -1
- package/dist/builtins/composer/composition-runtime.d.ts +5 -0
- package/dist/builtins/composer/composition-target.d.ts +6 -0
- package/dist/builtins/composer/field-record.d.ts +7 -0
- package/dist/builtins/composer/owned-fields-target.d.ts +9 -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/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/block-supervisor.d.ts +19 -1
- package/dist/builtins/core-blocks/composition-api.d.ts +7 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +108 -10
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +108 -10
- package/dist/builtins/device-history/device-history.addon.js +63 -953
- package/dist/builtins/device-history/device-history.addon.mjs +2 -892
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +3 -3
- package/dist/builtins/keychain/index.mjs +3 -3
- 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 +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- 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/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/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/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- 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 +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +3 -3
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-CiFkyAcf.mjs → composition-sources-B5nK7pHt.mjs} +1 -1
- package/dist/{composition-sources-DeUSUid6.js → composition-sources-BNOP9mFp.js} +1 -1
- package/dist/{constants-3Wj5OyEk.js → constants-D4VigrMM.js} +1 -1
- package/dist/{constants-DZJAkne8.mjs → constants-jbhoFV7w.mjs} +1 -1
- package/dist/{device-ca-DiV5xcMz.mjs → device-ca-CvBZeyVX.mjs} +1 -1
- package/dist/{device-ca-ftrfVdc0.js → device-ca-DhyQeXsa.js} +1 -1
- package/dist/{dist-DygMs3Qc.mjs → dist-B_V9r61Z.mjs} +21 -2
- package/dist/{dist-BtDdPdZ7.js → dist-CNU7cdFf.js} +21 -2
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-Bc9PnWu3.mjs → hub-mdns-ciao-BuUg0gpI.mjs} +2 -2
- package/dist/{hub-mdns-ciao-B1VcdfT1.js → hub-mdns-ciao-DFidf52M.js} +2 -2
- package/dist/index.js +7 -7
- package/dist/index.mjs +7 -7
- package/dist/{restore-archive-TDkQfmLO.js → restore-archive-BtCheeWu.js} +1 -1
- package/dist/{restore-archive-D_Pwryre.mjs → restore-archive-CDo4Zlmx.mjs} +1 -1
- package/dist/retention-1QA8e4o0.js +1024 -0
- package/dist/retention-lPgwLane.mjs +893 -0
- package/dist/{wal-maintenance-D3UwIKX5.js → wal-maintenance-B7xBeIFi.js} +0 -0
- package/dist/{wal-maintenance-rLv4uOmp.mjs → wal-maintenance-DItlr0Yu.mjs} +0 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
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-
|
|
2
|
-
import { n as normalizedNativeOwner } from "../../composition-sources-
|
|
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
|
+
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. */
|
|
@@ -788,6 +789,41 @@ function wantedClaims(live, claimGate) {
|
|
|
788
789
|
for (const owner of held) wanted.push(...claimGate.claimsOf(owner));
|
|
789
790
|
return wanted;
|
|
790
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* Claims an operator has already confirmed are no longer wanted: a deleted
|
|
794
|
+
* block (`retiredBlockIds`), or a cap an edit dropped. Released on this apply,
|
|
795
|
+
* not after a second one — a missing block in a single snapshot is still the
|
|
796
|
+
* two-apply gate (`wantedClaims`). A disabled block is not here: its runtime
|
|
797
|
+
* hands the fields back itself.
|
|
798
|
+
*/
|
|
799
|
+
function confirmedReleaseKeys(retiredBlockIds, planned, previous, claimGate) {
|
|
800
|
+
const retired = new Set(retiredBlockIds);
|
|
801
|
+
const seen = /* @__PURE__ */ new Set();
|
|
802
|
+
const keys = [];
|
|
803
|
+
const push = (key) => {
|
|
804
|
+
const id = claimKey(key);
|
|
805
|
+
if (seen.has(id)) return;
|
|
806
|
+
seen.add(id);
|
|
807
|
+
keys.push(key);
|
|
808
|
+
};
|
|
809
|
+
for (const owner of retired) for (const key of claimGate.claimsOf(owner)) push(key);
|
|
810
|
+
const plannedById = new Map(planned.map((block) => [block.block.id, block]));
|
|
811
|
+
for (const [blockId, entry] of previous) {
|
|
812
|
+
if (retired.has(blockId)) continue;
|
|
813
|
+
const next = plannedById.get(blockId);
|
|
814
|
+
if (next === void 0 || !next.block.enabled) continue;
|
|
815
|
+
if (entry.planned.block.updatedAt === next.block.updatedAt) continue;
|
|
816
|
+
for (const key of claimGate.claimsOf(blockId)) if (!planStillWantsClaim(next, key)) push(key);
|
|
817
|
+
}
|
|
818
|
+
return keys;
|
|
819
|
+
}
|
|
820
|
+
/** True when this plan still holds `key` — including while the plan cannot say, so a bad read releases nothing. */
|
|
821
|
+
function planStillWantsClaim(planned, key) {
|
|
822
|
+
const target = planned.target;
|
|
823
|
+
if (target.kind !== "existing") return false;
|
|
824
|
+
if (target.waiting !== null || target.gone !== null || target.deviceId === null || planned.problems.length > 0) return true;
|
|
825
|
+
return target.deviceId === key.deviceId && target.features.some((feature) => feature.cap === key.capName);
|
|
826
|
+
}
|
|
791
827
|
//#endregion
|
|
792
828
|
//#region src/builtins/composer/composition-declarations.ts
|
|
793
829
|
/**
|
|
@@ -1161,9 +1197,33 @@ function createFieldRecord(plan, byCap, lookupCap) {
|
|
|
1161
1197
|
hasWritten: false,
|
|
1162
1198
|
written: void 0,
|
|
1163
1199
|
expressionMemory: createStatefulExpressionMemory(),
|
|
1164
|
-
eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
|
|
1200
|
+
eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null,
|
|
1201
|
+
pulseHold: false
|
|
1165
1202
|
};
|
|
1166
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
|
+
}
|
|
1167
1227
|
function currentOutput(record) {
|
|
1168
1228
|
return record.hasWritten ? record.written : record.initial;
|
|
1169
1229
|
}
|
|
@@ -1217,6 +1277,7 @@ function namedField(r) {
|
|
|
1217
1277
|
function writable(record) {
|
|
1218
1278
|
const outcome = record.outcome;
|
|
1219
1279
|
if (outcome === null) return void 0;
|
|
1280
|
+
if (record.pulseHold && !record.hasWritten && (outcome.state === "unavailable" || outcome.value === null)) return;
|
|
1220
1281
|
if (outcome.state !== "unavailable") return outcome.value;
|
|
1221
1282
|
switch (record.plan.unavailable) {
|
|
1222
1283
|
case "null": return null;
|
|
@@ -1654,7 +1715,7 @@ var CompositionRuntime = class {
|
|
|
1654
1715
|
constructor(deps) {
|
|
1655
1716
|
this.deps = deps;
|
|
1656
1717
|
this.startedAtMs = deps.now();
|
|
1657
|
-
this.records =
|
|
1718
|
+
this.records = fieldRecords(deps.fields, deps.initialByCap, deps.lookupCap, deps.pulseSeeds);
|
|
1658
1719
|
deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
|
|
1659
1720
|
const eventContext = {
|
|
1660
1721
|
deviceId: deps.target.id,
|
|
@@ -1713,6 +1774,13 @@ var CompositionRuntime = class {
|
|
|
1713
1774
|
this.stopped = true;
|
|
1714
1775
|
this.deps.timer.disarm();
|
|
1715
1776
|
}
|
|
1777
|
+
/** `stop`, then wait until the target has sent nothing further. */
|
|
1778
|
+
quiesce() {
|
|
1779
|
+
this.stop();
|
|
1780
|
+
const quiesce = this.deps.target.quiesce;
|
|
1781
|
+
if (quiesce === void 0) return Promise.resolve();
|
|
1782
|
+
return quiesce.call(this.deps.target);
|
|
1783
|
+
}
|
|
1716
1784
|
state() {
|
|
1717
1785
|
return {
|
|
1718
1786
|
failure: this.failure,
|
|
@@ -2216,6 +2284,10 @@ var OwnedFieldsTarget = class {
|
|
|
2216
2284
|
listeners = [];
|
|
2217
2285
|
/** Per cap, bumped by every `withhold`: a claim that settles under another epoch claimed a stale field set and does not land (I-2). */
|
|
2218
2286
|
epoch = /* @__PURE__ */ new Map();
|
|
2287
|
+
/** Set by `quiesce`: nothing further is sent. */
|
|
2288
|
+
closed = false;
|
|
2289
|
+
idle = Promise.resolve();
|
|
2290
|
+
markIdle = null;
|
|
2219
2291
|
constructor(deps) {
|
|
2220
2292
|
this.deps = deps;
|
|
2221
2293
|
this.id = deps.deviceId;
|
|
@@ -2240,6 +2312,27 @@ var OwnedFieldsTarget = class {
|
|
|
2240
2312
|
}
|
|
2241
2313
|
/** `device-status` belongs to the native device; availability lives in the block report only. */
|
|
2242
2314
|
setAvailable(_available) {}
|
|
2315
|
+
/**
|
|
2316
|
+
* The block has stopped. Writes still queued are dropped; the call in flight
|
|
2317
|
+
* is allowed to finish, and then nothing else is sent.
|
|
2318
|
+
*/
|
|
2319
|
+
quiesce() {
|
|
2320
|
+
this.closed = true;
|
|
2321
|
+
const dropped = [...this.pending.keys()];
|
|
2322
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
2323
|
+
for (const cap of dropped) this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
|
|
2324
|
+
tags: { deviceId: this.deps.deviceId },
|
|
2325
|
+
meta: {
|
|
2326
|
+
cap,
|
|
2327
|
+
owner: this.deps.owner
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
if (this.inFlight.size === 0) return Promise.resolve();
|
|
2331
|
+
if (this.markIdle === null) this.idle = new Promise((resolve) => {
|
|
2332
|
+
this.markIdle = resolve;
|
|
2333
|
+
});
|
|
2334
|
+
return this.idle;
|
|
2335
|
+
}
|
|
2243
2336
|
onAsyncFailure(listener) {
|
|
2244
2337
|
this.listeners = [...this.listeners, listener];
|
|
2245
2338
|
}
|
|
@@ -2255,6 +2348,17 @@ var OwnedFieldsTarget = class {
|
|
|
2255
2348
|
return this.deps.fieldsOf(cap).filter((f) => held === void 0 || !held.has(f));
|
|
2256
2349
|
}
|
|
2257
2350
|
enqueue(cap, op) {
|
|
2351
|
+
if (this.closed) {
|
|
2352
|
+
this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
|
|
2353
|
+
tags: { deviceId: this.deps.deviceId },
|
|
2354
|
+
meta: {
|
|
2355
|
+
cap,
|
|
2356
|
+
owner: this.deps.owner,
|
|
2357
|
+
op: op.kind
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2258
2362
|
if (!this.inFlight.has(cap)) {
|
|
2259
2363
|
this.send(cap, op);
|
|
2260
2364
|
return;
|
|
@@ -2277,6 +2381,11 @@ var OwnedFieldsTarget = class {
|
|
|
2277
2381
|
}
|
|
2278
2382
|
settled(cap) {
|
|
2279
2383
|
this.inFlight = new Set([...this.inFlight].filter((c) => c !== cap));
|
|
2384
|
+
if (this.closed) {
|
|
2385
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
2386
|
+
if (this.inFlight.size === 0) this.markIdle?.();
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2280
2389
|
const next = this.pending.get(cap);
|
|
2281
2390
|
if (next === void 0) return;
|
|
2282
2391
|
this.pending = new Map([...this.pending].filter(([c]) => c !== cap));
|
|
@@ -2453,7 +2562,7 @@ function writerFor(deps, p, device, onClaimEvent) {
|
|
|
2453
2562
|
onClaim: (cap, event) => onClaimEvent(deviceId, cap, owner, event)
|
|
2454
2563
|
});
|
|
2455
2564
|
}
|
|
2456
|
-
function newCompositionRuntime(deps, p, target, seed) {
|
|
2565
|
+
function newCompositionRuntime(deps, p, target, seed, pulseSeeds) {
|
|
2457
2566
|
const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
|
|
2458
2567
|
const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
|
|
2459
2568
|
const nativeCaps = new Set(self?.native ?? []);
|
|
@@ -2466,6 +2575,7 @@ function newCompositionRuntime(deps, p, target, seed) {
|
|
|
2466
2575
|
target,
|
|
2467
2576
|
lookupCap: deps.lookupCap,
|
|
2468
2577
|
...seed === void 0 ? {} : { initialByCap: seed.byCap },
|
|
2578
|
+
...pulseSeeds === void 0 ? {} : { pulseSeeds },
|
|
2469
2579
|
read: (read) => deps.tracker.read(read, compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
|
|
2470
2580
|
isOwnRead: (read) => compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
|
|
2471
2581
|
now: deps.now,
|
|
@@ -2671,6 +2781,389 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
|
|
|
2671
2781
|
};
|
|
2672
2782
|
}
|
|
2673
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
|
|
2674
3167
|
//#region src/builtins/composer/composer.ts
|
|
2675
3168
|
/**
|
|
2676
3169
|
* The composer: every declarative composition on this hub, in one runner
|
|
@@ -2693,7 +3186,11 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
|
|
|
2693
3186
|
* filter in `composer.addon.ts` stays load-bearing. Its fields are CLAIMED on
|
|
2694
3187
|
* the hub under the block id (`OwnedFieldsTarget`), and released through the
|
|
2695
3188
|
* `ClaimGate` — two applies must agree, seeded at boot from the claims index —
|
|
2696
|
-
* when the block
|
|
3189
|
+
* when the block merely vanished from one snapshot. An operator delete
|
|
3190
|
+
* (`retiredBlockIds`) or an edit that drops a cap releases those claims on
|
|
3191
|
+
* THIS apply, after the old runtime has quiesced and before any replacement
|
|
3192
|
+
* claims; a release that does not land is logged with `tags: { deviceId }` and
|
|
3193
|
+
* returned on the apply. Its own replaced
|
|
2697
3194
|
* fields are read in the NATIVE view (rule 6). Its target row gone on two
|
|
2698
3195
|
* agreeing index reads fails the block by name and ungrafts what it added —
|
|
2699
3196
|
* whether the loss was seen on `device.unregistered` or on a later plan; its
|
|
@@ -2841,12 +3338,16 @@ var Composer = class {
|
|
|
2841
3338
|
};
|
|
2842
3339
|
});
|
|
2843
3340
|
await this.composed.reconcile(gate.integrationId, declarations, planned);
|
|
3341
|
+
const releasing = confirmedReleaseKeys(input.retiredBlockIds ?? [], planned, this.live, this.claimGate);
|
|
2844
3342
|
const rebuilt = await this.rebuildRuntimes(planned, declarations, restart, rebuildErrors);
|
|
3343
|
+
await Promise.all(rebuilt.stopped.map((runtime) => runtime.quiesce()));
|
|
3344
|
+
const unreleasedClaims = await this.releaseConfirmed(releasing);
|
|
2845
3345
|
await this.syncSources(rebuilt);
|
|
2846
3346
|
await this.reconcileClaims();
|
|
2847
3347
|
return {
|
|
2848
3348
|
blocks: planned.map((p) => this.verdict(p.block.id)),
|
|
2849
|
-
acted: true
|
|
3349
|
+
acted: true,
|
|
3350
|
+
unreleasedClaims
|
|
2850
3351
|
};
|
|
2851
3352
|
}
|
|
2852
3353
|
plan(block, restart, claimsWait) {
|
|
@@ -2867,7 +3368,13 @@ var Composer = class {
|
|
|
2867
3368
|
async rebuildRuntimes(planned, declarations, restart, rebuildErrors) {
|
|
2868
3369
|
const reset = /* @__PURE__ */ new Set();
|
|
2869
3370
|
const fresh = /* @__PURE__ */ new Set();
|
|
3371
|
+
const stopped = [];
|
|
2870
3372
|
const next = /* @__PURE__ */ new Map();
|
|
3373
|
+
const halt = (runtime) => {
|
|
3374
|
+
if (runtime === null) return;
|
|
3375
|
+
runtime.stop();
|
|
3376
|
+
stopped.push(runtime);
|
|
3377
|
+
};
|
|
2871
3378
|
for (const p of planned) {
|
|
2872
3379
|
const previous = this.live.get(p.block.id);
|
|
2873
3380
|
const lost = lostTarget(p, previous?.planned);
|
|
@@ -2894,6 +3401,7 @@ var Composer = class {
|
|
|
2894
3401
|
continue;
|
|
2895
3402
|
}
|
|
2896
3403
|
let targetSeed;
|
|
3404
|
+
let pulseSeeds;
|
|
2897
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 {
|
|
2898
3406
|
targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
|
|
2899
3407
|
reportSeedlessRestoredCaps({
|
|
@@ -2904,6 +3412,20 @@ var Composer = class {
|
|
|
2904
3412
|
seed: targetSeed,
|
|
2905
3413
|
lookupCap: this.deps.lookupCap
|
|
2906
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
|
+
});
|
|
2907
3429
|
} catch (err) {
|
|
2908
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`;
|
|
2909
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", {
|
|
@@ -2929,7 +3451,7 @@ var Composer = class {
|
|
|
2929
3451
|
const writer = writerFor(this.deps, p, device, (deviceId, cap, owner, event) => {
|
|
2930
3452
|
this.onClaimEvent(deviceId, cap, owner, event);
|
|
2931
3453
|
});
|
|
2932
|
-
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);
|
|
2933
3455
|
if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
|
|
2934
3456
|
deviceId: p.target.deviceId,
|
|
2935
3457
|
ref: p.target.ref,
|
|
@@ -2938,7 +3460,7 @@ var Composer = class {
|
|
|
2938
3460
|
enabled: p.block.enabled,
|
|
2939
3461
|
settled: p.problems.length === 0
|
|
2940
3462
|
});
|
|
2941
|
-
previous?.runtime
|
|
3463
|
+
halt(previous?.runtime ?? null);
|
|
2942
3464
|
if (runtime !== null) fresh.add(runtime);
|
|
2943
3465
|
next.set(p.block.id, {
|
|
2944
3466
|
planned: p,
|
|
@@ -2953,12 +3475,15 @@ var Composer = class {
|
|
|
2953
3475
|
for (const [blockId, entry] of this.live) {
|
|
2954
3476
|
if (next.has(blockId)) continue;
|
|
2955
3477
|
if (held.has(composedDeviceStableId(blockId))) next.set(blockId, entry);
|
|
2956
|
-
else entry.runtime
|
|
3478
|
+
else halt(entry.runtime);
|
|
2957
3479
|
}
|
|
2958
3480
|
this.live = next;
|
|
2959
3481
|
const liveRuntimes = new Set([...next.values()].map((e) => e.runtime));
|
|
2960
3482
|
this.unsynced = new Set([...[...this.unsynced].filter((r) => liveRuntimes.has(r)), ...fresh]);
|
|
2961
|
-
return {
|
|
3483
|
+
return {
|
|
3484
|
+
reset,
|
|
3485
|
+
stopped
|
|
3486
|
+
};
|
|
2962
3487
|
}
|
|
2963
3488
|
ungraftGone(blockId, target, deviceId) {
|
|
2964
3489
|
this.grafts.gone(deviceId);
|
|
@@ -3042,21 +3567,40 @@ var Composer = class {
|
|
|
3042
3567
|
if (!this.claimSeed.done) return;
|
|
3043
3568
|
for (const key of this.claimGate.filter(wantedClaims(this.live, this.claimGate))) await this.release(key);
|
|
3044
3569
|
}
|
|
3570
|
+
/** Confirmed releases, awaited. A failure is the string the hub gave; the gate retries it later. */
|
|
3571
|
+
async releaseConfirmed(keys) {
|
|
3572
|
+
const unreleased = [];
|
|
3573
|
+
for (const key of keys) {
|
|
3574
|
+
const error = await this.release(key);
|
|
3575
|
+
if (error === null) continue;
|
|
3576
|
+
unreleased.push({
|
|
3577
|
+
deviceId: key.deviceId,
|
|
3578
|
+
capName: key.capName,
|
|
3579
|
+
owner: key.owner,
|
|
3580
|
+
error
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
return unreleased;
|
|
3584
|
+
}
|
|
3585
|
+
/** Null when the hub confirmed the release (or held nothing of ours). */
|
|
3045
3586
|
async release(key) {
|
|
3046
3587
|
let outcome;
|
|
3047
3588
|
try {
|
|
3048
3589
|
outcome = await this.deps.claims.releaseClaim(key);
|
|
3049
3590
|
} catch (err) {
|
|
3050
|
-
|
|
3051
|
-
|
|
3591
|
+
const error = errMsg(err);
|
|
3592
|
+
this.claimGate.releaseFailed(key, error);
|
|
3593
|
+
return error;
|
|
3052
3594
|
}
|
|
3053
3595
|
if (!outcome.ok && !RELEASE_SETTLED_CODES.has(outcome.code)) {
|
|
3054
|
-
|
|
3055
|
-
|
|
3596
|
+
const error = `${outcome.code}: ${outcome.message}`;
|
|
3597
|
+
this.claimGate.releaseFailed(key, error);
|
|
3598
|
+
return error;
|
|
3056
3599
|
}
|
|
3057
3600
|
this.claimGate.released(key, outcome.ok ? "released" : outcome.code);
|
|
3058
3601
|
await this.grafts.released(key.deviceId, key.capName);
|
|
3059
3602
|
await this.onClaimEvent(key.deviceId, key.capName, key.owner, "free");
|
|
3603
|
+
return null;
|
|
3060
3604
|
}
|
|
3061
3605
|
/**
|
|
3062
3606
|
* Rule 5: a customization whose target row is gone — the tracker's two
|
|
@@ -4040,6 +4584,8 @@ var SourceTracker = class {
|
|
|
4040
4584
|
*/
|
|
4041
4585
|
/** Ignored by `DeclaredDevices` whenever `integrationId` is supplied, which is always here. */
|
|
4042
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;
|
|
4043
4589
|
/** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
|
|
4044
4590
|
function parseNativeShadowChanged(data) {
|
|
4045
4591
|
if (!isRecord$2(data)) return null;
|
|
@@ -4188,6 +4734,26 @@ var ComposerAddon = class extends BaseAddon {
|
|
|
4188
4734
|
deviceId,
|
|
4189
4735
|
capName
|
|
4190
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
|
+
},
|
|
4191
4757
|
describeTarget: readBoundCaps,
|
|
4192
4758
|
grafts,
|
|
4193
4759
|
now: () => Date.now(),
|