@camstack/system 1.2.86 → 1.2.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +155 -18
- package/dist/builtins/device-manager/device-manager.addon.mjs +155 -18
- package/dist/builtins/device-manager/device-state-mirror.d.ts +51 -6
- package/dist/builtins/device-manager/runtime-state-persist-gate.d.ts +32 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.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/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +15 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +37 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +37 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-B90N7AWO.mjs → dist-CuAaqtdt.mjs} +929 -83
- package/dist/{dist-BV5Bug9t.js → dist-D4nuaPdd.js} +934 -82
- package/dist/index.js +30 -4
- package/dist/index.mjs +30 -4
- package/dist/kernel/config-manager.d.ts +32 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-D4nuaPdd.js");
|
|
7
7
|
const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ut as EventCategory, _t as BaseAddon, gt as errMsg, v as addonPagesCapability } from "../../dist-CuAaqtdt.mjs";
|
|
2
2
|
import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-D4nuaPdd.js");
|
|
7
7
|
const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ut as EventCategory, _t as BaseAddon, b as addonWidgetsCapability, gt as errMsg } from "../../dist-CuAaqtdt.mjs";
|
|
2
2
|
import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-D4nuaPdd.js");
|
|
7
7
|
//#region src/builtins/alerts/prune-policy.ts
|
|
8
8
|
/**
|
|
9
9
|
* Ids older than `cutoffMs`, oldest first.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { At as createEvent, Ut as EventCategory, _t as BaseAddon, gt as errMsg, n as AlertSchema, x as alertsCapability } from "../../dist-CuAaqtdt.mjs";
|
|
2
2
|
//#region src/builtins/alerts/prune-policy.ts
|
|
3
3
|
/**
|
|
4
4
|
* Ids older than `cutoffMs`, oldest first.
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-D4nuaPdd.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let buffer = require("buffer");
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as backupCapability,
|
|
1
|
+
import { C as backupCapability, Ut as EventCategory, _t as BaseAddon } from "../../dist-CuAaqtdt.mjs";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { Buffer as Buffer$1 } from "buffer";
|
|
4
4
|
import * as fsp from "node:fs/promises";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-D4nuaPdd.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
//#region src/builtins/console-logging/console-destination.ts
|
|
9
9
|
var LEVEL_RANK = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { J as logDestinationCapability,
|
|
1
|
+
import { J as logDestinationCapability, _t as BaseAddon } from "../../dist-CuAaqtdt.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
//#region src/builtins/console-logging/console-destination.ts
|
|
4
4
|
var LEVEL_RANK = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-D4nuaPdd.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as coreBlockIdFromAddonId, E as coreBlockAddonId, O as coreBlocksCapability,
|
|
1
|
+
import { D as coreBlockIdFromAddonId, E as coreBlockAddonId, O as coreBlocksCapability, _t as BaseAddon, c as CoreBlockSchema, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX } from "../../dist-CuAaqtdt.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-D4nuaPdd.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let _camstack_types_node = require("@camstack/types/node");
|
|
9
9
|
//#region src/builtins/device-manager/adoption-job-engine.ts
|
|
@@ -3942,6 +3942,88 @@ var DeviceMetaStore = class {
|
|
|
3942
3942
|
};
|
|
3943
3943
|
};
|
|
3944
3944
|
//#endregion
|
|
3945
|
+
//#region src/builtins/device-manager/runtime-state-persist-gate.ts
|
|
3946
|
+
/**
|
|
3947
|
+
* The subset of `blob` that is allowed on disk: slices whose capability
|
|
3948
|
+
* declared `durability: 'restored'`.
|
|
3949
|
+
*
|
|
3950
|
+
* A cap the lookup does not know resolves to `'session'` and is dropped. That
|
|
3951
|
+
* direction is deliberate: the cost of losing a session value is a cold window
|
|
3952
|
+
* until the provider republishes; the cost of persisting an unasked-for one is
|
|
3953
|
+
* a commit per second on the fleet's busiest write path.
|
|
3954
|
+
*/
|
|
3955
|
+
function persistableBlob(blob, policyFor) {
|
|
3956
|
+
const out = {};
|
|
3957
|
+
for (const [capName, slice] of Object.entries(blob)) {
|
|
3958
|
+
if (policyFor(capName).durability !== "restored") continue;
|
|
3959
|
+
out[capName] = { ...slice };
|
|
3960
|
+
}
|
|
3961
|
+
return out;
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Are two slices the same, ignoring the fields the cap declared volatile?
|
|
3965
|
+
*
|
|
3966
|
+
* A volatile field is still WRITTEN — it simply does not, on its own, justify
|
|
3967
|
+
* the write. A field the cap did NOT declare volatile is content, even when its
|
|
3968
|
+
* name looks like a clock: the capability is the only authority on which of its
|
|
3969
|
+
* fields carry meaning (D4), and the guard makes an undeclared clock visible in
|
|
3970
|
+
* the diff rather than assuming it.
|
|
3971
|
+
*/
|
|
3972
|
+
function sliceEffectivelyEqual(a, b, volatileFields) {
|
|
3973
|
+
const skip = new Set(volatileFields);
|
|
3974
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3975
|
+
for (const k of Object.keys(a)) if (!skip.has(k)) keys.add(k);
|
|
3976
|
+
for (const k of Object.keys(b)) if (!skip.has(k)) keys.add(k);
|
|
3977
|
+
for (const k of keys) {
|
|
3978
|
+
if (!deepEqual(a[k], b[k])) return false;
|
|
3979
|
+
if (Object.hasOwn(a, k) !== Object.hasOwn(b, k)) return false;
|
|
3980
|
+
}
|
|
3981
|
+
return true;
|
|
3982
|
+
}
|
|
3983
|
+
/**
|
|
3984
|
+
* Is the whole persistable blob unchanged in every way that matters?
|
|
3985
|
+
*
|
|
3986
|
+
* A cap slice APPEARING or DISAPPEARING is always a change — the write is a
|
|
3987
|
+
* whole-blob replace, so a dropped slice only leaves disk if the write happens.
|
|
3988
|
+
*/
|
|
3989
|
+
function effectivelyEqual(before, after, policyFor) {
|
|
3990
|
+
const caps = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
3991
|
+
for (const capName of caps) {
|
|
3992
|
+
const a = before[capName];
|
|
3993
|
+
const b = after[capName];
|
|
3994
|
+
if (a === void 0 || b === void 0) return false;
|
|
3995
|
+
if (!sliceEffectivelyEqual(a, b, policyFor(capName).volatileFields)) return false;
|
|
3996
|
+
}
|
|
3997
|
+
return true;
|
|
3998
|
+
}
|
|
3999
|
+
/**
|
|
4000
|
+
* Structural equality over the JSON values a runtime-state slice can hold.
|
|
4001
|
+
* `NaN` compares equal to itself so a sensor that reports it does not rewrite
|
|
4002
|
+
* the blob forever.
|
|
4003
|
+
*/
|
|
4004
|
+
function deepEqual(a, b) {
|
|
4005
|
+
if (a === b) return true;
|
|
4006
|
+
if (typeof a === "number" && typeof b === "number") return Number.isNaN(a) && Number.isNaN(b);
|
|
4007
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
4008
|
+
const aIsArray = Array.isArray(a);
|
|
4009
|
+
if (aIsArray !== Array.isArray(b)) return false;
|
|
4010
|
+
if (aIsArray) {
|
|
4011
|
+
const bArr = b;
|
|
4012
|
+
if (a.length !== bArr.length) return false;
|
|
4013
|
+
for (let i = 0; i < a.length; i += 1) if (!deepEqual(a[i], bArr[i])) return false;
|
|
4014
|
+
return true;
|
|
4015
|
+
}
|
|
4016
|
+
const aRec = a;
|
|
4017
|
+
const bRec = b;
|
|
4018
|
+
const aKeys = Object.keys(aRec);
|
|
4019
|
+
if (aKeys.length !== Object.keys(bRec).length) return false;
|
|
4020
|
+
for (const k of aKeys) {
|
|
4021
|
+
if (!Object.hasOwn(bRec, k)) return false;
|
|
4022
|
+
if (!deepEqual(aRec[k], bRec[k])) return false;
|
|
4023
|
+
}
|
|
4024
|
+
return true;
|
|
4025
|
+
}
|
|
4026
|
+
//#endregion
|
|
3945
4027
|
//#region src/builtins/device-manager/device-state-mirror.ts
|
|
3946
4028
|
/**
|
|
3947
4029
|
* Hub-side runtime-state mirror for the device-manager addon.
|
|
@@ -3960,6 +4042,7 @@ var DeviceMetaStore = class {
|
|
|
3960
4042
|
*/
|
|
3961
4043
|
var DeviceStateMirror = class DeviceStateMirror {
|
|
3962
4044
|
ctx;
|
|
4045
|
+
policyFor;
|
|
3963
4046
|
/**
|
|
3964
4047
|
* Hub-side mirror of every device's cap-keyed runtime state.
|
|
3965
4048
|
* Key: deviceId. Value: per-cap slice map. Empty by default —
|
|
@@ -3973,8 +4056,24 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
3973
4056
|
*/
|
|
3974
4057
|
runtimeStateDebounce = /* @__PURE__ */ new Map();
|
|
3975
4058
|
static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
|
|
3976
|
-
|
|
4059
|
+
/**
|
|
4060
|
+
* What is believed to be ON DISK for each device — the persistable projection
|
|
4061
|
+
* of the last blob actually written (or seeded at boot). The effective-change
|
|
4062
|
+
* gate compares against THIS, not against the previous mirror state: two
|
|
4063
|
+
* clock-only ticks in a row must not add up to a write just because each was
|
|
4064
|
+
* compared with its immediate predecessor.
|
|
4065
|
+
*/
|
|
4066
|
+
lastPersisted = /* @__PURE__ */ new Map();
|
|
4067
|
+
/**
|
|
4068
|
+
* Per-device count of writes the effective-change gate skipped since the last
|
|
4069
|
+
* real one. Reported on the next write that DOES happen, so the log says how
|
|
4070
|
+
* much churn the gate absorbed instead of saying nothing at all — a branch
|
|
4071
|
+
* that drops work silently reads as "never happened".
|
|
4072
|
+
*/
|
|
4073
|
+
skippedSinceWrite = /* @__PURE__ */ new Map();
|
|
4074
|
+
constructor(ctx, policyFor = require_dist.runtimeStatePolicyFor) {
|
|
3977
4075
|
this.ctx = ctx;
|
|
4076
|
+
this.policyFor = policyFor;
|
|
3978
4077
|
}
|
|
3979
4078
|
/**
|
|
3980
4079
|
* Single-cap mirror update — diff against the current mirror,
|
|
@@ -4001,13 +4100,23 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4001
4100
|
return true;
|
|
4002
4101
|
}
|
|
4003
4102
|
/**
|
|
4004
|
-
* Debounced disk writer
|
|
4005
|
-
*
|
|
4006
|
-
*
|
|
4007
|
-
*
|
|
4103
|
+
* Debounced disk writer, behind two gates.
|
|
4104
|
+
*
|
|
4105
|
+
* GATE — DURABILITY: a change to a `durability: 'session'` cap never even
|
|
4106
|
+
* arms the timer. That is where the fleet's write rate goes: `audio-metrics`
|
|
4107
|
+
* and `zone-analytics` are ~90 % of the offered 12–19 writes/s, they change
|
|
4108
|
+
* genuinely every second, and their restored value is worthless.
|
|
4109
|
+
*
|
|
4110
|
+
* GATE — EFFECTIVE CHANGE (at flush): the persistable blob is compared with
|
|
4111
|
+
* what is believed to be on disk, ignoring the clock fields each cap declared
|
|
4112
|
+
* volatile. `battery` wrote its blob 526 times in 25 minutes without one
|
|
4113
|
+
* percentage moving; this is the gate that turns those into one write.
|
|
4114
|
+
*
|
|
4115
|
+
* The blob is read from the live mirror at flush time, so the disk picture is
|
|
4008
4116
|
* always the latest state — no risk of writing a stale snapshot.
|
|
4009
4117
|
*/
|
|
4010
|
-
scheduleRuntimeStateDiskWrite(deviceId, settings) {
|
|
4118
|
+
scheduleRuntimeStateDiskWrite(deviceId, settings, changedCap) {
|
|
4119
|
+
if (this.policyFor(changedCap).durability !== "restored") return;
|
|
4011
4120
|
let slot = this.runtimeStateDebounce.get(deviceId);
|
|
4012
4121
|
if (!slot) {
|
|
4013
4122
|
slot = {
|
|
@@ -4019,10 +4128,21 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4019
4128
|
if (slot.timer) return;
|
|
4020
4129
|
slot.timer = setTimeout(() => {
|
|
4021
4130
|
slot.timer = null;
|
|
4022
|
-
const blob = this.
|
|
4131
|
+
const blob = this.persistableForDevice(deviceId);
|
|
4132
|
+
if (this.isAlreadyPersisted(deviceId, blob)) return;
|
|
4133
|
+
const skipped = this.skippedSinceWrite.get(deviceId) ?? 0;
|
|
4134
|
+
this.skippedSinceWrite.delete(deviceId);
|
|
4023
4135
|
const write = (async () => {
|
|
4024
4136
|
try {
|
|
4025
4137
|
await settings.writeDeviceRuntimeState(deviceId, blob);
|
|
4138
|
+
this.lastPersisted.set(deviceId, blob);
|
|
4139
|
+
if (skipped > 0) this.ctx.logger.debug("runtime state persisted", {
|
|
4140
|
+
tags: { deviceId },
|
|
4141
|
+
meta: {
|
|
4142
|
+
caps: Object.keys(blob).length,
|
|
4143
|
+
skippedSinceLastWrite: skipped
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4026
4146
|
} catch (err) {
|
|
4027
4147
|
this.ctx.logger.warn("writeDeviceRuntimeState failed", {
|
|
4028
4148
|
tags: { deviceId },
|
|
@@ -4036,6 +4156,22 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4036
4156
|
}, DeviceStateMirror.RUNTIME_STATE_DEBOUNCE_MS);
|
|
4037
4157
|
}
|
|
4038
4158
|
/**
|
|
4159
|
+
* True when `blob` differs from what is believed to be on disk only in fields
|
|
4160
|
+
* the owning caps declared volatile. Counts the skip so the next real write
|
|
4161
|
+
* can report how much churn was absorbed.
|
|
4162
|
+
*/
|
|
4163
|
+
isAlreadyPersisted(deviceId, blob) {
|
|
4164
|
+
const onDisk = this.lastPersisted.get(deviceId);
|
|
4165
|
+
if (!onDisk) return false;
|
|
4166
|
+
if (!effectivelyEqual(onDisk, blob, this.policyFor)) return false;
|
|
4167
|
+
this.skippedSinceWrite.set(deviceId, (this.skippedSinceWrite.get(deviceId) ?? 0) + 1);
|
|
4168
|
+
return true;
|
|
4169
|
+
}
|
|
4170
|
+
/** The device's mirror, restricted to the slices that may reach disk. */
|
|
4171
|
+
persistableForDevice(deviceId) {
|
|
4172
|
+
return persistableBlob(this.snapshotForDevice(deviceId), this.policyFor);
|
|
4173
|
+
}
|
|
4174
|
+
/**
|
|
4039
4175
|
* One-shot mirror seed used by `loadRuntimeState` at boot so the
|
|
4040
4176
|
* hub knows about every persisted slice without waiting for the
|
|
4041
4177
|
* first `setCapSlice` call. No events emitted — this is
|
|
@@ -4055,6 +4191,7 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4055
4191
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
4056
4192
|
perCap.set(capName, { ...raw });
|
|
4057
4193
|
}
|
|
4194
|
+
this.lastPersisted.set(deviceId, persistableBlob(this.snapshotForDevice(deviceId), this.policyFor));
|
|
4058
4195
|
}
|
|
4059
4196
|
/**
|
|
4060
4197
|
* The hub mirror's `feature-probe.lastProbedAt` is a PER-SESSION liveness
|
|
@@ -4168,15 +4305,15 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4168
4305
|
if (slot.timer) {
|
|
4169
4306
|
clearTimeout(slot.timer);
|
|
4170
4307
|
slot.timer = null;
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
}
|
|
4178
|
-
})
|
|
4179
|
-
}
|
|
4308
|
+
const blob = settings ? this.persistableForDevice(deviceId) : null;
|
|
4309
|
+
if (settings && blob !== null && !this.isAlreadyPersisted(deviceId, blob)) pending.push(settings.writeDeviceRuntimeState(deviceId, blob).then(() => {
|
|
4310
|
+
this.lastPersisted.set(deviceId, blob);
|
|
4311
|
+
}).catch((err) => {
|
|
4312
|
+
this.ctx.logger.warn("shutdown writeDeviceRuntimeState failed", {
|
|
4313
|
+
tags: { deviceId },
|
|
4314
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
4315
|
+
});
|
|
4316
|
+
}));
|
|
4180
4317
|
}
|
|
4181
4318
|
if (slot.inFlight) pending.push(slot.inFlight);
|
|
4182
4319
|
}
|
|
@@ -4642,7 +4779,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
4642
4779
|
setCapSlice: async (input) => {
|
|
4643
4780
|
const { deviceId, capName, slice } = input;
|
|
4644
4781
|
if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] setCapSlice: unknown device id=${deviceId}`);
|
|
4645
|
-
if (stateMirror.applySingleCapUpdate(deviceId, capName, slice)) stateMirror.scheduleRuntimeStateDiskWrite(deviceId, settings);
|
|
4782
|
+
if (stateMirror.applySingleCapUpdate(deviceId, capName, slice)) stateMirror.scheduleRuntimeStateDiskWrite(deviceId, settings, capName);
|
|
4646
4783
|
}
|
|
4647
4784
|
}
|
|
4648
4785
|
}];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ht as
|
|
1
|
+
import { Ct as DeviceType, Et as WELL_KNOWN_TAB_MAP, Ft as isDeviceConfigCap, Ht as sleep, I as enumerateItemArrayFields, L as enumerateSchemaFields, M as deviceStateCapability, N as deviceStatusCapability, Q as normalizeUnit, St as DeviceRole, T as buildStreamParamsConfigSchema, Ut as EventCategory, _t as BaseAddon, a as CAP_NAMES_WITH_STATUS, et as parseStreamParamsFormPatch, gt as errMsg, j as deviceManagerCapability, p as STREAM_PROFILE_META, rt as runtimeStatePolicyFor, t as ALL_CAPABILITY_DEFINITIONS, u as DeviceStatusSchema, xt as DeviceFeature } from "../../dist-CuAaqtdt.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
4
4
|
//#region src/builtins/device-manager/adoption-job-engine.ts
|
|
@@ -3937,6 +3937,88 @@ var DeviceMetaStore = class {
|
|
|
3937
3937
|
};
|
|
3938
3938
|
};
|
|
3939
3939
|
//#endregion
|
|
3940
|
+
//#region src/builtins/device-manager/runtime-state-persist-gate.ts
|
|
3941
|
+
/**
|
|
3942
|
+
* The subset of `blob` that is allowed on disk: slices whose capability
|
|
3943
|
+
* declared `durability: 'restored'`.
|
|
3944
|
+
*
|
|
3945
|
+
* A cap the lookup does not know resolves to `'session'` and is dropped. That
|
|
3946
|
+
* direction is deliberate: the cost of losing a session value is a cold window
|
|
3947
|
+
* until the provider republishes; the cost of persisting an unasked-for one is
|
|
3948
|
+
* a commit per second on the fleet's busiest write path.
|
|
3949
|
+
*/
|
|
3950
|
+
function persistableBlob(blob, policyFor) {
|
|
3951
|
+
const out = {};
|
|
3952
|
+
for (const [capName, slice] of Object.entries(blob)) {
|
|
3953
|
+
if (policyFor(capName).durability !== "restored") continue;
|
|
3954
|
+
out[capName] = { ...slice };
|
|
3955
|
+
}
|
|
3956
|
+
return out;
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* Are two slices the same, ignoring the fields the cap declared volatile?
|
|
3960
|
+
*
|
|
3961
|
+
* A volatile field is still WRITTEN — it simply does not, on its own, justify
|
|
3962
|
+
* the write. A field the cap did NOT declare volatile is content, even when its
|
|
3963
|
+
* name looks like a clock: the capability is the only authority on which of its
|
|
3964
|
+
* fields carry meaning (D4), and the guard makes an undeclared clock visible in
|
|
3965
|
+
* the diff rather than assuming it.
|
|
3966
|
+
*/
|
|
3967
|
+
function sliceEffectivelyEqual(a, b, volatileFields) {
|
|
3968
|
+
const skip = new Set(volatileFields);
|
|
3969
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3970
|
+
for (const k of Object.keys(a)) if (!skip.has(k)) keys.add(k);
|
|
3971
|
+
for (const k of Object.keys(b)) if (!skip.has(k)) keys.add(k);
|
|
3972
|
+
for (const k of keys) {
|
|
3973
|
+
if (!deepEqual(a[k], b[k])) return false;
|
|
3974
|
+
if (Object.hasOwn(a, k) !== Object.hasOwn(b, k)) return false;
|
|
3975
|
+
}
|
|
3976
|
+
return true;
|
|
3977
|
+
}
|
|
3978
|
+
/**
|
|
3979
|
+
* Is the whole persistable blob unchanged in every way that matters?
|
|
3980
|
+
*
|
|
3981
|
+
* A cap slice APPEARING or DISAPPEARING is always a change — the write is a
|
|
3982
|
+
* whole-blob replace, so a dropped slice only leaves disk if the write happens.
|
|
3983
|
+
*/
|
|
3984
|
+
function effectivelyEqual(before, after, policyFor) {
|
|
3985
|
+
const caps = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
3986
|
+
for (const capName of caps) {
|
|
3987
|
+
const a = before[capName];
|
|
3988
|
+
const b = after[capName];
|
|
3989
|
+
if (a === void 0 || b === void 0) return false;
|
|
3990
|
+
if (!sliceEffectivelyEqual(a, b, policyFor(capName).volatileFields)) return false;
|
|
3991
|
+
}
|
|
3992
|
+
return true;
|
|
3993
|
+
}
|
|
3994
|
+
/**
|
|
3995
|
+
* Structural equality over the JSON values a runtime-state slice can hold.
|
|
3996
|
+
* `NaN` compares equal to itself so a sensor that reports it does not rewrite
|
|
3997
|
+
* the blob forever.
|
|
3998
|
+
*/
|
|
3999
|
+
function deepEqual(a, b) {
|
|
4000
|
+
if (a === b) return true;
|
|
4001
|
+
if (typeof a === "number" && typeof b === "number") return Number.isNaN(a) && Number.isNaN(b);
|
|
4002
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
4003
|
+
const aIsArray = Array.isArray(a);
|
|
4004
|
+
if (aIsArray !== Array.isArray(b)) return false;
|
|
4005
|
+
if (aIsArray) {
|
|
4006
|
+
const bArr = b;
|
|
4007
|
+
if (a.length !== bArr.length) return false;
|
|
4008
|
+
for (let i = 0; i < a.length; i += 1) if (!deepEqual(a[i], bArr[i])) return false;
|
|
4009
|
+
return true;
|
|
4010
|
+
}
|
|
4011
|
+
const aRec = a;
|
|
4012
|
+
const bRec = b;
|
|
4013
|
+
const aKeys = Object.keys(aRec);
|
|
4014
|
+
if (aKeys.length !== Object.keys(bRec).length) return false;
|
|
4015
|
+
for (const k of aKeys) {
|
|
4016
|
+
if (!Object.hasOwn(bRec, k)) return false;
|
|
4017
|
+
if (!deepEqual(aRec[k], bRec[k])) return false;
|
|
4018
|
+
}
|
|
4019
|
+
return true;
|
|
4020
|
+
}
|
|
4021
|
+
//#endregion
|
|
3940
4022
|
//#region src/builtins/device-manager/device-state-mirror.ts
|
|
3941
4023
|
/**
|
|
3942
4024
|
* Hub-side runtime-state mirror for the device-manager addon.
|
|
@@ -3955,6 +4037,7 @@ var DeviceMetaStore = class {
|
|
|
3955
4037
|
*/
|
|
3956
4038
|
var DeviceStateMirror = class DeviceStateMirror {
|
|
3957
4039
|
ctx;
|
|
4040
|
+
policyFor;
|
|
3958
4041
|
/**
|
|
3959
4042
|
* Hub-side mirror of every device's cap-keyed runtime state.
|
|
3960
4043
|
* Key: deviceId. Value: per-cap slice map. Empty by default —
|
|
@@ -3968,8 +4051,24 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
3968
4051
|
*/
|
|
3969
4052
|
runtimeStateDebounce = /* @__PURE__ */ new Map();
|
|
3970
4053
|
static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
|
|
3971
|
-
|
|
4054
|
+
/**
|
|
4055
|
+
* What is believed to be ON DISK for each device — the persistable projection
|
|
4056
|
+
* of the last blob actually written (or seeded at boot). The effective-change
|
|
4057
|
+
* gate compares against THIS, not against the previous mirror state: two
|
|
4058
|
+
* clock-only ticks in a row must not add up to a write just because each was
|
|
4059
|
+
* compared with its immediate predecessor.
|
|
4060
|
+
*/
|
|
4061
|
+
lastPersisted = /* @__PURE__ */ new Map();
|
|
4062
|
+
/**
|
|
4063
|
+
* Per-device count of writes the effective-change gate skipped since the last
|
|
4064
|
+
* real one. Reported on the next write that DOES happen, so the log says how
|
|
4065
|
+
* much churn the gate absorbed instead of saying nothing at all — a branch
|
|
4066
|
+
* that drops work silently reads as "never happened".
|
|
4067
|
+
*/
|
|
4068
|
+
skippedSinceWrite = /* @__PURE__ */ new Map();
|
|
4069
|
+
constructor(ctx, policyFor = runtimeStatePolicyFor) {
|
|
3972
4070
|
this.ctx = ctx;
|
|
4071
|
+
this.policyFor = policyFor;
|
|
3973
4072
|
}
|
|
3974
4073
|
/**
|
|
3975
4074
|
* Single-cap mirror update — diff against the current mirror,
|
|
@@ -3996,13 +4095,23 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
3996
4095
|
return true;
|
|
3997
4096
|
}
|
|
3998
4097
|
/**
|
|
3999
|
-
* Debounced disk writer
|
|
4000
|
-
*
|
|
4001
|
-
*
|
|
4002
|
-
*
|
|
4098
|
+
* Debounced disk writer, behind two gates.
|
|
4099
|
+
*
|
|
4100
|
+
* GATE — DURABILITY: a change to a `durability: 'session'` cap never even
|
|
4101
|
+
* arms the timer. That is where the fleet's write rate goes: `audio-metrics`
|
|
4102
|
+
* and `zone-analytics` are ~90 % of the offered 12–19 writes/s, they change
|
|
4103
|
+
* genuinely every second, and their restored value is worthless.
|
|
4104
|
+
*
|
|
4105
|
+
* GATE — EFFECTIVE CHANGE (at flush): the persistable blob is compared with
|
|
4106
|
+
* what is believed to be on disk, ignoring the clock fields each cap declared
|
|
4107
|
+
* volatile. `battery` wrote its blob 526 times in 25 minutes without one
|
|
4108
|
+
* percentage moving; this is the gate that turns those into one write.
|
|
4109
|
+
*
|
|
4110
|
+
* The blob is read from the live mirror at flush time, so the disk picture is
|
|
4003
4111
|
* always the latest state — no risk of writing a stale snapshot.
|
|
4004
4112
|
*/
|
|
4005
|
-
scheduleRuntimeStateDiskWrite(deviceId, settings) {
|
|
4113
|
+
scheduleRuntimeStateDiskWrite(deviceId, settings, changedCap) {
|
|
4114
|
+
if (this.policyFor(changedCap).durability !== "restored") return;
|
|
4006
4115
|
let slot = this.runtimeStateDebounce.get(deviceId);
|
|
4007
4116
|
if (!slot) {
|
|
4008
4117
|
slot = {
|
|
@@ -4014,10 +4123,21 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4014
4123
|
if (slot.timer) return;
|
|
4015
4124
|
slot.timer = setTimeout(() => {
|
|
4016
4125
|
slot.timer = null;
|
|
4017
|
-
const blob = this.
|
|
4126
|
+
const blob = this.persistableForDevice(deviceId);
|
|
4127
|
+
if (this.isAlreadyPersisted(deviceId, blob)) return;
|
|
4128
|
+
const skipped = this.skippedSinceWrite.get(deviceId) ?? 0;
|
|
4129
|
+
this.skippedSinceWrite.delete(deviceId);
|
|
4018
4130
|
const write = (async () => {
|
|
4019
4131
|
try {
|
|
4020
4132
|
await settings.writeDeviceRuntimeState(deviceId, blob);
|
|
4133
|
+
this.lastPersisted.set(deviceId, blob);
|
|
4134
|
+
if (skipped > 0) this.ctx.logger.debug("runtime state persisted", {
|
|
4135
|
+
tags: { deviceId },
|
|
4136
|
+
meta: {
|
|
4137
|
+
caps: Object.keys(blob).length,
|
|
4138
|
+
skippedSinceLastWrite: skipped
|
|
4139
|
+
}
|
|
4140
|
+
});
|
|
4021
4141
|
} catch (err) {
|
|
4022
4142
|
this.ctx.logger.warn("writeDeviceRuntimeState failed", {
|
|
4023
4143
|
tags: { deviceId },
|
|
@@ -4031,6 +4151,22 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4031
4151
|
}, DeviceStateMirror.RUNTIME_STATE_DEBOUNCE_MS);
|
|
4032
4152
|
}
|
|
4033
4153
|
/**
|
|
4154
|
+
* True when `blob` differs from what is believed to be on disk only in fields
|
|
4155
|
+
* the owning caps declared volatile. Counts the skip so the next real write
|
|
4156
|
+
* can report how much churn was absorbed.
|
|
4157
|
+
*/
|
|
4158
|
+
isAlreadyPersisted(deviceId, blob) {
|
|
4159
|
+
const onDisk = this.lastPersisted.get(deviceId);
|
|
4160
|
+
if (!onDisk) return false;
|
|
4161
|
+
if (!effectivelyEqual(onDisk, blob, this.policyFor)) return false;
|
|
4162
|
+
this.skippedSinceWrite.set(deviceId, (this.skippedSinceWrite.get(deviceId) ?? 0) + 1);
|
|
4163
|
+
return true;
|
|
4164
|
+
}
|
|
4165
|
+
/** The device's mirror, restricted to the slices that may reach disk. */
|
|
4166
|
+
persistableForDevice(deviceId) {
|
|
4167
|
+
return persistableBlob(this.snapshotForDevice(deviceId), this.policyFor);
|
|
4168
|
+
}
|
|
4169
|
+
/**
|
|
4034
4170
|
* One-shot mirror seed used by `loadRuntimeState` at boot so the
|
|
4035
4171
|
* hub knows about every persisted slice without waiting for the
|
|
4036
4172
|
* first `setCapSlice` call. No events emitted — this is
|
|
@@ -4050,6 +4186,7 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4050
4186
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
4051
4187
|
perCap.set(capName, { ...raw });
|
|
4052
4188
|
}
|
|
4189
|
+
this.lastPersisted.set(deviceId, persistableBlob(this.snapshotForDevice(deviceId), this.policyFor));
|
|
4053
4190
|
}
|
|
4054
4191
|
/**
|
|
4055
4192
|
* The hub mirror's `feature-probe.lastProbedAt` is a PER-SESSION liveness
|
|
@@ -4163,15 +4300,15 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
4163
4300
|
if (slot.timer) {
|
|
4164
4301
|
clearTimeout(slot.timer);
|
|
4165
4302
|
slot.timer = null;
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
}
|
|
4173
|
-
})
|
|
4174
|
-
}
|
|
4303
|
+
const blob = settings ? this.persistableForDevice(deviceId) : null;
|
|
4304
|
+
if (settings && blob !== null && !this.isAlreadyPersisted(deviceId, blob)) pending.push(settings.writeDeviceRuntimeState(deviceId, blob).then(() => {
|
|
4305
|
+
this.lastPersisted.set(deviceId, blob);
|
|
4306
|
+
}).catch((err) => {
|
|
4307
|
+
this.ctx.logger.warn("shutdown writeDeviceRuntimeState failed", {
|
|
4308
|
+
tags: { deviceId },
|
|
4309
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
4310
|
+
});
|
|
4311
|
+
}));
|
|
4175
4312
|
}
|
|
4176
4313
|
if (slot.inFlight) pending.push(slot.inFlight);
|
|
4177
4314
|
}
|
|
@@ -4637,7 +4774,7 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
4637
4774
|
setCapSlice: async (input) => {
|
|
4638
4775
|
const { deviceId, capName, slice } = input;
|
|
4639
4776
|
if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] setCapSlice: unknown device id=${deviceId}`);
|
|
4640
|
-
if (stateMirror.applySingleCapUpdate(deviceId, capName, slice)) stateMirror.scheduleRuntimeStateDiskWrite(deviceId, settings);
|
|
4777
|
+
if (stateMirror.applySingleCapUpdate(deviceId, capName, slice)) stateMirror.scheduleRuntimeStateDiskWrite(deviceId, settings, capName);
|
|
4641
4778
|
}
|
|
4642
4779
|
}
|
|
4643
4780
|
}];
|