@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
|
@@ -3,8 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
7
|
-
const require_composition_sources = require("../../composition-sources-
|
|
6
|
+
const require_dist = require("../../dist-CNU7cdFf.js");
|
|
7
|
+
const require_composition_sources = require("../../composition-sources-BNOP9mFp.js");
|
|
8
|
+
const require_retention = require("../../retention-1QA8e4o0.js");
|
|
8
9
|
let zod = require("zod");
|
|
9
10
|
//#region src/builtins/shared/device-state-changed.ts
|
|
10
11
|
/** Structural narrowing helper for untrusted bus payloads. */
|
|
@@ -793,6 +794,41 @@ function wantedClaims(live, claimGate) {
|
|
|
793
794
|
for (const owner of held) wanted.push(...claimGate.claimsOf(owner));
|
|
794
795
|
return wanted;
|
|
795
796
|
}
|
|
797
|
+
/**
|
|
798
|
+
* Claims an operator has already confirmed are no longer wanted: a deleted
|
|
799
|
+
* block (`retiredBlockIds`), or a cap an edit dropped. Released on this apply,
|
|
800
|
+
* not after a second one — a missing block in a single snapshot is still the
|
|
801
|
+
* two-apply gate (`wantedClaims`). A disabled block is not here: its runtime
|
|
802
|
+
* hands the fields back itself.
|
|
803
|
+
*/
|
|
804
|
+
function confirmedReleaseKeys(retiredBlockIds, planned, previous, claimGate) {
|
|
805
|
+
const retired = new Set(retiredBlockIds);
|
|
806
|
+
const seen = /* @__PURE__ */ new Set();
|
|
807
|
+
const keys = [];
|
|
808
|
+
const push = (key) => {
|
|
809
|
+
const id = claimKey(key);
|
|
810
|
+
if (seen.has(id)) return;
|
|
811
|
+
seen.add(id);
|
|
812
|
+
keys.push(key);
|
|
813
|
+
};
|
|
814
|
+
for (const owner of retired) for (const key of claimGate.claimsOf(owner)) push(key);
|
|
815
|
+
const plannedById = new Map(planned.map((block) => [block.block.id, block]));
|
|
816
|
+
for (const [blockId, entry] of previous) {
|
|
817
|
+
if (retired.has(blockId)) continue;
|
|
818
|
+
const next = plannedById.get(blockId);
|
|
819
|
+
if (next === void 0 || !next.block.enabled) continue;
|
|
820
|
+
if (entry.planned.block.updatedAt === next.block.updatedAt) continue;
|
|
821
|
+
for (const key of claimGate.claimsOf(blockId)) if (!planStillWantsClaim(next, key)) push(key);
|
|
822
|
+
}
|
|
823
|
+
return keys;
|
|
824
|
+
}
|
|
825
|
+
/** True when this plan still holds `key` — including while the plan cannot say, so a bad read releases nothing. */
|
|
826
|
+
function planStillWantsClaim(planned, key) {
|
|
827
|
+
const target = planned.target;
|
|
828
|
+
if (target.kind !== "existing") return false;
|
|
829
|
+
if (target.waiting !== null || target.gone !== null || target.deviceId === null || planned.problems.length > 0) return true;
|
|
830
|
+
return target.deviceId === key.deviceId && target.features.some((feature) => feature.cap === key.capName);
|
|
831
|
+
}
|
|
796
832
|
//#endregion
|
|
797
833
|
//#region src/builtins/composer/composition-declarations.ts
|
|
798
834
|
/**
|
|
@@ -1166,9 +1202,33 @@ function createFieldRecord(plan, byCap, lookupCap) {
|
|
|
1166
1202
|
hasWritten: false,
|
|
1167
1203
|
written: void 0,
|
|
1168
1204
|
expressionMemory: require_dist.createStatefulExpressionMemory(),
|
|
1169
|
-
eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
|
|
1205
|
+
eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null,
|
|
1206
|
+
pulseHold: false
|
|
1170
1207
|
};
|
|
1171
1208
|
}
|
|
1209
|
+
/** Apply a pulse start seed (D938). `keep` leaves the slice seed in place. */
|
|
1210
|
+
function applyPulseSeed(record, seed) {
|
|
1211
|
+
if (seed === void 0 || seed.kind === "keep") return record;
|
|
1212
|
+
if (seed.kind === "hold") return {
|
|
1213
|
+
...record,
|
|
1214
|
+
initial: void 0,
|
|
1215
|
+
eventHighWater: null,
|
|
1216
|
+
pulseHold: true
|
|
1217
|
+
};
|
|
1218
|
+
const value = seed.value;
|
|
1219
|
+
return {
|
|
1220
|
+
...record,
|
|
1221
|
+
initial: value,
|
|
1222
|
+
eventHighWater: typeof value === "number" && Number.isFinite(value) ? value : null,
|
|
1223
|
+
pulseHold: false
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
function fieldRecords(fields, byCap, lookupCap, pulseSeeds) {
|
|
1227
|
+
return new Map(fields.map((plan) => {
|
|
1228
|
+
const key = fieldKey(plan);
|
|
1229
|
+
return [key, applyPulseSeed(createFieldRecord(plan, byCap, lookupCap), pulseSeeds?.get(key))];
|
|
1230
|
+
}));
|
|
1231
|
+
}
|
|
1172
1232
|
function currentOutput(record) {
|
|
1173
1233
|
return record.hasWritten ? record.written : record.initial;
|
|
1174
1234
|
}
|
|
@@ -1222,6 +1282,7 @@ function namedField(r) {
|
|
|
1222
1282
|
function writable(record) {
|
|
1223
1283
|
const outcome = record.outcome;
|
|
1224
1284
|
if (outcome === null) return void 0;
|
|
1285
|
+
if (record.pulseHold && !record.hasWritten && (outcome.state === "unavailable" || outcome.value === null)) return;
|
|
1225
1286
|
if (outcome.state !== "unavailable") return outcome.value;
|
|
1226
1287
|
switch (record.plan.unavailable) {
|
|
1227
1288
|
case "null": return null;
|
|
@@ -1659,7 +1720,7 @@ var CompositionRuntime = class {
|
|
|
1659
1720
|
constructor(deps) {
|
|
1660
1721
|
this.deps = deps;
|
|
1661
1722
|
this.startedAtMs = deps.now();
|
|
1662
|
-
this.records =
|
|
1723
|
+
this.records = fieldRecords(deps.fields, deps.initialByCap, deps.lookupCap, deps.pulseSeeds);
|
|
1663
1724
|
deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
|
|
1664
1725
|
const eventContext = {
|
|
1665
1726
|
deviceId: deps.target.id,
|
|
@@ -1718,6 +1779,13 @@ var CompositionRuntime = class {
|
|
|
1718
1779
|
this.stopped = true;
|
|
1719
1780
|
this.deps.timer.disarm();
|
|
1720
1781
|
}
|
|
1782
|
+
/** `stop`, then wait until the target has sent nothing further. */
|
|
1783
|
+
quiesce() {
|
|
1784
|
+
this.stop();
|
|
1785
|
+
const quiesce = this.deps.target.quiesce;
|
|
1786
|
+
if (quiesce === void 0) return Promise.resolve();
|
|
1787
|
+
return quiesce.call(this.deps.target);
|
|
1788
|
+
}
|
|
1721
1789
|
state() {
|
|
1722
1790
|
return {
|
|
1723
1791
|
failure: this.failure,
|
|
@@ -2221,6 +2289,10 @@ var OwnedFieldsTarget = class {
|
|
|
2221
2289
|
listeners = [];
|
|
2222
2290
|
/** Per cap, bumped by every `withhold`: a claim that settles under another epoch claimed a stale field set and does not land (I-2). */
|
|
2223
2291
|
epoch = /* @__PURE__ */ new Map();
|
|
2292
|
+
/** Set by `quiesce`: nothing further is sent. */
|
|
2293
|
+
closed = false;
|
|
2294
|
+
idle = Promise.resolve();
|
|
2295
|
+
markIdle = null;
|
|
2224
2296
|
constructor(deps) {
|
|
2225
2297
|
this.deps = deps;
|
|
2226
2298
|
this.id = deps.deviceId;
|
|
@@ -2245,6 +2317,27 @@ var OwnedFieldsTarget = class {
|
|
|
2245
2317
|
}
|
|
2246
2318
|
/** `device-status` belongs to the native device; availability lives in the block report only. */
|
|
2247
2319
|
setAvailable(_available) {}
|
|
2320
|
+
/**
|
|
2321
|
+
* The block has stopped. Writes still queued are dropped; the call in flight
|
|
2322
|
+
* is allowed to finish, and then nothing else is sent.
|
|
2323
|
+
*/
|
|
2324
|
+
quiesce() {
|
|
2325
|
+
this.closed = true;
|
|
2326
|
+
const dropped = [...this.pending.keys()];
|
|
2327
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
2328
|
+
for (const cap of dropped) this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
|
|
2329
|
+
tags: { deviceId: this.deps.deviceId },
|
|
2330
|
+
meta: {
|
|
2331
|
+
cap,
|
|
2332
|
+
owner: this.deps.owner
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
if (this.inFlight.size === 0) return Promise.resolve();
|
|
2336
|
+
if (this.markIdle === null) this.idle = new Promise((resolve) => {
|
|
2337
|
+
this.markIdle = resolve;
|
|
2338
|
+
});
|
|
2339
|
+
return this.idle;
|
|
2340
|
+
}
|
|
2248
2341
|
onAsyncFailure(listener) {
|
|
2249
2342
|
this.listeners = [...this.listeners, listener];
|
|
2250
2343
|
}
|
|
@@ -2260,6 +2353,17 @@ var OwnedFieldsTarget = class {
|
|
|
2260
2353
|
return this.deps.fieldsOf(cap).filter((f) => held === void 0 || !held.has(f));
|
|
2261
2354
|
}
|
|
2262
2355
|
enqueue(cap, op) {
|
|
2356
|
+
if (this.closed) {
|
|
2357
|
+
this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
|
|
2358
|
+
tags: { deviceId: this.deps.deviceId },
|
|
2359
|
+
meta: {
|
|
2360
|
+
cap,
|
|
2361
|
+
owner: this.deps.owner,
|
|
2362
|
+
op: op.kind
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2263
2367
|
if (!this.inFlight.has(cap)) {
|
|
2264
2368
|
this.send(cap, op);
|
|
2265
2369
|
return;
|
|
@@ -2282,6 +2386,11 @@ var OwnedFieldsTarget = class {
|
|
|
2282
2386
|
}
|
|
2283
2387
|
settled(cap) {
|
|
2284
2388
|
this.inFlight = new Set([...this.inFlight].filter((c) => c !== cap));
|
|
2389
|
+
if (this.closed) {
|
|
2390
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
2391
|
+
if (this.inFlight.size === 0) this.markIdle?.();
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2285
2394
|
const next = this.pending.get(cap);
|
|
2286
2395
|
if (next === void 0) return;
|
|
2287
2396
|
this.pending = new Map([...this.pending].filter(([c]) => c !== cap));
|
|
@@ -2458,7 +2567,7 @@ function writerFor(deps, p, device, onClaimEvent) {
|
|
|
2458
2567
|
onClaim: (cap, event) => onClaimEvent(deviceId, cap, owner, event)
|
|
2459
2568
|
});
|
|
2460
2569
|
}
|
|
2461
|
-
function newCompositionRuntime(deps, p, target, seed) {
|
|
2570
|
+
function newCompositionRuntime(deps, p, target, seed, pulseSeeds) {
|
|
2462
2571
|
const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
|
|
2463
2572
|
const targetKey = p.target.kind === "existing" ? require_dist.compositionSourceKey(p.target.ref) : null;
|
|
2464
2573
|
const nativeCaps = new Set(self?.native ?? []);
|
|
@@ -2471,6 +2580,7 @@ function newCompositionRuntime(deps, p, target, seed) {
|
|
|
2471
2580
|
target,
|
|
2472
2581
|
lookupCap: deps.lookupCap,
|
|
2473
2582
|
...seed === void 0 ? {} : { initialByCap: seed.byCap },
|
|
2583
|
+
...pulseSeeds === void 0 ? {} : { pulseSeeds },
|
|
2474
2584
|
read: (read) => deps.tracker.read(read, require_dist.compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
|
|
2475
2585
|
isOwnRead: (read) => require_dist.compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
|
|
2476
2586
|
now: deps.now,
|
|
@@ -2676,6 +2786,389 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
|
|
|
2676
2786
|
};
|
|
2677
2787
|
}
|
|
2678
2788
|
//#endregion
|
|
2789
|
+
//#region src/builtins/composer/pulse-seed.ts
|
|
2790
|
+
/** The last false→true in time order. A series that opens already on is not a transition. */
|
|
2791
|
+
function lastOffToOn(points, beforeOn) {
|
|
2792
|
+
const ordered = [...points].toSorted((a, b) => a.t - b.t);
|
|
2793
|
+
let previous = beforeOn;
|
|
2794
|
+
let rise = null;
|
|
2795
|
+
for (const point of ordered) {
|
|
2796
|
+
if (previous === false && point.on && Number.isFinite(point.t)) rise = point.t;
|
|
2797
|
+
previous = point.on;
|
|
2798
|
+
}
|
|
2799
|
+
return rise;
|
|
2800
|
+
}
|
|
2801
|
+
function finite(value) {
|
|
2802
|
+
return value !== null && Number.isFinite(value) ? value : null;
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* What the source proves. A failed read is `hold`, never `none`.
|
|
2806
|
+
* When the source records no history and it is off, `lastChangedAt` is the
|
|
2807
|
+
* end of the last ring: it bounds the ring from above (the press was at or
|
|
2808
|
+
* before that instant). It is used only when it is newer than the target's
|
|
2809
|
+
* native stamp — an older bound does not prove the native value.
|
|
2810
|
+
*/
|
|
2811
|
+
function proof(facts) {
|
|
2812
|
+
if (facts.history.kind === "unread") return {
|
|
2813
|
+
kind: "hold",
|
|
2814
|
+
reason: "history-unread"
|
|
2815
|
+
};
|
|
2816
|
+
if (facts.history.kind === "series") {
|
|
2817
|
+
const at = lastOffToOn(facts.history.points, facts.history.beforeOn);
|
|
2818
|
+
return at === null ? { kind: "none" } : {
|
|
2819
|
+
kind: "rise",
|
|
2820
|
+
at,
|
|
2821
|
+
via: "history"
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
if (facts.level.kind === "unread") return {
|
|
2825
|
+
kind: "hold",
|
|
2826
|
+
reason: "level-unread"
|
|
2827
|
+
};
|
|
2828
|
+
const changedAt = facts.level.kind === "off" ? finite(facts.level.changedAt) : null;
|
|
2829
|
+
const newerThanNative = changedAt !== null && (facts.nativeStamp === null || changedAt > facts.nativeStamp);
|
|
2830
|
+
if (changedAt !== null && newerThanNative) return {
|
|
2831
|
+
kind: "rise",
|
|
2832
|
+
at: changedAt,
|
|
2833
|
+
via: "changed-at-bound"
|
|
2834
|
+
};
|
|
2835
|
+
return { kind: "none" };
|
|
2836
|
+
}
|
|
2837
|
+
function claimed(owned) {
|
|
2838
|
+
return owned.kind === "stamp" || owned.kind === "never";
|
|
2839
|
+
}
|
|
2840
|
+
function decidePulseSeed(facts) {
|
|
2841
|
+
const proved = proof(facts);
|
|
2842
|
+
if (proved.kind === "hold") return claimed(facts.owned) ? { kind: "keep" } : proved;
|
|
2843
|
+
if (proved.kind === "rise") {
|
|
2844
|
+
if (facts.owned.kind === "stamp" && facts.owned.at >= proved.at) return { kind: "keep" };
|
|
2845
|
+
const count = facts.owned.kind === "stamp" ? facts.owned.count : 0;
|
|
2846
|
+
return {
|
|
2847
|
+
kind: "stamp",
|
|
2848
|
+
at: proved.at,
|
|
2849
|
+
count,
|
|
2850
|
+
via: proved.via
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
return claimed(facts.owned) ? { kind: "keep" } : {
|
|
2854
|
+
kind: "none",
|
|
2855
|
+
count: 0
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
/** One failed source blocks "no ring". A rise from another source still stands. */
|
|
2859
|
+
function mergePulseDecisions(decisions) {
|
|
2860
|
+
if (decisions.length === 0) return {
|
|
2861
|
+
kind: "none",
|
|
2862
|
+
count: 0
|
|
2863
|
+
};
|
|
2864
|
+
const stamps = decisions.flatMap((decision) => decision.kind === "stamp" ? [decision] : []);
|
|
2865
|
+
if (stamps.length > 0) return stamps.reduce((best, next) => next.at > best.at ? next : best);
|
|
2866
|
+
const hold = decisions.find((decision) => decision.kind === "hold");
|
|
2867
|
+
if (hold !== void 0) return hold;
|
|
2868
|
+
if (decisions.some((decision) => decision.kind === "keep")) return { kind: "keep" };
|
|
2869
|
+
return {
|
|
2870
|
+
kind: "none",
|
|
2871
|
+
count: 0
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
//#endregion
|
|
2875
|
+
//#region src/builtins/composer/pulse-seed-resolve.ts
|
|
2876
|
+
/**
|
|
2877
|
+
* Gather a pulse override's start seed (D938). The stored expression is not
|
|
2878
|
+
* changed. A failed read holds the field: no seed, and a claimed stamp is
|
|
2879
|
+
* not cleared (D49).
|
|
2880
|
+
*/
|
|
2881
|
+
var ROSE_CALL = String.raw`rose\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)`;
|
|
2882
|
+
function historySeriesKey(cap, fieldPath) {
|
|
2883
|
+
return require_retention.capSeriesKey(cap, fieldPath);
|
|
2884
|
+
}
|
|
2885
|
+
async function resolvePulseSeeds(input) {
|
|
2886
|
+
try {
|
|
2887
|
+
return await resolveNow(input);
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
input.logger.warn("pulse override seed held — gathering the source facts threw, so nothing is seeded and nothing is cleared", {
|
|
2890
|
+
tags: { deviceId: input.deviceId },
|
|
2891
|
+
meta: {
|
|
2892
|
+
blockId: input.blockId,
|
|
2893
|
+
error: require_dist.errMsg(err)
|
|
2894
|
+
}
|
|
2895
|
+
});
|
|
2896
|
+
return /* @__PURE__ */ new Map();
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
async function resolveNow(input) {
|
|
2900
|
+
const roses = input.fields.filter(isPulseTimestamp);
|
|
2901
|
+
if (roses.length === 0) return /* @__PURE__ */ new Map();
|
|
2902
|
+
const listing = await readClaims(input);
|
|
2903
|
+
if (listing === null) return /* @__PURE__ */ new Map();
|
|
2904
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
2905
|
+
for (const field of roses) {
|
|
2906
|
+
const decision = await decideField(input, field, listing);
|
|
2907
|
+
logDecision(input, field, decision);
|
|
2908
|
+
for (const [key, seed] of seedsFor(input.fields, field, decision)) seeds.set(key, seed);
|
|
2909
|
+
}
|
|
2910
|
+
return seeds;
|
|
2911
|
+
}
|
|
2912
|
+
function isPulseTimestamp(field) {
|
|
2913
|
+
return field.source.kind === "expression" && require_dist.expressionContainsBuiltinCall(field.source.expr, "rose");
|
|
2914
|
+
}
|
|
2915
|
+
async function readClaims(input) {
|
|
2916
|
+
try {
|
|
2917
|
+
const listing = await input.claims.listClaims({ ownerPrefix: input.blockId });
|
|
2918
|
+
if (listing.state === "loaded") return listing;
|
|
2919
|
+
input.logger.warn("pulse override seed held — the claims index has not loaded, so nothing is seeded and nothing is cleared", {
|
|
2920
|
+
tags: { deviceId: input.deviceId },
|
|
2921
|
+
meta: {
|
|
2922
|
+
blockId: input.blockId,
|
|
2923
|
+
reason: listing.reason
|
|
2924
|
+
}
|
|
2925
|
+
});
|
|
2926
|
+
return null;
|
|
2927
|
+
} catch (err) {
|
|
2928
|
+
input.logger.warn("pulse override seed held — the claims index could not be read, so nothing is seeded and nothing is cleared", {
|
|
2929
|
+
tags: { deviceId: input.deviceId },
|
|
2930
|
+
meta: {
|
|
2931
|
+
blockId: input.blockId,
|
|
2932
|
+
error: require_dist.errMsg(err)
|
|
2933
|
+
}
|
|
2934
|
+
});
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
async function decideField(input, field, listing) {
|
|
2939
|
+
if (listing.state !== "loaded") return {
|
|
2940
|
+
kind: "hold",
|
|
2941
|
+
reason: "claims-unread"
|
|
2942
|
+
};
|
|
2943
|
+
const claim = listing.claims.find((row) => row.owner === input.blockId && row.deviceId === input.deviceId && row.capName === field.cap);
|
|
2944
|
+
const slice = input.targetSeed.byCap.get(field.cap) ?? null;
|
|
2945
|
+
const counter = counterField(input.fields, field);
|
|
2946
|
+
const owned = ownedOf(claim, slice, field.path, counter?.path ?? null);
|
|
2947
|
+
const nativeStamp = stampOf(slice, field.path);
|
|
2948
|
+
const pairs = rosePairs(field);
|
|
2949
|
+
if (pairs.length === 0) return { kind: "keep" };
|
|
2950
|
+
const decisions = [];
|
|
2951
|
+
for (const pair of pairs) decisions.push(await decidePair(input, pair, owned, nativeStamp));
|
|
2952
|
+
return mergePulseDecisions(decisions);
|
|
2953
|
+
}
|
|
2954
|
+
async function decidePair(input, pair, owned, nativeStamp) {
|
|
2955
|
+
const resolution = await input.sources.resolve(pair.source, false);
|
|
2956
|
+
if (resolution.kind === "missing") return { kind: "keep" };
|
|
2957
|
+
if (resolution.kind !== "found") return decidePulseSeed({
|
|
2958
|
+
owned,
|
|
2959
|
+
nativeStamp,
|
|
2960
|
+
history: { kind: "unread" },
|
|
2961
|
+
level: { kind: "unread" }
|
|
2962
|
+
});
|
|
2963
|
+
return decidePulseSeed({
|
|
2964
|
+
owned,
|
|
2965
|
+
nativeStamp,
|
|
2966
|
+
history: await historyOf(input, resolution.deviceId, pair),
|
|
2967
|
+
level: await levelOf(input, resolution.deviceId, pair)
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
async function historyOf(input, deviceId, pair) {
|
|
2971
|
+
if (!recordsField(input.lookupCap, pair.cap, pair.activeField)) return { kind: "absent" };
|
|
2972
|
+
const now = input.now();
|
|
2973
|
+
const fromMs = now - require_retention.DEFAULT_RETENTION.rawDays * require_retention.DAY_MS;
|
|
2974
|
+
try {
|
|
2975
|
+
const answer = await input.readPulseHistory(deviceId, pair.cap, pair.activeField, fromMs, now);
|
|
2976
|
+
if (answer.kind === "unread") return { kind: "unread" };
|
|
2977
|
+
return {
|
|
2978
|
+
kind: "series",
|
|
2979
|
+
beforeOn: answer.series.beforeOn,
|
|
2980
|
+
points: answer.series.points
|
|
2981
|
+
};
|
|
2982
|
+
} catch (err) {
|
|
2983
|
+
input.logger.warn("pulse override history read threw — treated as unread", {
|
|
2984
|
+
tags: { deviceId: input.deviceId },
|
|
2985
|
+
meta: {
|
|
2986
|
+
blockId: input.blockId,
|
|
2987
|
+
sourceDeviceId: deviceId,
|
|
2988
|
+
cap: pair.cap,
|
|
2989
|
+
field: pair.activeField,
|
|
2990
|
+
error: require_dist.errMsg(err)
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
return { kind: "unread" };
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
async function levelOf(input, deviceId, pair) {
|
|
2997
|
+
try {
|
|
2998
|
+
return levelFromSlice(await input.readCap(deviceId, pair.cap), pair);
|
|
2999
|
+
} catch (err) {
|
|
3000
|
+
input.logger.warn("pulse override source slice could not be read — treated as unread", {
|
|
3001
|
+
tags: { deviceId: input.deviceId },
|
|
3002
|
+
meta: {
|
|
3003
|
+
blockId: input.blockId,
|
|
3004
|
+
sourceDeviceId: deviceId,
|
|
3005
|
+
cap: pair.cap,
|
|
3006
|
+
error: require_dist.errMsg(err)
|
|
3007
|
+
}
|
|
3008
|
+
});
|
|
3009
|
+
return { kind: "unread" };
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
function levelFromSlice(slice, pair) {
|
|
3013
|
+
if (slice === null) return {
|
|
3014
|
+
kind: "off",
|
|
3015
|
+
changedAt: null
|
|
3016
|
+
};
|
|
3017
|
+
const active = slice[pair.activeField];
|
|
3018
|
+
if (typeof active !== "boolean") return { kind: "unread" };
|
|
3019
|
+
const changed = slice[pair.changedAtField];
|
|
3020
|
+
const changedAt = typeof changed === "number" && Number.isFinite(changed) ? changed : null;
|
|
3021
|
+
return active ? {
|
|
3022
|
+
kind: "on",
|
|
3023
|
+
changedAt
|
|
3024
|
+
} : {
|
|
3025
|
+
kind: "off",
|
|
3026
|
+
changedAt
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
function recordsField(lookup, cap, fieldPath) {
|
|
3030
|
+
const history = lookup(cap)?.history;
|
|
3031
|
+
if (history === void 0 || history === "none") return false;
|
|
3032
|
+
return Object.hasOwn(history.fields, fieldPath);
|
|
3033
|
+
}
|
|
3034
|
+
function ownedOf(claim, slice, timestampPath, counterPath) {
|
|
3035
|
+
if (claim === void 0 || slice === null || !claim.fields.includes(timestampPath)) return { kind: "none" };
|
|
3036
|
+
const at = slice[timestampPath];
|
|
3037
|
+
if (at === null) return { kind: "never" };
|
|
3038
|
+
if (typeof at !== "number" || !Number.isFinite(at)) return { kind: "none" };
|
|
3039
|
+
const rawCount = counterPath === null ? void 0 : slice[counterPath];
|
|
3040
|
+
return {
|
|
3041
|
+
kind: "stamp",
|
|
3042
|
+
at,
|
|
3043
|
+
count: typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount >= 0 ? rawCount : 0
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
function stampOf(slice, path) {
|
|
3047
|
+
const at = slice?.[path];
|
|
3048
|
+
return typeof at === "number" && Number.isFinite(at) ? at : null;
|
|
3049
|
+
}
|
|
3050
|
+
function counterField(fields, timestamp) {
|
|
3051
|
+
return fields.find((field) => {
|
|
3052
|
+
if (field.cap !== timestamp.cap || field.path === timestamp.path) return false;
|
|
3053
|
+
return field.source.kind === "expression" && field.source.expr === `count(${timestamp.path})`;
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
function rosePairs(field) {
|
|
3057
|
+
if (field.source.kind !== "expression") return [];
|
|
3058
|
+
const found = [];
|
|
3059
|
+
for (const match of field.source.expr.matchAll(new RegExp(ROSE_CALL, "g"))) {
|
|
3060
|
+
const pair = rosePair(field, match[1], match[2]);
|
|
3061
|
+
if (pair !== null) found.push(pair);
|
|
3062
|
+
}
|
|
3063
|
+
return found;
|
|
3064
|
+
}
|
|
3065
|
+
function rosePair(field, activeName, changedName) {
|
|
3066
|
+
if (field.source.kind !== "expression") return null;
|
|
3067
|
+
if (activeName === void 0 || changedName === void 0) return null;
|
|
3068
|
+
const active = field.source.bindings[activeName];
|
|
3069
|
+
const changed = field.source.bindings[changedName];
|
|
3070
|
+
if (active === void 0 || changed === void 0) return null;
|
|
3071
|
+
if (active.kind !== "from" || changed.kind !== "from") return null;
|
|
3072
|
+
if (active.cap !== changed.cap) return null;
|
|
3073
|
+
if (active.source.addonId !== changed.source.addonId) return null;
|
|
3074
|
+
if (active.source.stableId !== changed.source.stableId) return null;
|
|
3075
|
+
return {
|
|
3076
|
+
source: active.source,
|
|
3077
|
+
cap: active.cap,
|
|
3078
|
+
activeField: active.fieldPath,
|
|
3079
|
+
changedAtField: changed.fieldPath
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
function seedsFor(fields, timestamp, decision) {
|
|
3083
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
3084
|
+
if (decision.kind === "keep") return seeds;
|
|
3085
|
+
const counter = counterField(fields, timestamp);
|
|
3086
|
+
if (decision.kind === "hold") {
|
|
3087
|
+
seeds.set(fieldKey(timestamp), { kind: "hold" });
|
|
3088
|
+
if (counter !== void 0) seeds.set(fieldKey(counter), {
|
|
3089
|
+
kind: "value",
|
|
3090
|
+
value: 0
|
|
3091
|
+
});
|
|
3092
|
+
return seeds;
|
|
3093
|
+
}
|
|
3094
|
+
if (decision.kind === "none") {
|
|
3095
|
+
seeds.set(fieldKey(timestamp), {
|
|
3096
|
+
kind: "value",
|
|
3097
|
+
value: null
|
|
3098
|
+
});
|
|
3099
|
+
if (counter !== void 0) seeds.set(fieldKey(counter), {
|
|
3100
|
+
kind: "value",
|
|
3101
|
+
value: 0
|
|
3102
|
+
});
|
|
3103
|
+
return seeds;
|
|
3104
|
+
}
|
|
3105
|
+
seeds.set(fieldKey(timestamp), {
|
|
3106
|
+
kind: "value",
|
|
3107
|
+
value: decision.at
|
|
3108
|
+
});
|
|
3109
|
+
if (counter !== void 0) seeds.set(fieldKey(counter), {
|
|
3110
|
+
kind: "value",
|
|
3111
|
+
value: decision.count
|
|
3112
|
+
});
|
|
3113
|
+
return seeds;
|
|
3114
|
+
}
|
|
3115
|
+
/** Raw boolean points of the device-level series (`scope` empty). 1 is on. */
|
|
3116
|
+
function pulseHistoryFromQuery(series) {
|
|
3117
|
+
const row = series.find((item) => item.scope === "");
|
|
3118
|
+
if (row === void 0) return {
|
|
3119
|
+
beforeOn: null,
|
|
3120
|
+
points: []
|
|
3121
|
+
};
|
|
3122
|
+
return {
|
|
3123
|
+
beforeOn: levelBit(row.before?.value ?? null),
|
|
3124
|
+
points: row.points.flatMap((point) => {
|
|
3125
|
+
const on = levelBit(point.last);
|
|
3126
|
+
return on === null ? [] : [{
|
|
3127
|
+
t: point.t,
|
|
3128
|
+
on
|
|
3129
|
+
}];
|
|
3130
|
+
})
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
function levelBit(value) {
|
|
3134
|
+
if (value === 1) return true;
|
|
3135
|
+
if (value === 0) return false;
|
|
3136
|
+
return null;
|
|
3137
|
+
}
|
|
3138
|
+
function logDecision(input, field, decision) {
|
|
3139
|
+
const meta = {
|
|
3140
|
+
blockId: input.blockId,
|
|
3141
|
+
cap: field.cap,
|
|
3142
|
+
field: field.path
|
|
3143
|
+
};
|
|
3144
|
+
if (decision.kind === "stamp") {
|
|
3145
|
+
input.logger.info("pulse override seeded lastPressedAt from its source", {
|
|
3146
|
+
tags: { deviceId: input.deviceId },
|
|
3147
|
+
meta: {
|
|
3148
|
+
...meta,
|
|
3149
|
+
at: decision.at,
|
|
3150
|
+
via: decision.via,
|
|
3151
|
+
count: decision.count
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
if (decision.kind === "none") {
|
|
3157
|
+
input.logger.info("pulse override seeded no ring — the source proved none, so the native stamp is not used", {
|
|
3158
|
+
tags: { deviceId: input.deviceId },
|
|
3159
|
+
meta
|
|
3160
|
+
});
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
if (decision.kind === "hold") input.logger.warn("pulse override seed held — a source read failed, so nothing is seeded and nothing is cleared", {
|
|
3164
|
+
tags: { deviceId: input.deviceId },
|
|
3165
|
+
meta: {
|
|
3166
|
+
...meta,
|
|
3167
|
+
reason: decision.reason
|
|
3168
|
+
}
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
//#endregion
|
|
2679
3172
|
//#region src/builtins/composer/composer.ts
|
|
2680
3173
|
/**
|
|
2681
3174
|
* The composer: every declarative composition on this hub, in one runner
|
|
@@ -2698,7 +3191,11 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
|
|
|
2698
3191
|
* filter in `composer.addon.ts` stays load-bearing. Its fields are CLAIMED on
|
|
2699
3192
|
* the hub under the block id (`OwnedFieldsTarget`), and released through the
|
|
2700
3193
|
* `ClaimGate` — two applies must agree, seeded at boot from the claims index —
|
|
2701
|
-
* when the block
|
|
3194
|
+
* when the block merely vanished from one snapshot. An operator delete
|
|
3195
|
+
* (`retiredBlockIds`) or an edit that drops a cap releases those claims on
|
|
3196
|
+
* THIS apply, after the old runtime has quiesced and before any replacement
|
|
3197
|
+
* claims; a release that does not land is logged with `tags: { deviceId }` and
|
|
3198
|
+
* returned on the apply. Its own replaced
|
|
2702
3199
|
* fields are read in the NATIVE view (rule 6). Its target row gone on two
|
|
2703
3200
|
* agreeing index reads fails the block by name and ungrafts what it added —
|
|
2704
3201
|
* whether the loss was seen on `device.unregistered` or on a later plan; its
|
|
@@ -2846,12 +3343,16 @@ var Composer = class {
|
|
|
2846
3343
|
};
|
|
2847
3344
|
});
|
|
2848
3345
|
await this.composed.reconcile(gate.integrationId, declarations, planned);
|
|
3346
|
+
const releasing = confirmedReleaseKeys(input.retiredBlockIds ?? [], planned, this.live, this.claimGate);
|
|
2849
3347
|
const rebuilt = await this.rebuildRuntimes(planned, declarations, restart, rebuildErrors);
|
|
3348
|
+
await Promise.all(rebuilt.stopped.map((runtime) => runtime.quiesce()));
|
|
3349
|
+
const unreleasedClaims = await this.releaseConfirmed(releasing);
|
|
2850
3350
|
await this.syncSources(rebuilt);
|
|
2851
3351
|
await this.reconcileClaims();
|
|
2852
3352
|
return {
|
|
2853
3353
|
blocks: planned.map((p) => this.verdict(p.block.id)),
|
|
2854
|
-
acted: true
|
|
3354
|
+
acted: true,
|
|
3355
|
+
unreleasedClaims
|
|
2855
3356
|
};
|
|
2856
3357
|
}
|
|
2857
3358
|
plan(block, restart, claimsWait) {
|
|
@@ -2872,7 +3373,13 @@ var Composer = class {
|
|
|
2872
3373
|
async rebuildRuntimes(planned, declarations, restart, rebuildErrors) {
|
|
2873
3374
|
const reset = /* @__PURE__ */ new Set();
|
|
2874
3375
|
const fresh = /* @__PURE__ */ new Set();
|
|
3376
|
+
const stopped = [];
|
|
2875
3377
|
const next = /* @__PURE__ */ new Map();
|
|
3378
|
+
const halt = (runtime) => {
|
|
3379
|
+
if (runtime === null) return;
|
|
3380
|
+
runtime.stop();
|
|
3381
|
+
stopped.push(runtime);
|
|
3382
|
+
};
|
|
2876
3383
|
for (const p of planned) {
|
|
2877
3384
|
const previous = this.live.get(p.block.id);
|
|
2878
3385
|
const lost = lostTarget(p, previous?.planned);
|
|
@@ -2899,6 +3406,7 @@ var Composer = class {
|
|
|
2899
3406
|
continue;
|
|
2900
3407
|
}
|
|
2901
3408
|
let targetSeed;
|
|
3409
|
+
let pulseSeeds;
|
|
2902
3410
|
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 {
|
|
2903
3411
|
targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
|
|
2904
3412
|
reportSeedlessRestoredCaps({
|
|
@@ -2909,6 +3417,20 @@ var Composer = class {
|
|
|
2909
3417
|
seed: targetSeed,
|
|
2910
3418
|
lookupCap: this.deps.lookupCap
|
|
2911
3419
|
});
|
|
3420
|
+
const readHistory = this.deps.readPulseHistory;
|
|
3421
|
+
if (readHistory !== void 0) pulseSeeds = await resolvePulseSeeds({
|
|
3422
|
+
blockId: p.block.id,
|
|
3423
|
+
deviceId: p.target.deviceId,
|
|
3424
|
+
fields: p.fields,
|
|
3425
|
+
targetSeed,
|
|
3426
|
+
lookupCap: this.deps.lookupCap,
|
|
3427
|
+
claims: this.deps.claims,
|
|
3428
|
+
sources: this.deps.tracker,
|
|
3429
|
+
readCap: this.deps.readExistingTargetCap,
|
|
3430
|
+
readPulseHistory: readHistory,
|
|
3431
|
+
now: this.deps.now,
|
|
3432
|
+
logger: this.deps.logger
|
|
3433
|
+
});
|
|
2912
3434
|
} catch (err) {
|
|
2913
3435
|
const reason = `the target device state could not be read before ownership (${require_dist.errMsg(err)}) — nothing is grafted or claimed until it is; retried on the next apply`;
|
|
2914
3436
|
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", {
|
|
@@ -2934,7 +3456,7 @@ var Composer = class {
|
|
|
2934
3456
|
const writer = writerFor(this.deps, p, device, (deviceId, cap, owner, event) => {
|
|
2935
3457
|
this.onClaimEvent(deviceId, cap, owner, event);
|
|
2936
3458
|
});
|
|
2937
|
-
const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed);
|
|
3459
|
+
const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed, pulseSeeds);
|
|
2938
3460
|
if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
|
|
2939
3461
|
deviceId: p.target.deviceId,
|
|
2940
3462
|
ref: p.target.ref,
|
|
@@ -2943,7 +3465,7 @@ var Composer = class {
|
|
|
2943
3465
|
enabled: p.block.enabled,
|
|
2944
3466
|
settled: p.problems.length === 0
|
|
2945
3467
|
});
|
|
2946
|
-
previous?.runtime
|
|
3468
|
+
halt(previous?.runtime ?? null);
|
|
2947
3469
|
if (runtime !== null) fresh.add(runtime);
|
|
2948
3470
|
next.set(p.block.id, {
|
|
2949
3471
|
planned: p,
|
|
@@ -2958,12 +3480,15 @@ var Composer = class {
|
|
|
2958
3480
|
for (const [blockId, entry] of this.live) {
|
|
2959
3481
|
if (next.has(blockId)) continue;
|
|
2960
3482
|
if (held.has(require_dist.composedDeviceStableId(blockId))) next.set(blockId, entry);
|
|
2961
|
-
else entry.runtime
|
|
3483
|
+
else halt(entry.runtime);
|
|
2962
3484
|
}
|
|
2963
3485
|
this.live = next;
|
|
2964
3486
|
const liveRuntimes = new Set([...next.values()].map((e) => e.runtime));
|
|
2965
3487
|
this.unsynced = new Set([...[...this.unsynced].filter((r) => liveRuntimes.has(r)), ...fresh]);
|
|
2966
|
-
return {
|
|
3488
|
+
return {
|
|
3489
|
+
reset,
|
|
3490
|
+
stopped
|
|
3491
|
+
};
|
|
2967
3492
|
}
|
|
2968
3493
|
ungraftGone(blockId, target, deviceId) {
|
|
2969
3494
|
this.grafts.gone(deviceId);
|
|
@@ -3047,21 +3572,40 @@ var Composer = class {
|
|
|
3047
3572
|
if (!this.claimSeed.done) return;
|
|
3048
3573
|
for (const key of this.claimGate.filter(wantedClaims(this.live, this.claimGate))) await this.release(key);
|
|
3049
3574
|
}
|
|
3575
|
+
/** Confirmed releases, awaited. A failure is the string the hub gave; the gate retries it later. */
|
|
3576
|
+
async releaseConfirmed(keys) {
|
|
3577
|
+
const unreleased = [];
|
|
3578
|
+
for (const key of keys) {
|
|
3579
|
+
const error = await this.release(key);
|
|
3580
|
+
if (error === null) continue;
|
|
3581
|
+
unreleased.push({
|
|
3582
|
+
deviceId: key.deviceId,
|
|
3583
|
+
capName: key.capName,
|
|
3584
|
+
owner: key.owner,
|
|
3585
|
+
error
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
return unreleased;
|
|
3589
|
+
}
|
|
3590
|
+
/** Null when the hub confirmed the release (or held nothing of ours). */
|
|
3050
3591
|
async release(key) {
|
|
3051
3592
|
let outcome;
|
|
3052
3593
|
try {
|
|
3053
3594
|
outcome = await this.deps.claims.releaseClaim(key);
|
|
3054
3595
|
} catch (err) {
|
|
3055
|
-
|
|
3056
|
-
|
|
3596
|
+
const error = require_dist.errMsg(err);
|
|
3597
|
+
this.claimGate.releaseFailed(key, error);
|
|
3598
|
+
return error;
|
|
3057
3599
|
}
|
|
3058
3600
|
if (!outcome.ok && !RELEASE_SETTLED_CODES.has(outcome.code)) {
|
|
3059
|
-
|
|
3060
|
-
|
|
3601
|
+
const error = `${outcome.code}: ${outcome.message}`;
|
|
3602
|
+
this.claimGate.releaseFailed(key, error);
|
|
3603
|
+
return error;
|
|
3061
3604
|
}
|
|
3062
3605
|
this.claimGate.released(key, outcome.ok ? "released" : outcome.code);
|
|
3063
3606
|
await this.grafts.released(key.deviceId, key.capName);
|
|
3064
3607
|
await this.onClaimEvent(key.deviceId, key.capName, key.owner, "free");
|
|
3608
|
+
return null;
|
|
3065
3609
|
}
|
|
3066
3610
|
/**
|
|
3067
3611
|
* Rule 5: a customization whose target row is gone — the tracker's two
|
|
@@ -4045,6 +4589,8 @@ var SourceTracker = class {
|
|
|
4045
4589
|
*/
|
|
4046
4590
|
/** Ignored by `DeclaredDevices` whenever `integrationId` is supplied, which is always here. */
|
|
4047
4591
|
var DECLARED_INTEGRATION_NAME_UNUSED = "Blocks";
|
|
4592
|
+
/** The device-history query ceiling. A switch's raw tier is change-only, so this is the whole window. */
|
|
4593
|
+
var PULSE_HISTORY_MAX_POINTS = 5e3;
|
|
4048
4594
|
/** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
|
|
4049
4595
|
function parseNativeShadowChanged(data) {
|
|
4050
4596
|
if (!isRecord$2(data)) return null;
|
|
@@ -4193,6 +4739,26 @@ var ComposerAddon = class extends require_dist.BaseAddon {
|
|
|
4193
4739
|
deviceId,
|
|
4194
4740
|
capName
|
|
4195
4741
|
}) ?? null,
|
|
4742
|
+
readPulseHistory: async (deviceId, cap, fieldPath, fromMs, toMs) => {
|
|
4743
|
+
try {
|
|
4744
|
+
return {
|
|
4745
|
+
kind: "series",
|
|
4746
|
+
series: pulseHistoryFromQuery((await api.deviceHistory.query.query({
|
|
4747
|
+
deviceId,
|
|
4748
|
+
key: historySeriesKey(cap, fieldPath),
|
|
4749
|
+
from: fromMs,
|
|
4750
|
+
to: toMs,
|
|
4751
|
+
resolution: "raw",
|
|
4752
|
+
maxPoints: PULSE_HISTORY_MAX_POINTS
|
|
4753
|
+
})).series)
|
|
4754
|
+
};
|
|
4755
|
+
} catch (err) {
|
|
4756
|
+
return {
|
|
4757
|
+
kind: "unread",
|
|
4758
|
+
error: require_dist.errMsg(err)
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
},
|
|
4196
4762
|
describeTarget: readBoundCaps,
|
|
4197
4763
|
grafts,
|
|
4198
4764
|
now: () => Date.now(),
|