@camstack/system 1.2.316 → 1.2.318
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/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/auth/auth-manager.d.ts +11 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +347 -181
- package/dist/builtins/composer/composer.addon.mjs +347 -181
- package/dist/builtins/composer/composition-evaluation.d.ts +37 -0
- package/dist/builtins/composer/composition-runtime.d.ts +11 -52
- package/dist/builtins/composer/composition-target.d.ts +50 -0
- package/dist/builtins/composer/existing-target.d.ts +19 -1
- package/dist/builtins/composer/own-field-reads.d.ts +19 -0
- 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 +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +265 -29
- package/dist/builtins/device-manager/device-manager.addon.mjs +265 -29
- package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
- package/dist/builtins/device-manager/device-source-activation.d.ts +26 -1
- package/dist/builtins/device-manager/device-state-mirror.d.ts +41 -9
- package/dist/builtins/device-manager/mirror-row-writer.d.ts +59 -1
- package/dist/builtins/doorbell/binding-mirror.d.ts +45 -81
- package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +4 -3
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +53 -11
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -82
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -82
- 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 +47 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +46 -4
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +4 -4
- package/dist/builtins/system-config/system-config.addon.mjs +4 -4
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{child-cap-dispatch-AAltfBUH.mjs → child-cap-dispatch-C2VINHu1.mjs} +70 -18
- package/dist/{child-cap-dispatch-DzcUd_ct.js → child-cap-dispatch-C8uts1P4.js} +70 -18
- package/dist/{composition-sources-BwE1POnF.js → composition-sources-BjRPhWtU.js} +1 -1
- package/dist/{composition-sources-fUyF4z39.mjs → composition-sources-Cg2MP-qf.mjs} +1 -1
- package/dist/{dist-BPvku43i.mjs → dist-CQ9vXSUh.mjs} +220 -8
- package/dist/{dist-RAl10EfG.js → dist-Dcy0gVHu.js} +220 -8
- package/dist/index.js +739 -350
- package/dist/index.mjs +737 -352
- package/dist/kernel/bootstrap-config.d.ts +8 -0
- package/dist/kernel/capability-registry.d.ts +30 -0
- package/dist/kernel/config-manager.d.ts +75 -129
- package/dist/kernel/index.d.ts +5 -2
- package/dist/kernel/moleculer/runtime-state-load.d.ts +19 -0
- package/dist/kernel/settings-door-view.d.ts +24 -1
- package/dist/kernel/settings-read-error.d.ts +37 -0
- package/dist/kernel/settings-view-types.d.ts +105 -0
- package/dist/kernel/store-settings-view.d.ts +17 -0
- package/dist/kernel/system-settings-mirror.d.ts +34 -0
- package/dist/kernel/system-settings-policy.d.ts +47 -0
- package/dist/{retired-settings-keys-DqVWSrVw.mjs → retired-settings-keys-DNIbyfTR.mjs} +1 -1
- package/dist/{retired-settings-keys-D3rGTGq5.js → retired-settings-keys-pzlrLUbA.js} +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,8 @@ 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_retired_settings_keys = require("../../retired-settings-keys-
|
|
6
|
+
const require_dist = require("../../dist-Dcy0gVHu.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-pzlrLUbA.js");
|
|
8
8
|
let zod = require("zod");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
10
10
|
let _camstack_types_node = require("@camstack/types/node");
|
|
@@ -4141,6 +4141,73 @@ async function setWrapperActive(deps, input) {
|
|
|
4141
4141
|
}
|
|
4142
4142
|
});
|
|
4143
4143
|
}
|
|
4144
|
+
function isRecord(value) {
|
|
4145
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4146
|
+
}
|
|
4147
|
+
/** The key of this addon's store that holds every wrapper activation. */
|
|
4148
|
+
var DEVICE_BINDINGS_KEY = "deviceBindings";
|
|
4149
|
+
/**
|
|
4150
|
+
* The STORED activation for one `(device, capName)` pair, three-way — the
|
|
4151
|
+
* provider behind `deviceManager.getWrapperActivation` (D675).
|
|
4152
|
+
*
|
|
4153
|
+
* Reads ONE row — this addon's `deviceBindings` key (~1.6 KB live) — through
|
|
4154
|
+
* the settings view's single-key three-way read, never the addon's whole store
|
|
4155
|
+
* (~625 KB) and never `readAddonStore()`, which folds a failed read into `{}`.
|
|
4156
|
+
*
|
|
4157
|
+
* Absence is graded by LEVEL:
|
|
4158
|
+
* - STORE level — the read failed, or the `deviceBindings` row does not exist,
|
|
4159
|
+
* or it is not a map — is `failed`. Every hub that has ever bound anything
|
|
4160
|
+
* holds that row, so its absence is the signature of a store answering from
|
|
4161
|
+
* an empty or unready snapshot (D44), and it must never silence a press
|
|
4162
|
+
* (D49). An install that has never bound anything lands here too; erring
|
|
4163
|
+
* `failed` there costs nothing, because nothing is bound to silence.
|
|
4164
|
+
* - PAIR level — the row was read and has no entry for this device, or none
|
|
4165
|
+
* for this cap, or none with `wrapperAddonId` — is `missing`: the operator
|
|
4166
|
+
* never touched this pair.
|
|
4167
|
+
*/
|
|
4168
|
+
async function readWrapperActivation(deps, query) {
|
|
4169
|
+
const settings = deps.ctx.settings;
|
|
4170
|
+
if (settings?.readAddonStoreKeyResult === void 0) return {
|
|
4171
|
+
kind: "failed",
|
|
4172
|
+
error: "this settings view has no single-key three-way read"
|
|
4173
|
+
};
|
|
4174
|
+
const read = await settings.readAddonStoreKeyResult(DEVICE_BINDINGS_KEY);
|
|
4175
|
+
if (read.kind === "unreadable") return {
|
|
4176
|
+
kind: "failed",
|
|
4177
|
+
error: read.error ?? read.reason
|
|
4178
|
+
};
|
|
4179
|
+
if (read.kind === "absent") return {
|
|
4180
|
+
kind: "failed",
|
|
4181
|
+
error: "the store holds no deviceBindings row — an empty snapshot is not evidence of an unbind"
|
|
4182
|
+
};
|
|
4183
|
+
const deviceBindings = read.value;
|
|
4184
|
+
if (!isRecord(deviceBindings)) return {
|
|
4185
|
+
kind: "failed",
|
|
4186
|
+
error: "stored deviceBindings is not a map"
|
|
4187
|
+
};
|
|
4188
|
+
const perDevice = deviceBindings[String(query.deviceId)];
|
|
4189
|
+
if (perDevice === void 0) return { kind: "missing" };
|
|
4190
|
+
if (!isRecord(perDevice)) return {
|
|
4191
|
+
kind: "failed",
|
|
4192
|
+
error: `stored bindings of device ${query.deviceId} are not a map`
|
|
4193
|
+
};
|
|
4194
|
+
const activation = perDevice[query.capName];
|
|
4195
|
+
if (activation === void 0) return { kind: "missing" };
|
|
4196
|
+
if (!isRecord(activation)) return {
|
|
4197
|
+
kind: "failed",
|
|
4198
|
+
error: `stored "${query.capName}" activation is not a record`
|
|
4199
|
+
};
|
|
4200
|
+
const wrapperAddonId = activation["wrapperAddonId"];
|
|
4201
|
+
if (wrapperAddonId === void 0) return { kind: "missing" };
|
|
4202
|
+
if (wrapperAddonId === null || typeof wrapperAddonId === "string") return {
|
|
4203
|
+
kind: "value",
|
|
4204
|
+
wrapperAddonId
|
|
4205
|
+
};
|
|
4206
|
+
return {
|
|
4207
|
+
kind: "failed",
|
|
4208
|
+
error: `stored "${query.capName}" wrapperAddonId is not a string`
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4144
4211
|
//#endregion
|
|
4145
4212
|
//#region src/builtins/device-manager/device-event-propagator.ts
|
|
4146
4213
|
/**
|
|
@@ -5545,6 +5612,7 @@ async function removeDevice(pctx, input) {
|
|
|
5545
5612
|
});
|
|
5546
5613
|
});
|
|
5547
5614
|
await pctx.settings.clearDeviceStore(deviceId);
|
|
5615
|
+
await pctx.host.forgetMirrorDevice(deviceId);
|
|
5548
5616
|
await pctx.settings.clearDeviceRuntimeState(deviceId);
|
|
5549
5617
|
await pctx.host.fieldClaims.pruneDevice(deviceId, pctx.settings);
|
|
5550
5618
|
const bindingKey = String(deviceId);
|
|
@@ -8066,6 +8134,20 @@ function deepEqual(a, b) {
|
|
|
8066
8134
|
}
|
|
8067
8135
|
//#endregion
|
|
8068
8136
|
//#region src/builtins/device-manager/mirror-row-writer.ts
|
|
8137
|
+
/**
|
|
8138
|
+
* A write-through refused because the device's row could not be read first
|
|
8139
|
+
* (D677). Nothing was written; the caller rolls its memory change back.
|
|
8140
|
+
*/
|
|
8141
|
+
var RuntimeStateRowUnreadError = class extends Error {
|
|
8142
|
+
deviceId;
|
|
8143
|
+
constructor(deviceId) {
|
|
8144
|
+
super(`runtime-state row of device ${deviceId} could not be read — nothing written, a row this process has not read is never replaced (D677)`);
|
|
8145
|
+
this.deviceId = deviceId;
|
|
8146
|
+
this.name = "RuntimeStateRowUnreadError";
|
|
8147
|
+
}
|
|
8148
|
+
};
|
|
8149
|
+
var ROW_UNREAD_REFUSAL_MESSAGE = "runtime-state row write refused — the row could not be read first, nothing written (D677)";
|
|
8150
|
+
var ROW_DROPS_CAPS_MESSAGE = "runtime-state row write DROPS caps the stored row holds (D677)";
|
|
8069
8151
|
function errorMessage$2(err) {
|
|
8070
8152
|
return err instanceof Error ? err.message : String(err);
|
|
8071
8153
|
}
|
|
@@ -8085,6 +8167,13 @@ var RuntimeStateDrainError = class extends MigrationRefusedError {
|
|
|
8085
8167
|
var MirrorRowWriter = class MirrorRowWriter {
|
|
8086
8168
|
deps;
|
|
8087
8169
|
static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
|
|
8170
|
+
/** Ceiling of the re-arm backoff while a device's row cannot be read (D677, D3). */
|
|
8171
|
+
static UNREAD_REARM_MAX_MS = 6e4;
|
|
8172
|
+
/** The re-arm delay after `refusals` consecutive unread refusals: 1 s, 2 s, 4 s … capped at 60 s. */
|
|
8173
|
+
static unreadRearmDelayMs(refusals) {
|
|
8174
|
+
const exponent = Math.max(0, refusals - 1);
|
|
8175
|
+
return Math.min(MirrorRowWriter.RUNTIME_STATE_DEBOUNCE_MS * 2 ** exponent, MirrorRowWriter.UNREAD_REARM_MAX_MS);
|
|
8176
|
+
}
|
|
8088
8177
|
/**
|
|
8089
8178
|
* Per-device disk-write debouncer for runtime-state. `setCapSlice`
|
|
8090
8179
|
* updates the in-memory mirror synchronously and emits the change
|
|
@@ -8113,6 +8202,8 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8113
8202
|
* that drops work silently reads as "never happened".
|
|
8114
8203
|
*/
|
|
8115
8204
|
skippedSinceWrite = /* @__PURE__ */ new Map();
|
|
8205
|
+
/** Devices whose unreadable-row refusal is already logged — once per episode. */
|
|
8206
|
+
rowUnreadLogged = /* @__PURE__ */ new Set();
|
|
8116
8207
|
constructor(deps) {
|
|
8117
8208
|
this.deps = deps;
|
|
8118
8209
|
}
|
|
@@ -8149,7 +8240,8 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8149
8240
|
slot = {
|
|
8150
8241
|
timer: null,
|
|
8151
8242
|
inFlight: null,
|
|
8152
|
-
lastWriteFailed: false
|
|
8243
|
+
lastWriteFailed: false,
|
|
8244
|
+
unreadRefusals: 0
|
|
8153
8245
|
};
|
|
8154
8246
|
this.runtimeStateDebounce.set(deviceId, slot);
|
|
8155
8247
|
}
|
|
@@ -8184,23 +8276,116 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8184
8276
|
this.armOrHold(deviceId, settings);
|
|
8185
8277
|
}
|
|
8186
8278
|
/** Arm the debounce timer; at fire, queue a write of the live blob unless disk already has it. */
|
|
8187
|
-
armDiskWrite(deviceId, settings) {
|
|
8279
|
+
armDiskWrite(deviceId, settings, delayMs = MirrorRowWriter.RUNTIME_STATE_DEBOUNCE_MS) {
|
|
8188
8280
|
const slot = this.slotFor(deviceId);
|
|
8189
8281
|
if (slot.timer) return;
|
|
8190
8282
|
slot.timer = setTimeout(() => {
|
|
8191
8283
|
slot.timer = null;
|
|
8192
|
-
this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot));
|
|
8193
|
-
},
|
|
8284
|
+
this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot, "debounced"));
|
|
8285
|
+
}, delayMs);
|
|
8286
|
+
}
|
|
8287
|
+
/**
|
|
8288
|
+
* Forget a removed device: cancel its armed write, await one in flight, and
|
|
8289
|
+
* drop its baseline and counters — nothing writes its row after this.
|
|
8290
|
+
*/
|
|
8291
|
+
async forget(deviceId) {
|
|
8292
|
+
const slot = this.runtimeStateDebounce.get(deviceId);
|
|
8293
|
+
if (slot?.timer) {
|
|
8294
|
+
clearTimeout(slot.timer);
|
|
8295
|
+
slot.timer = null;
|
|
8296
|
+
}
|
|
8297
|
+
if (slot?.inFlight) await slot.inFlight;
|
|
8298
|
+
this.runtimeStateDebounce.delete(deviceId);
|
|
8299
|
+
this.lastPersisted.delete(deviceId);
|
|
8300
|
+
this.skippedSinceWrite.delete(deviceId);
|
|
8301
|
+
this.rowUnreadLogged.delete(deviceId);
|
|
8302
|
+
this.heldDirty.delete(deviceId);
|
|
8303
|
+
}
|
|
8304
|
+
/**
|
|
8305
|
+
* The row is in memory, or is read now (D677). False when the read failed:
|
|
8306
|
+
* the caller writes nothing. Logged once per episode, tagged; the episode
|
|
8307
|
+
* ends at the next successful read.
|
|
8308
|
+
*/
|
|
8309
|
+
async rowReadOrRefuse(deviceId, settings, path) {
|
|
8310
|
+
if (!this.deps.isRowRead(deviceId)) try {
|
|
8311
|
+
await this.deps.readRowFirst(deviceId, settings);
|
|
8312
|
+
} catch (err) {
|
|
8313
|
+
if (!this.rowUnreadLogged.has(deviceId)) {
|
|
8314
|
+
this.rowUnreadLogged.add(deviceId);
|
|
8315
|
+
this.deps.logger.warn(ROW_UNREAD_REFUSAL_MESSAGE, {
|
|
8316
|
+
tags: { deviceId },
|
|
8317
|
+
meta: {
|
|
8318
|
+
path,
|
|
8319
|
+
error: errorMessage$2(err)
|
|
8320
|
+
}
|
|
8321
|
+
});
|
|
8322
|
+
}
|
|
8323
|
+
return false;
|
|
8324
|
+
}
|
|
8325
|
+
this.rowUnreadLogged.delete(deviceId);
|
|
8326
|
+
return true;
|
|
8194
8327
|
}
|
|
8195
|
-
/**
|
|
8196
|
-
|
|
8328
|
+
/**
|
|
8329
|
+
* The ONE call of `writeDeviceRuntimeState`. A blob that lacks a cap the
|
|
8330
|
+
* stored row holds is written (memory is the authority once the row was
|
|
8331
|
+
* read) but never silently: the dropped caps are named at warn (D677).
|
|
8332
|
+
* `$owned` is excluded — a release that empties it is the claim path's own
|
|
8333
|
+
* logged business.
|
|
8334
|
+
*/
|
|
8335
|
+
async writeRow(deviceId, settings, blob, path) {
|
|
8336
|
+
const onDisk = this.lastPersisted.get(deviceId);
|
|
8337
|
+
if (onDisk) {
|
|
8338
|
+
const dropped = Object.keys(onDisk).filter((cap) => cap !== "$owned" && !Object.hasOwn(blob, cap));
|
|
8339
|
+
if (dropped.length > 0) this.deps.logger.warn(ROW_DROPS_CAPS_MESSAGE, {
|
|
8340
|
+
tags: { deviceId },
|
|
8341
|
+
meta: {
|
|
8342
|
+
path,
|
|
8343
|
+
dropped,
|
|
8344
|
+
storedCaps: Object.keys(onDisk).length,
|
|
8345
|
+
writtenCaps: Object.keys(blob).length
|
|
8346
|
+
}
|
|
8347
|
+
});
|
|
8348
|
+
}
|
|
8349
|
+
await settings.writeDeviceRuntimeState(deviceId, blob);
|
|
8350
|
+
this.lastPersisted.set(deviceId, blob);
|
|
8351
|
+
}
|
|
8352
|
+
/**
|
|
8353
|
+
* A claim's write-through (D663): queued behind the device's writes, the row
|
|
8354
|
+
* read first, the blob taken when it runs. Rejects with
|
|
8355
|
+
* {@link RuntimeStateRowUnreadError} when the row cannot be read, or with the
|
|
8356
|
+
* write's own error; resolves to what landed.
|
|
8357
|
+
*/
|
|
8358
|
+
writeThroughRow(deviceId, settings, path) {
|
|
8359
|
+
return this.serialiseWrite(deviceId, async () => {
|
|
8360
|
+
if (!await this.rowReadOrRefuse(deviceId, settings, path)) throw new RuntimeStateRowUnreadError(deviceId);
|
|
8361
|
+
const live = this.deps.persistableFor(deviceId);
|
|
8362
|
+
await this.writeRow(deviceId, settings, live, path);
|
|
8363
|
+
return live;
|
|
8364
|
+
});
|
|
8365
|
+
}
|
|
8366
|
+
/**
|
|
8367
|
+
* The debounced write itself: the row read first (D677), then the blob as
|
|
8368
|
+
* memory holds it NOW, behind the effective-change gate. Never rejects. An
|
|
8369
|
+
* unreadable row writes nothing; the debounced path re-arms with a backoff
|
|
8370
|
+
* (1 s doubling to 60 s — never a 1 Hz loop, D3), and the armed timer is
|
|
8371
|
+
* what a migration drain sees as owed, so a row that has become readable
|
|
8372
|
+
* drains normally. The shutdown flush does not re-arm.
|
|
8373
|
+
*/
|
|
8374
|
+
async writeLiveBlob(deviceId, settings, slot, path) {
|
|
8375
|
+
if (!await this.rowReadOrRefuse(deviceId, settings, path)) {
|
|
8376
|
+
if (path === "debounced") {
|
|
8377
|
+
slot.unreadRefusals += 1;
|
|
8378
|
+
this.rearmAfterUnread(deviceId, settings, slot);
|
|
8379
|
+
}
|
|
8380
|
+
return;
|
|
8381
|
+
}
|
|
8382
|
+
slot.unreadRefusals = 0;
|
|
8197
8383
|
const blob = this.deps.persistableFor(deviceId);
|
|
8198
8384
|
if (this.isAlreadyPersisted(deviceId, blob)) return;
|
|
8199
8385
|
const skipped = this.skippedSinceWrite.get(deviceId) ?? 0;
|
|
8200
8386
|
this.skippedSinceWrite.delete(deviceId);
|
|
8201
8387
|
try {
|
|
8202
|
-
await
|
|
8203
|
-
this.lastPersisted.set(deviceId, blob);
|
|
8388
|
+
await this.writeRow(deviceId, settings, blob, path);
|
|
8204
8389
|
slot.lastWriteFailed = false;
|
|
8205
8390
|
if (skipped > 0) this.deps.logger.debug("runtime state persisted", {
|
|
8206
8391
|
tags: { deviceId },
|
|
@@ -8217,6 +8402,14 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8217
8402
|
});
|
|
8218
8403
|
}
|
|
8219
8404
|
}
|
|
8405
|
+
/** Re-arm an unread refusal with its backoff, or remember it for `release` while held. */
|
|
8406
|
+
rearmAfterUnread(deviceId, settings, slot) {
|
|
8407
|
+
if (this.writesHeld.has(deviceId)) {
|
|
8408
|
+
this.heldDirty.add(deviceId);
|
|
8409
|
+
return;
|
|
8410
|
+
}
|
|
8411
|
+
this.armDiskWrite(deviceId, settings, MirrorRowWriter.unreadRearmDelayMs(slot.unreadRefusals));
|
|
8412
|
+
}
|
|
8220
8413
|
/**
|
|
8221
8414
|
* Hold the debounced writer off these devices' rows while a migration swaps
|
|
8222
8415
|
* them (D663 re-review N-6). Resolves once each device is DRAINED: a write
|
|
@@ -8270,6 +8463,7 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8270
8463
|
if (slot?.inFlight) await slot.inFlight;
|
|
8271
8464
|
const lastFailed = slot?.lastWriteFailed ?? false;
|
|
8272
8465
|
if (!pending && !lastFailed) return;
|
|
8466
|
+
if (!await this.rowReadOrRefuse(deviceId, settings, "drain")) this.refuseDrain(deviceId, "its runtime-state row could not be read first", null);
|
|
8273
8467
|
const blob = this.deps.persistableFor(deviceId);
|
|
8274
8468
|
if (this.isAlreadyPersisted(deviceId, blob)) {
|
|
8275
8469
|
if (slot) slot.lastWriteFailed = false;
|
|
@@ -8277,11 +8471,10 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8277
8471
|
}
|
|
8278
8472
|
if (lastFailed) this.refuseDrain(deviceId, "its last runtime-state write did not land", null);
|
|
8279
8473
|
try {
|
|
8280
|
-
await this.serialiseWrite(deviceId, () =>
|
|
8474
|
+
await this.serialiseWrite(deviceId, () => this.writeRow(deviceId, settings, blob, "drain"));
|
|
8281
8475
|
} catch (err) {
|
|
8282
8476
|
this.refuseDrain(deviceId, `the pending runtime-state write failed: ${errorMessage$2(err)}`, err);
|
|
8283
8477
|
}
|
|
8284
|
-
this.lastPersisted.set(deviceId, blob);
|
|
8285
8478
|
}
|
|
8286
8479
|
/** A drain that cannot vouch for the row: still owed (re-armed on release), logged, refused. */
|
|
8287
8480
|
refuseDrain(deviceId, reason, cause) {
|
|
@@ -8314,7 +8507,7 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8314
8507
|
clearTimeout(slot.timer);
|
|
8315
8508
|
slot.timer = null;
|
|
8316
8509
|
if (settings) {
|
|
8317
|
-
pending.push(this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot)));
|
|
8510
|
+
pending.push(this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot, "shutdown-flush")));
|
|
8318
8511
|
continue;
|
|
8319
8512
|
}
|
|
8320
8513
|
}
|
|
@@ -8371,9 +8564,18 @@ var DeviceStateMirror = class {
|
|
|
8371
8564
|
stateMirror = /* @__PURE__ */ new Map();
|
|
8372
8565
|
/** The claims and the merged view of every claimed cap (D663). */
|
|
8373
8566
|
claims;
|
|
8374
|
-
/**
|
|
8567
|
+
/**
|
|
8568
|
+
* Devices whose CLAIMS are known: the row was read, or a loaded claims index
|
|
8569
|
+
* vouched that it names none (D447). This is a READ-path fact — what a
|
|
8570
|
+
* status read may answer from. It never licenses a row write (D677).
|
|
8571
|
+
*/
|
|
8375
8572
|
seeded = /* @__PURE__ */ new Set();
|
|
8376
|
-
/**
|
|
8573
|
+
/**
|
|
8574
|
+
* Devices whose row was actually READ into memory in THIS process. Every row
|
|
8575
|
+
* write of a device requires it (D677): the write replaces the whole row, so
|
|
8576
|
+
* memory must hold what the row held — an index that vouches for the claims
|
|
8577
|
+
* says nothing about the native slices beside them.
|
|
8578
|
+
*/
|
|
8377
8579
|
rowRead = /* @__PURE__ */ new Set();
|
|
8378
8580
|
/** One row read per device, however many writes arrive during it. */
|
|
8379
8581
|
seeding = /* @__PURE__ */ new Map();
|
|
@@ -8392,7 +8594,9 @@ var DeviceStateMirror = class {
|
|
|
8392
8594
|
this.writer = new MirrorRowWriter({
|
|
8393
8595
|
logger: ctx.logger,
|
|
8394
8596
|
policyFor,
|
|
8395
|
-
persistableFor: (deviceId) => this.persistableForDevice(deviceId)
|
|
8597
|
+
persistableFor: (deviceId) => this.persistableForDevice(deviceId),
|
|
8598
|
+
isRowRead: (deviceId) => this.rowRead.has(deviceId),
|
|
8599
|
+
readRowFirst: (deviceId, settings) => this.ensureRowRead(deviceId, settings)
|
|
8396
8600
|
});
|
|
8397
8601
|
}
|
|
8398
8602
|
/**
|
|
@@ -8592,8 +8796,28 @@ var DeviceStateMirror = class {
|
|
|
8592
8796
|
return this.seeded.has(deviceId);
|
|
8593
8797
|
}
|
|
8594
8798
|
/**
|
|
8799
|
+
* Forget a removed device (D677): its armed row write is cancelled and one
|
|
8800
|
+
* in flight awaited BEFORE its memory goes, so nothing writes its row back
|
|
8801
|
+
* after `removeDevice` clears it.
|
|
8802
|
+
*/
|
|
8803
|
+
async forgetDevice(deviceId) {
|
|
8804
|
+
await this.seeding.get(deviceId)?.catch(() => void 0);
|
|
8805
|
+
await this.writer.forget(deviceId);
|
|
8806
|
+
this.stateMirror.delete(deviceId);
|
|
8807
|
+
this.claims.clearDevice(deviceId);
|
|
8808
|
+
this.seeded.delete(deviceId);
|
|
8809
|
+
this.rowRead.delete(deviceId);
|
|
8810
|
+
this.seedRefusalLogged.delete(deviceId);
|
|
8811
|
+
}
|
|
8812
|
+
/** The device's row was read in this process — the precondition of every row write (D677). */
|
|
8813
|
+
isRowRead(deviceId) {
|
|
8814
|
+
return this.rowRead.has(deviceId);
|
|
8815
|
+
}
|
|
8816
|
+
/**
|
|
8595
8817
|
* The SYNCHRONOUS half of {@link ensureSeeded}, branch (c): a LOADED claims
|
|
8596
|
-
* index that does not name the device vouches for
|
|
8818
|
+
* index that does not name the device vouches for its CLAIMS — seeded, no
|
|
8819
|
+
* read. That is all it vouches for: the device is still not `rowRead`, and
|
|
8820
|
+
* the writer reads the row before its first write (D677).
|
|
8597
8821
|
* Refused (false) while a read is in flight, because that read may be
|
|
8598
8822
|
* landing a claim the index does not know yet; refused when the index cannot
|
|
8599
8823
|
* vouch. The overlay's `peek` calls this instead of `ensureSeeded`: a sync
|
|
@@ -8608,14 +8832,21 @@ var DeviceStateMirror = class {
|
|
|
8608
8832
|
return true;
|
|
8609
8833
|
}
|
|
8610
8834
|
/**
|
|
8611
|
-
* Make sure the device's
|
|
8612
|
-
* (D49). Index first, single-flight (ruling S6):
|
|
8835
|
+
* Make sure the device's CLAIMS are known before a status read is answered
|
|
8836
|
+
* or a native slice applied (D49). Index first, single-flight (ruling S6):
|
|
8613
8837
|
* (a) already seeded → nothing;
|
|
8614
8838
|
* (b) a seed in flight → await it;
|
|
8615
8839
|
* (c) a LOADED index that does not name the device → seeded, no read;
|
|
8616
8840
|
* (d) otherwise read the row once and seed it.
|
|
8617
|
-
* A read that throws propagates
|
|
8618
|
-
*
|
|
8841
|
+
* A read that throws propagates and is logged once per device transition
|
|
8842
|
+
* (D391).
|
|
8843
|
+
*
|
|
8844
|
+
* Also the native-write RPC's gate: a slice may be APPLIED once its claims
|
|
8845
|
+
* are known. Never the ROW write's: branch (c) knows the claims and nothing
|
|
8846
|
+
* about the native slices on the row; a writer that trusted it replaced a
|
|
8847
|
+
* row it never read with the one slice it held, and every restored cap
|
|
8848
|
+
* nobody re-wrote soon after boot was erased (D677, superseding S6(c) for
|
|
8849
|
+
* writes). The writer calls {@link ensureRowRead}.
|
|
8619
8850
|
*/
|
|
8620
8851
|
async ensureSeeded(deviceId, settings, index) {
|
|
8621
8852
|
if (this.seeded.has(deviceId)) return;
|
|
@@ -8624,7 +8855,13 @@ var DeviceStateMirror = class {
|
|
|
8624
8855
|
if (this.seedIfIndexClears(deviceId, index)) return;
|
|
8625
8856
|
return this.readRowOnce(deviceId, settings);
|
|
8626
8857
|
}
|
|
8627
|
-
/**
|
|
8858
|
+
/**
|
|
8859
|
+
* A row write replaces the whole row: memory must hold what the row held
|
|
8860
|
+
* (D677). Reads the row once, single-flight, and fills memory's gaps from it
|
|
8861
|
+
* (`seedMirror('fill')`: a slice memory already holds is newer and wins).
|
|
8862
|
+
* A read that throws propagates — nothing is written — and is logged once
|
|
8863
|
+
* per device transition (D49, D391).
|
|
8864
|
+
*/
|
|
8628
8865
|
async ensureRowRead(deviceId, settings) {
|
|
8629
8866
|
if (this.rowRead.has(deviceId)) return;
|
|
8630
8867
|
const inFlight = this.seeding.get(deviceId);
|
|
@@ -8678,13 +8915,8 @@ var DeviceStateMirror = class {
|
|
|
8678
8915
|
* `changed: false`, and consumers are handed the truth that stands.
|
|
8679
8916
|
*/
|
|
8680
8917
|
async writeThrough(deviceId, settings, op, rollback) {
|
|
8681
|
-
let blob;
|
|
8682
8918
|
try {
|
|
8683
|
-
|
|
8684
|
-
const live = this.persistableForDevice(deviceId);
|
|
8685
|
-
await settings.writeDeviceRuntimeState(deviceId, live);
|
|
8686
|
-
return live;
|
|
8687
|
-
});
|
|
8919
|
+
await this.writer.writeThroughRow(deviceId, settings, op.op);
|
|
8688
8920
|
} catch (err) {
|
|
8689
8921
|
const extras = {
|
|
8690
8922
|
tags: { deviceId },
|
|
@@ -8703,7 +8935,6 @@ var DeviceStateMirror = class {
|
|
|
8703
8935
|
}
|
|
8704
8936
|
throw err;
|
|
8705
8937
|
}
|
|
8706
|
-
this.writer.markPersisted(deviceId, blob);
|
|
8707
8938
|
}
|
|
8708
8939
|
/** After a rollback, re-emit what is true now: the restored claim's merge, or the native. */
|
|
8709
8940
|
reemitAfterRollback(deviceId, capName) {
|
|
@@ -9331,6 +9562,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
9331
9562
|
},
|
|
9332
9563
|
holdMirrorWrites: (deviceIds, settings) => this.stateMirror.holdDiskWrites(deviceIds, settings),
|
|
9333
9564
|
mirrorOwnedCaps: (deviceId) => this.stateMirror.ownedCaps(deviceId),
|
|
9565
|
+
forgetMirrorDevice: (deviceId) => this.stateMirror.forgetDevice(deviceId),
|
|
9334
9566
|
resolveDeviceOnline: (deviceId, fallbackOnline) => this.stateMirror.resolveDeviceOnline(deviceId, fallbackOnline),
|
|
9335
9567
|
resolveDeviceProbed: (deviceId) => this.stateMirror.resolveDeviceProbed(deviceId),
|
|
9336
9568
|
waitDeviceProvider: (addonId, timeoutMs) => this.waitDeviceProvider(addonId, timeoutMs),
|
|
@@ -9673,6 +9905,10 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
9673
9905
|
active: input.active
|
|
9674
9906
|
});
|
|
9675
9907
|
},
|
|
9908
|
+
getWrapperActivation: (input) => readWrapperActivation(this.wrapperActivationDeps, {
|
|
9909
|
+
deviceId: input.deviceId,
|
|
9910
|
+
capName: input.capName
|
|
9911
|
+
}),
|
|
9676
9912
|
listWrappersForCap: async (input) => this.listWrappersForCap(input),
|
|
9677
9913
|
listBindableCapsForDeviceType: async (input) => this.listBindableCapsForDeviceType(input),
|
|
9678
9914
|
getDeviceSettingsAggregate: async (input) => {
|