@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Gn as BaseAddon, Hn as DeviceRole, Kt as normalizeUnit, Lt as locationIconKey, Nt as legacyAutoLinkIds, S as LocationIconIdSchema, Un as DeviceType, Vn as DeviceFeature, Wn as errMsg, Yt as parseStreamParamsFormPatch, Zn as WELL_KNOWN_TAB_MAP, _t as enumerateSchemaFields, an as runtimeStatePolicyFor, bt as expandContainer, ct as defineExtensionSlotProvider, dt as deviceManagerCapability, ft as deviceStateCapability, g as DeviceStatusSchema, gt as enumerateItemArrayFields, hr as EventCategory, it as createDeviceExtensionProvider, j as STREAM_PROFILE_META, mr as sleep$1, o as CAMERA_SWITCH_ORDER, or as isDeviceConfigCap, p as DAY_NAMES, pt as deviceStatusCapability, q as buildStreamParamsConfigSchema, s as CAP_NAMES_WITH_STATUS, t as ALL_CAPABILITY_DEFINITIONS, un as siteZoneKey, ut as deviceExtensionCapability, v as FieldClaimModeSchema, w as MINUTES_PER_DAY, wt as formatMinutes, y as FieldClaimSchema } from "../../dist-
|
|
2
|
-
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-
|
|
1
|
+
import { Gn as BaseAddon, Hn as DeviceRole, Kt as normalizeUnit, Lt as locationIconKey, Nt as legacyAutoLinkIds, S as LocationIconIdSchema, Un as DeviceType, Vn as DeviceFeature, Wn as errMsg, Yt as parseStreamParamsFormPatch, Zn as WELL_KNOWN_TAB_MAP, _t as enumerateSchemaFields, an as runtimeStatePolicyFor, bt as expandContainer, ct as defineExtensionSlotProvider, dt as deviceManagerCapability, ft as deviceStateCapability, g as DeviceStatusSchema, gt as enumerateItemArrayFields, hr as EventCategory, it as createDeviceExtensionProvider, j as STREAM_PROFILE_META, mr as sleep$1, o as CAMERA_SWITCH_ORDER, or as isDeviceConfigCap, p as DAY_NAMES, pt as deviceStatusCapability, q as buildStreamParamsConfigSchema, s as CAP_NAMES_WITH_STATUS, t as ALL_CAPABILITY_DEFINITIONS, un as siteZoneKey, ut as deviceExtensionCapability, v as FieldClaimModeSchema, w as MINUTES_PER_DAY, wt as formatMinutes, y as FieldClaimSchema } from "../../dist-CQ9vXSUh.mjs";
|
|
2
|
+
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-DNIbyfTR.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
@@ -4136,6 +4136,73 @@ async function setWrapperActive(deps, input) {
|
|
|
4136
4136
|
}
|
|
4137
4137
|
});
|
|
4138
4138
|
}
|
|
4139
|
+
function isRecord(value) {
|
|
4140
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4141
|
+
}
|
|
4142
|
+
/** The key of this addon's store that holds every wrapper activation. */
|
|
4143
|
+
var DEVICE_BINDINGS_KEY = "deviceBindings";
|
|
4144
|
+
/**
|
|
4145
|
+
* The STORED activation for one `(device, capName)` pair, three-way — the
|
|
4146
|
+
* provider behind `deviceManager.getWrapperActivation` (D675).
|
|
4147
|
+
*
|
|
4148
|
+
* Reads ONE row — this addon's `deviceBindings` key (~1.6 KB live) — through
|
|
4149
|
+
* the settings view's single-key three-way read, never the addon's whole store
|
|
4150
|
+
* (~625 KB) and never `readAddonStore()`, which folds a failed read into `{}`.
|
|
4151
|
+
*
|
|
4152
|
+
* Absence is graded by LEVEL:
|
|
4153
|
+
* - STORE level — the read failed, or the `deviceBindings` row does not exist,
|
|
4154
|
+
* or it is not a map — is `failed`. Every hub that has ever bound anything
|
|
4155
|
+
* holds that row, so its absence is the signature of a store answering from
|
|
4156
|
+
* an empty or unready snapshot (D44), and it must never silence a press
|
|
4157
|
+
* (D49). An install that has never bound anything lands here too; erring
|
|
4158
|
+
* `failed` there costs nothing, because nothing is bound to silence.
|
|
4159
|
+
* - PAIR level — the row was read and has no entry for this device, or none
|
|
4160
|
+
* for this cap, or none with `wrapperAddonId` — is `missing`: the operator
|
|
4161
|
+
* never touched this pair.
|
|
4162
|
+
*/
|
|
4163
|
+
async function readWrapperActivation(deps, query) {
|
|
4164
|
+
const settings = deps.ctx.settings;
|
|
4165
|
+
if (settings?.readAddonStoreKeyResult === void 0) return {
|
|
4166
|
+
kind: "failed",
|
|
4167
|
+
error: "this settings view has no single-key three-way read"
|
|
4168
|
+
};
|
|
4169
|
+
const read = await settings.readAddonStoreKeyResult(DEVICE_BINDINGS_KEY);
|
|
4170
|
+
if (read.kind === "unreadable") return {
|
|
4171
|
+
kind: "failed",
|
|
4172
|
+
error: read.error ?? read.reason
|
|
4173
|
+
};
|
|
4174
|
+
if (read.kind === "absent") return {
|
|
4175
|
+
kind: "failed",
|
|
4176
|
+
error: "the store holds no deviceBindings row — an empty snapshot is not evidence of an unbind"
|
|
4177
|
+
};
|
|
4178
|
+
const deviceBindings = read.value;
|
|
4179
|
+
if (!isRecord(deviceBindings)) return {
|
|
4180
|
+
kind: "failed",
|
|
4181
|
+
error: "stored deviceBindings is not a map"
|
|
4182
|
+
};
|
|
4183
|
+
const perDevice = deviceBindings[String(query.deviceId)];
|
|
4184
|
+
if (perDevice === void 0) return { kind: "missing" };
|
|
4185
|
+
if (!isRecord(perDevice)) return {
|
|
4186
|
+
kind: "failed",
|
|
4187
|
+
error: `stored bindings of device ${query.deviceId} are not a map`
|
|
4188
|
+
};
|
|
4189
|
+
const activation = perDevice[query.capName];
|
|
4190
|
+
if (activation === void 0) return { kind: "missing" };
|
|
4191
|
+
if (!isRecord(activation)) return {
|
|
4192
|
+
kind: "failed",
|
|
4193
|
+
error: `stored "${query.capName}" activation is not a record`
|
|
4194
|
+
};
|
|
4195
|
+
const wrapperAddonId = activation["wrapperAddonId"];
|
|
4196
|
+
if (wrapperAddonId === void 0) return { kind: "missing" };
|
|
4197
|
+
if (wrapperAddonId === null || typeof wrapperAddonId === "string") return {
|
|
4198
|
+
kind: "value",
|
|
4199
|
+
wrapperAddonId
|
|
4200
|
+
};
|
|
4201
|
+
return {
|
|
4202
|
+
kind: "failed",
|
|
4203
|
+
error: `stored "${query.capName}" wrapperAddonId is not a string`
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4139
4206
|
//#endregion
|
|
4140
4207
|
//#region src/builtins/device-manager/device-event-propagator.ts
|
|
4141
4208
|
/**
|
|
@@ -5540,6 +5607,7 @@ async function removeDevice(pctx, input) {
|
|
|
5540
5607
|
});
|
|
5541
5608
|
});
|
|
5542
5609
|
await pctx.settings.clearDeviceStore(deviceId);
|
|
5610
|
+
await pctx.host.forgetMirrorDevice(deviceId);
|
|
5543
5611
|
await pctx.settings.clearDeviceRuntimeState(deviceId);
|
|
5544
5612
|
await pctx.host.fieldClaims.pruneDevice(deviceId, pctx.settings);
|
|
5545
5613
|
const bindingKey = String(deviceId);
|
|
@@ -8061,6 +8129,20 @@ function deepEqual(a, b) {
|
|
|
8061
8129
|
}
|
|
8062
8130
|
//#endregion
|
|
8063
8131
|
//#region src/builtins/device-manager/mirror-row-writer.ts
|
|
8132
|
+
/**
|
|
8133
|
+
* A write-through refused because the device's row could not be read first
|
|
8134
|
+
* (D677). Nothing was written; the caller rolls its memory change back.
|
|
8135
|
+
*/
|
|
8136
|
+
var RuntimeStateRowUnreadError = class extends Error {
|
|
8137
|
+
deviceId;
|
|
8138
|
+
constructor(deviceId) {
|
|
8139
|
+
super(`runtime-state row of device ${deviceId} could not be read — nothing written, a row this process has not read is never replaced (D677)`);
|
|
8140
|
+
this.deviceId = deviceId;
|
|
8141
|
+
this.name = "RuntimeStateRowUnreadError";
|
|
8142
|
+
}
|
|
8143
|
+
};
|
|
8144
|
+
var ROW_UNREAD_REFUSAL_MESSAGE = "runtime-state row write refused — the row could not be read first, nothing written (D677)";
|
|
8145
|
+
var ROW_DROPS_CAPS_MESSAGE = "runtime-state row write DROPS caps the stored row holds (D677)";
|
|
8064
8146
|
function errorMessage$2(err) {
|
|
8065
8147
|
return err instanceof Error ? err.message : String(err);
|
|
8066
8148
|
}
|
|
@@ -8080,6 +8162,13 @@ var RuntimeStateDrainError = class extends MigrationRefusedError {
|
|
|
8080
8162
|
var MirrorRowWriter = class MirrorRowWriter {
|
|
8081
8163
|
deps;
|
|
8082
8164
|
static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
|
|
8165
|
+
/** Ceiling of the re-arm backoff while a device's row cannot be read (D677, D3). */
|
|
8166
|
+
static UNREAD_REARM_MAX_MS = 6e4;
|
|
8167
|
+
/** The re-arm delay after `refusals` consecutive unread refusals: 1 s, 2 s, 4 s … capped at 60 s. */
|
|
8168
|
+
static unreadRearmDelayMs(refusals) {
|
|
8169
|
+
const exponent = Math.max(0, refusals - 1);
|
|
8170
|
+
return Math.min(MirrorRowWriter.RUNTIME_STATE_DEBOUNCE_MS * 2 ** exponent, MirrorRowWriter.UNREAD_REARM_MAX_MS);
|
|
8171
|
+
}
|
|
8083
8172
|
/**
|
|
8084
8173
|
* Per-device disk-write debouncer for runtime-state. `setCapSlice`
|
|
8085
8174
|
* updates the in-memory mirror synchronously and emits the change
|
|
@@ -8108,6 +8197,8 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8108
8197
|
* that drops work silently reads as "never happened".
|
|
8109
8198
|
*/
|
|
8110
8199
|
skippedSinceWrite = /* @__PURE__ */ new Map();
|
|
8200
|
+
/** Devices whose unreadable-row refusal is already logged — once per episode. */
|
|
8201
|
+
rowUnreadLogged = /* @__PURE__ */ new Set();
|
|
8111
8202
|
constructor(deps) {
|
|
8112
8203
|
this.deps = deps;
|
|
8113
8204
|
}
|
|
@@ -8144,7 +8235,8 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8144
8235
|
slot = {
|
|
8145
8236
|
timer: null,
|
|
8146
8237
|
inFlight: null,
|
|
8147
|
-
lastWriteFailed: false
|
|
8238
|
+
lastWriteFailed: false,
|
|
8239
|
+
unreadRefusals: 0
|
|
8148
8240
|
};
|
|
8149
8241
|
this.runtimeStateDebounce.set(deviceId, slot);
|
|
8150
8242
|
}
|
|
@@ -8179,23 +8271,116 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8179
8271
|
this.armOrHold(deviceId, settings);
|
|
8180
8272
|
}
|
|
8181
8273
|
/** Arm the debounce timer; at fire, queue a write of the live blob unless disk already has it. */
|
|
8182
|
-
armDiskWrite(deviceId, settings) {
|
|
8274
|
+
armDiskWrite(deviceId, settings, delayMs = MirrorRowWriter.RUNTIME_STATE_DEBOUNCE_MS) {
|
|
8183
8275
|
const slot = this.slotFor(deviceId);
|
|
8184
8276
|
if (slot.timer) return;
|
|
8185
8277
|
slot.timer = setTimeout(() => {
|
|
8186
8278
|
slot.timer = null;
|
|
8187
|
-
this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot));
|
|
8188
|
-
},
|
|
8279
|
+
this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot, "debounced"));
|
|
8280
|
+
}, delayMs);
|
|
8281
|
+
}
|
|
8282
|
+
/**
|
|
8283
|
+
* Forget a removed device: cancel its armed write, await one in flight, and
|
|
8284
|
+
* drop its baseline and counters — nothing writes its row after this.
|
|
8285
|
+
*/
|
|
8286
|
+
async forget(deviceId) {
|
|
8287
|
+
const slot = this.runtimeStateDebounce.get(deviceId);
|
|
8288
|
+
if (slot?.timer) {
|
|
8289
|
+
clearTimeout(slot.timer);
|
|
8290
|
+
slot.timer = null;
|
|
8291
|
+
}
|
|
8292
|
+
if (slot?.inFlight) await slot.inFlight;
|
|
8293
|
+
this.runtimeStateDebounce.delete(deviceId);
|
|
8294
|
+
this.lastPersisted.delete(deviceId);
|
|
8295
|
+
this.skippedSinceWrite.delete(deviceId);
|
|
8296
|
+
this.rowUnreadLogged.delete(deviceId);
|
|
8297
|
+
this.heldDirty.delete(deviceId);
|
|
8298
|
+
}
|
|
8299
|
+
/**
|
|
8300
|
+
* The row is in memory, or is read now (D677). False when the read failed:
|
|
8301
|
+
* the caller writes nothing. Logged once per episode, tagged; the episode
|
|
8302
|
+
* ends at the next successful read.
|
|
8303
|
+
*/
|
|
8304
|
+
async rowReadOrRefuse(deviceId, settings, path) {
|
|
8305
|
+
if (!this.deps.isRowRead(deviceId)) try {
|
|
8306
|
+
await this.deps.readRowFirst(deviceId, settings);
|
|
8307
|
+
} catch (err) {
|
|
8308
|
+
if (!this.rowUnreadLogged.has(deviceId)) {
|
|
8309
|
+
this.rowUnreadLogged.add(deviceId);
|
|
8310
|
+
this.deps.logger.warn(ROW_UNREAD_REFUSAL_MESSAGE, {
|
|
8311
|
+
tags: { deviceId },
|
|
8312
|
+
meta: {
|
|
8313
|
+
path,
|
|
8314
|
+
error: errorMessage$2(err)
|
|
8315
|
+
}
|
|
8316
|
+
});
|
|
8317
|
+
}
|
|
8318
|
+
return false;
|
|
8319
|
+
}
|
|
8320
|
+
this.rowUnreadLogged.delete(deviceId);
|
|
8321
|
+
return true;
|
|
8189
8322
|
}
|
|
8190
|
-
/**
|
|
8191
|
-
|
|
8323
|
+
/**
|
|
8324
|
+
* The ONE call of `writeDeviceRuntimeState`. A blob that lacks a cap the
|
|
8325
|
+
* stored row holds is written (memory is the authority once the row was
|
|
8326
|
+
* read) but never silently: the dropped caps are named at warn (D677).
|
|
8327
|
+
* `$owned` is excluded — a release that empties it is the claim path's own
|
|
8328
|
+
* logged business.
|
|
8329
|
+
*/
|
|
8330
|
+
async writeRow(deviceId, settings, blob, path) {
|
|
8331
|
+
const onDisk = this.lastPersisted.get(deviceId);
|
|
8332
|
+
if (onDisk) {
|
|
8333
|
+
const dropped = Object.keys(onDisk).filter((cap) => cap !== "$owned" && !Object.hasOwn(blob, cap));
|
|
8334
|
+
if (dropped.length > 0) this.deps.logger.warn(ROW_DROPS_CAPS_MESSAGE, {
|
|
8335
|
+
tags: { deviceId },
|
|
8336
|
+
meta: {
|
|
8337
|
+
path,
|
|
8338
|
+
dropped,
|
|
8339
|
+
storedCaps: Object.keys(onDisk).length,
|
|
8340
|
+
writtenCaps: Object.keys(blob).length
|
|
8341
|
+
}
|
|
8342
|
+
});
|
|
8343
|
+
}
|
|
8344
|
+
await settings.writeDeviceRuntimeState(deviceId, blob);
|
|
8345
|
+
this.lastPersisted.set(deviceId, blob);
|
|
8346
|
+
}
|
|
8347
|
+
/**
|
|
8348
|
+
* A claim's write-through (D663): queued behind the device's writes, the row
|
|
8349
|
+
* read first, the blob taken when it runs. Rejects with
|
|
8350
|
+
* {@link RuntimeStateRowUnreadError} when the row cannot be read, or with the
|
|
8351
|
+
* write's own error; resolves to what landed.
|
|
8352
|
+
*/
|
|
8353
|
+
writeThroughRow(deviceId, settings, path) {
|
|
8354
|
+
return this.serialiseWrite(deviceId, async () => {
|
|
8355
|
+
if (!await this.rowReadOrRefuse(deviceId, settings, path)) throw new RuntimeStateRowUnreadError(deviceId);
|
|
8356
|
+
const live = this.deps.persistableFor(deviceId);
|
|
8357
|
+
await this.writeRow(deviceId, settings, live, path);
|
|
8358
|
+
return live;
|
|
8359
|
+
});
|
|
8360
|
+
}
|
|
8361
|
+
/**
|
|
8362
|
+
* The debounced write itself: the row read first (D677), then the blob as
|
|
8363
|
+
* memory holds it NOW, behind the effective-change gate. Never rejects. An
|
|
8364
|
+
* unreadable row writes nothing; the debounced path re-arms with a backoff
|
|
8365
|
+
* (1 s doubling to 60 s — never a 1 Hz loop, D3), and the armed timer is
|
|
8366
|
+
* what a migration drain sees as owed, so a row that has become readable
|
|
8367
|
+
* drains normally. The shutdown flush does not re-arm.
|
|
8368
|
+
*/
|
|
8369
|
+
async writeLiveBlob(deviceId, settings, slot, path) {
|
|
8370
|
+
if (!await this.rowReadOrRefuse(deviceId, settings, path)) {
|
|
8371
|
+
if (path === "debounced") {
|
|
8372
|
+
slot.unreadRefusals += 1;
|
|
8373
|
+
this.rearmAfterUnread(deviceId, settings, slot);
|
|
8374
|
+
}
|
|
8375
|
+
return;
|
|
8376
|
+
}
|
|
8377
|
+
slot.unreadRefusals = 0;
|
|
8192
8378
|
const blob = this.deps.persistableFor(deviceId);
|
|
8193
8379
|
if (this.isAlreadyPersisted(deviceId, blob)) return;
|
|
8194
8380
|
const skipped = this.skippedSinceWrite.get(deviceId) ?? 0;
|
|
8195
8381
|
this.skippedSinceWrite.delete(deviceId);
|
|
8196
8382
|
try {
|
|
8197
|
-
await
|
|
8198
|
-
this.lastPersisted.set(deviceId, blob);
|
|
8383
|
+
await this.writeRow(deviceId, settings, blob, path);
|
|
8199
8384
|
slot.lastWriteFailed = false;
|
|
8200
8385
|
if (skipped > 0) this.deps.logger.debug("runtime state persisted", {
|
|
8201
8386
|
tags: { deviceId },
|
|
@@ -8212,6 +8397,14 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8212
8397
|
});
|
|
8213
8398
|
}
|
|
8214
8399
|
}
|
|
8400
|
+
/** Re-arm an unread refusal with its backoff, or remember it for `release` while held. */
|
|
8401
|
+
rearmAfterUnread(deviceId, settings, slot) {
|
|
8402
|
+
if (this.writesHeld.has(deviceId)) {
|
|
8403
|
+
this.heldDirty.add(deviceId);
|
|
8404
|
+
return;
|
|
8405
|
+
}
|
|
8406
|
+
this.armDiskWrite(deviceId, settings, MirrorRowWriter.unreadRearmDelayMs(slot.unreadRefusals));
|
|
8407
|
+
}
|
|
8215
8408
|
/**
|
|
8216
8409
|
* Hold the debounced writer off these devices' rows while a migration swaps
|
|
8217
8410
|
* them (D663 re-review N-6). Resolves once each device is DRAINED: a write
|
|
@@ -8265,6 +8458,7 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8265
8458
|
if (slot?.inFlight) await slot.inFlight;
|
|
8266
8459
|
const lastFailed = slot?.lastWriteFailed ?? false;
|
|
8267
8460
|
if (!pending && !lastFailed) return;
|
|
8461
|
+
if (!await this.rowReadOrRefuse(deviceId, settings, "drain")) this.refuseDrain(deviceId, "its runtime-state row could not be read first", null);
|
|
8268
8462
|
const blob = this.deps.persistableFor(deviceId);
|
|
8269
8463
|
if (this.isAlreadyPersisted(deviceId, blob)) {
|
|
8270
8464
|
if (slot) slot.lastWriteFailed = false;
|
|
@@ -8272,11 +8466,10 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8272
8466
|
}
|
|
8273
8467
|
if (lastFailed) this.refuseDrain(deviceId, "its last runtime-state write did not land", null);
|
|
8274
8468
|
try {
|
|
8275
|
-
await this.serialiseWrite(deviceId, () =>
|
|
8469
|
+
await this.serialiseWrite(deviceId, () => this.writeRow(deviceId, settings, blob, "drain"));
|
|
8276
8470
|
} catch (err) {
|
|
8277
8471
|
this.refuseDrain(deviceId, `the pending runtime-state write failed: ${errorMessage$2(err)}`, err);
|
|
8278
8472
|
}
|
|
8279
|
-
this.lastPersisted.set(deviceId, blob);
|
|
8280
8473
|
}
|
|
8281
8474
|
/** A drain that cannot vouch for the row: still owed (re-armed on release), logged, refused. */
|
|
8282
8475
|
refuseDrain(deviceId, reason, cause) {
|
|
@@ -8309,7 +8502,7 @@ var MirrorRowWriter = class MirrorRowWriter {
|
|
|
8309
8502
|
clearTimeout(slot.timer);
|
|
8310
8503
|
slot.timer = null;
|
|
8311
8504
|
if (settings) {
|
|
8312
|
-
pending.push(this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot)));
|
|
8505
|
+
pending.push(this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot, "shutdown-flush")));
|
|
8313
8506
|
continue;
|
|
8314
8507
|
}
|
|
8315
8508
|
}
|
|
@@ -8366,9 +8559,18 @@ var DeviceStateMirror = class {
|
|
|
8366
8559
|
stateMirror = /* @__PURE__ */ new Map();
|
|
8367
8560
|
/** The claims and the merged view of every claimed cap (D663). */
|
|
8368
8561
|
claims;
|
|
8369
|
-
/**
|
|
8562
|
+
/**
|
|
8563
|
+
* Devices whose CLAIMS are known: the row was read, or a loaded claims index
|
|
8564
|
+
* vouched that it names none (D447). This is a READ-path fact — what a
|
|
8565
|
+
* status read may answer from. It never licenses a row write (D677).
|
|
8566
|
+
*/
|
|
8370
8567
|
seeded = /* @__PURE__ */ new Set();
|
|
8371
|
-
/**
|
|
8568
|
+
/**
|
|
8569
|
+
* Devices whose row was actually READ into memory in THIS process. Every row
|
|
8570
|
+
* write of a device requires it (D677): the write replaces the whole row, so
|
|
8571
|
+
* memory must hold what the row held — an index that vouches for the claims
|
|
8572
|
+
* says nothing about the native slices beside them.
|
|
8573
|
+
*/
|
|
8372
8574
|
rowRead = /* @__PURE__ */ new Set();
|
|
8373
8575
|
/** One row read per device, however many writes arrive during it. */
|
|
8374
8576
|
seeding = /* @__PURE__ */ new Map();
|
|
@@ -8387,7 +8589,9 @@ var DeviceStateMirror = class {
|
|
|
8387
8589
|
this.writer = new MirrorRowWriter({
|
|
8388
8590
|
logger: ctx.logger,
|
|
8389
8591
|
policyFor,
|
|
8390
|
-
persistableFor: (deviceId) => this.persistableForDevice(deviceId)
|
|
8592
|
+
persistableFor: (deviceId) => this.persistableForDevice(deviceId),
|
|
8593
|
+
isRowRead: (deviceId) => this.rowRead.has(deviceId),
|
|
8594
|
+
readRowFirst: (deviceId, settings) => this.ensureRowRead(deviceId, settings)
|
|
8391
8595
|
});
|
|
8392
8596
|
}
|
|
8393
8597
|
/**
|
|
@@ -8587,8 +8791,28 @@ var DeviceStateMirror = class {
|
|
|
8587
8791
|
return this.seeded.has(deviceId);
|
|
8588
8792
|
}
|
|
8589
8793
|
/**
|
|
8794
|
+
* Forget a removed device (D677): its armed row write is cancelled and one
|
|
8795
|
+
* in flight awaited BEFORE its memory goes, so nothing writes its row back
|
|
8796
|
+
* after `removeDevice` clears it.
|
|
8797
|
+
*/
|
|
8798
|
+
async forgetDevice(deviceId) {
|
|
8799
|
+
await this.seeding.get(deviceId)?.catch(() => void 0);
|
|
8800
|
+
await this.writer.forget(deviceId);
|
|
8801
|
+
this.stateMirror.delete(deviceId);
|
|
8802
|
+
this.claims.clearDevice(deviceId);
|
|
8803
|
+
this.seeded.delete(deviceId);
|
|
8804
|
+
this.rowRead.delete(deviceId);
|
|
8805
|
+
this.seedRefusalLogged.delete(deviceId);
|
|
8806
|
+
}
|
|
8807
|
+
/** The device's row was read in this process — the precondition of every row write (D677). */
|
|
8808
|
+
isRowRead(deviceId) {
|
|
8809
|
+
return this.rowRead.has(deviceId);
|
|
8810
|
+
}
|
|
8811
|
+
/**
|
|
8590
8812
|
* The SYNCHRONOUS half of {@link ensureSeeded}, branch (c): a LOADED claims
|
|
8591
|
-
* index that does not name the device vouches for
|
|
8813
|
+
* index that does not name the device vouches for its CLAIMS — seeded, no
|
|
8814
|
+
* read. That is all it vouches for: the device is still not `rowRead`, and
|
|
8815
|
+
* the writer reads the row before its first write (D677).
|
|
8592
8816
|
* Refused (false) while a read is in flight, because that read may be
|
|
8593
8817
|
* landing a claim the index does not know yet; refused when the index cannot
|
|
8594
8818
|
* vouch. The overlay's `peek` calls this instead of `ensureSeeded`: a sync
|
|
@@ -8603,14 +8827,21 @@ var DeviceStateMirror = class {
|
|
|
8603
8827
|
return true;
|
|
8604
8828
|
}
|
|
8605
8829
|
/**
|
|
8606
|
-
* Make sure the device's
|
|
8607
|
-
* (D49). Index first, single-flight (ruling S6):
|
|
8830
|
+
* Make sure the device's CLAIMS are known before a status read is answered
|
|
8831
|
+
* or a native slice applied (D49). Index first, single-flight (ruling S6):
|
|
8608
8832
|
* (a) already seeded → nothing;
|
|
8609
8833
|
* (b) a seed in flight → await it;
|
|
8610
8834
|
* (c) a LOADED index that does not name the device → seeded, no read;
|
|
8611
8835
|
* (d) otherwise read the row once and seed it.
|
|
8612
|
-
* A read that throws propagates
|
|
8613
|
-
*
|
|
8836
|
+
* A read that throws propagates and is logged once per device transition
|
|
8837
|
+
* (D391).
|
|
8838
|
+
*
|
|
8839
|
+
* Also the native-write RPC's gate: a slice may be APPLIED once its claims
|
|
8840
|
+
* are known. Never the ROW write's: branch (c) knows the claims and nothing
|
|
8841
|
+
* about the native slices on the row; a writer that trusted it replaced a
|
|
8842
|
+
* row it never read with the one slice it held, and every restored cap
|
|
8843
|
+
* nobody re-wrote soon after boot was erased (D677, superseding S6(c) for
|
|
8844
|
+
* writes). The writer calls {@link ensureRowRead}.
|
|
8614
8845
|
*/
|
|
8615
8846
|
async ensureSeeded(deviceId, settings, index) {
|
|
8616
8847
|
if (this.seeded.has(deviceId)) return;
|
|
@@ -8619,7 +8850,13 @@ var DeviceStateMirror = class {
|
|
|
8619
8850
|
if (this.seedIfIndexClears(deviceId, index)) return;
|
|
8620
8851
|
return this.readRowOnce(deviceId, settings);
|
|
8621
8852
|
}
|
|
8622
|
-
/**
|
|
8853
|
+
/**
|
|
8854
|
+
* A row write replaces the whole row: memory must hold what the row held
|
|
8855
|
+
* (D677). Reads the row once, single-flight, and fills memory's gaps from it
|
|
8856
|
+
* (`seedMirror('fill')`: a slice memory already holds is newer and wins).
|
|
8857
|
+
* A read that throws propagates — nothing is written — and is logged once
|
|
8858
|
+
* per device transition (D49, D391).
|
|
8859
|
+
*/
|
|
8623
8860
|
async ensureRowRead(deviceId, settings) {
|
|
8624
8861
|
if (this.rowRead.has(deviceId)) return;
|
|
8625
8862
|
const inFlight = this.seeding.get(deviceId);
|
|
@@ -8673,13 +8910,8 @@ var DeviceStateMirror = class {
|
|
|
8673
8910
|
* `changed: false`, and consumers are handed the truth that stands.
|
|
8674
8911
|
*/
|
|
8675
8912
|
async writeThrough(deviceId, settings, op, rollback) {
|
|
8676
|
-
let blob;
|
|
8677
8913
|
try {
|
|
8678
|
-
|
|
8679
|
-
const live = this.persistableForDevice(deviceId);
|
|
8680
|
-
await settings.writeDeviceRuntimeState(deviceId, live);
|
|
8681
|
-
return live;
|
|
8682
|
-
});
|
|
8914
|
+
await this.writer.writeThroughRow(deviceId, settings, op.op);
|
|
8683
8915
|
} catch (err) {
|
|
8684
8916
|
const extras = {
|
|
8685
8917
|
tags: { deviceId },
|
|
@@ -8698,7 +8930,6 @@ var DeviceStateMirror = class {
|
|
|
8698
8930
|
}
|
|
8699
8931
|
throw err;
|
|
8700
8932
|
}
|
|
8701
|
-
this.writer.markPersisted(deviceId, blob);
|
|
8702
8933
|
}
|
|
8703
8934
|
/** After a rollback, re-emit what is true now: the restored claim's merge, or the native. */
|
|
8704
8935
|
reemitAfterRollback(deviceId, capName) {
|
|
@@ -9326,6 +9557,7 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
9326
9557
|
},
|
|
9327
9558
|
holdMirrorWrites: (deviceIds, settings) => this.stateMirror.holdDiskWrites(deviceIds, settings),
|
|
9328
9559
|
mirrorOwnedCaps: (deviceId) => this.stateMirror.ownedCaps(deviceId),
|
|
9560
|
+
forgetMirrorDevice: (deviceId) => this.stateMirror.forgetDevice(deviceId),
|
|
9329
9561
|
resolveDeviceOnline: (deviceId, fallbackOnline) => this.stateMirror.resolveDeviceOnline(deviceId, fallbackOnline),
|
|
9330
9562
|
resolveDeviceProbed: (deviceId) => this.stateMirror.resolveDeviceProbed(deviceId),
|
|
9331
9563
|
waitDeviceProvider: (addonId, timeoutMs) => this.waitDeviceProvider(addonId, timeoutMs),
|
|
@@ -9668,6 +9900,10 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
9668
9900
|
active: input.active
|
|
9669
9901
|
});
|
|
9670
9902
|
},
|
|
9903
|
+
getWrapperActivation: (input) => readWrapperActivation(this.wrapperActivationDeps, {
|
|
9904
|
+
deviceId: input.deviceId,
|
|
9905
|
+
capName: input.capName
|
|
9906
|
+
}),
|
|
9671
9907
|
listWrappersForCap: async (input) => this.listWrappersForCap(input),
|
|
9672
9908
|
listBindableCapsForDeviceType: async (input) => this.listBindableCapsForDeviceType(input),
|
|
9673
9909
|
getDeviceSettingsAggregate: async (input) => {
|
|
@@ -40,6 +40,12 @@ export interface ProviderHost {
|
|
|
40
40
|
* write-through may not have landed yet. A migration's second look (D663).
|
|
41
41
|
*/
|
|
42
42
|
mirrorOwnedCaps(deviceId: number): OwnedCaps;
|
|
43
|
+
/**
|
|
44
|
+
* Forget a REMOVED device in the mirror: cancel its armed row write, await
|
|
45
|
+
* one in flight, drop its slices, claims and baseline (D677). Called before
|
|
46
|
+
* the row is cleared, so no write lands an orphan row after it.
|
|
47
|
+
*/
|
|
48
|
+
forgetMirrorDevice(deviceId: number): Promise<void>;
|
|
43
49
|
/** Reset a stale per-session `feature-probe` timestamp before mirror seeding. */
|
|
44
50
|
withResetSessionProbe(blob: Record<string, unknown>): Record<string, unknown>;
|
|
45
51
|
/** The per-field claims index (D663) — `removeDevice` prunes a removed device from it. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AddonContext, ICapabilityRegistry } from '@camstack/types';
|
|
1
|
+
import { AddonContext, ICapabilityRegistry, WrapperActivationRead } from '@camstack/types';
|
|
2
2
|
import { WriteLock } from './write-lock.js';
|
|
3
3
|
/**
|
|
4
4
|
* What a wrapper-activation write needs, and nothing more.
|
|
@@ -34,3 +34,28 @@ export interface WrapperActivationInput {
|
|
|
34
34
|
* this module shipped until 2026-09-20.
|
|
35
35
|
*/
|
|
36
36
|
export declare function setWrapperActive(deps: WrapperActivationDeps, input: WrapperActivationInput): Promise<void>;
|
|
37
|
+
/** Which activation `getWrapperActivation` is asked about. */
|
|
38
|
+
export interface WrapperActivationQuery {
|
|
39
|
+
readonly deviceId: number;
|
|
40
|
+
readonly capName: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The STORED activation for one `(device, capName)` pair, three-way — the
|
|
44
|
+
* provider behind `deviceManager.getWrapperActivation` (D675).
|
|
45
|
+
*
|
|
46
|
+
* Reads ONE row — this addon's `deviceBindings` key (~1.6 KB live) — through
|
|
47
|
+
* the settings view's single-key three-way read, never the addon's whole store
|
|
48
|
+
* (~625 KB) and never `readAddonStore()`, which folds a failed read into `{}`.
|
|
49
|
+
*
|
|
50
|
+
* Absence is graded by LEVEL:
|
|
51
|
+
* - STORE level — the read failed, or the `deviceBindings` row does not exist,
|
|
52
|
+
* or it is not a map — is `failed`. Every hub that has ever bound anything
|
|
53
|
+
* holds that row, so its absence is the signature of a store answering from
|
|
54
|
+
* an empty or unready snapshot (D44), and it must never silence a press
|
|
55
|
+
* (D49). An install that has never bound anything lands here too; erring
|
|
56
|
+
* `failed` there costs nothing, because nothing is bound to silence.
|
|
57
|
+
* - PAIR level — the row was read and has no entry for this device, or none
|
|
58
|
+
* for this cap, or none with `wrapperAddonId` — is `missing`: the operator
|
|
59
|
+
* never touched this pair.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readWrapperActivation(deps: Pick<WrapperActivationDeps, 'ctx'>, query: WrapperActivationQuery): Promise<WrapperActivationRead>;
|
|
@@ -39,9 +39,18 @@ export declare class DeviceStateMirror {
|
|
|
39
39
|
private readonly stateMirror;
|
|
40
40
|
/** The claims and the merged view of every claimed cap (D663). */
|
|
41
41
|
private readonly claims;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Devices whose CLAIMS are known: the row was read, or a loaded claims index
|
|
44
|
+
* vouched that it names none (D447). This is a READ-path fact — what a
|
|
45
|
+
* status read may answer from. It never licenses a row write (D677).
|
|
46
|
+
*/
|
|
43
47
|
private readonly seeded;
|
|
44
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Devices whose row was actually READ into memory in THIS process. Every row
|
|
50
|
+
* write of a device requires it (D677): the write replaces the whole row, so
|
|
51
|
+
* memory must hold what the row held — an index that vouches for the claims
|
|
52
|
+
* says nothing about the native slices beside them.
|
|
53
|
+
*/
|
|
45
54
|
private readonly rowRead;
|
|
46
55
|
/** One row read per device, however many writes arrive during it. */
|
|
47
56
|
private readonly seeding;
|
|
@@ -103,9 +112,19 @@ export declare class DeviceStateMirror {
|
|
|
103
112
|
/** What the merge says for a claimed cap; null when the cap is not claimed. */
|
|
104
113
|
effectiveIfClaimed(deviceId: number, cap: string): MergedSlice | null;
|
|
105
114
|
isSeeded(deviceId: number): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Forget a removed device (D677): its armed row write is cancelled and one
|
|
117
|
+
* in flight awaited BEFORE its memory goes, so nothing writes its row back
|
|
118
|
+
* after `removeDevice` clears it.
|
|
119
|
+
*/
|
|
120
|
+
forgetDevice(deviceId: number): Promise<void>;
|
|
121
|
+
/** The device's row was read in this process — the precondition of every row write (D677). */
|
|
122
|
+
isRowRead(deviceId: number): boolean;
|
|
106
123
|
/**
|
|
107
124
|
* The SYNCHRONOUS half of {@link ensureSeeded}, branch (c): a LOADED claims
|
|
108
|
-
* index that does not name the device vouches for
|
|
125
|
+
* index that does not name the device vouches for its CLAIMS — seeded, no
|
|
126
|
+
* read. That is all it vouches for: the device is still not `rowRead`, and
|
|
127
|
+
* the writer reads the row before its first write (D677).
|
|
109
128
|
* Refused (false) while a read is in flight, because that read may be
|
|
110
129
|
* landing a claim the index does not know yet; refused when the index cannot
|
|
111
130
|
* vouch. The overlay's `peek` calls this instead of `ensureSeeded`: a sync
|
|
@@ -114,18 +133,31 @@ export declare class DeviceStateMirror {
|
|
|
114
133
|
*/
|
|
115
134
|
seedIfIndexClears(deviceId: number, index: ClaimsIndexView): boolean;
|
|
116
135
|
/**
|
|
117
|
-
* Make sure the device's
|
|
118
|
-
* (D49). Index first, single-flight (ruling S6):
|
|
136
|
+
* Make sure the device's CLAIMS are known before a status read is answered
|
|
137
|
+
* or a native slice applied (D49). Index first, single-flight (ruling S6):
|
|
119
138
|
* (a) already seeded → nothing;
|
|
120
139
|
* (b) a seed in flight → await it;
|
|
121
140
|
* (c) a LOADED index that does not name the device → seeded, no read;
|
|
122
141
|
* (d) otherwise read the row once and seed it.
|
|
123
|
-
* A read that throws propagates
|
|
124
|
-
*
|
|
142
|
+
* A read that throws propagates and is logged once per device transition
|
|
143
|
+
* (D391).
|
|
144
|
+
*
|
|
145
|
+
* Also the native-write RPC's gate: a slice may be APPLIED once its claims
|
|
146
|
+
* are known. Never the ROW write's: branch (c) knows the claims and nothing
|
|
147
|
+
* about the native slices on the row; a writer that trusted it replaced a
|
|
148
|
+
* row it never read with the one slice it held, and every restored cap
|
|
149
|
+
* nobody re-wrote soon after boot was erased (D677, superseding S6(c) for
|
|
150
|
+
* writes). The writer calls {@link ensureRowRead}.
|
|
125
151
|
*/
|
|
126
152
|
ensureSeeded(deviceId: number, settings: DeviceManagerSettings, index: ClaimsIndexView): Promise<void>;
|
|
127
|
-
/**
|
|
128
|
-
|
|
153
|
+
/**
|
|
154
|
+
* A row write replaces the whole row: memory must hold what the row held
|
|
155
|
+
* (D677). Reads the row once, single-flight, and fills memory's gaps from it
|
|
156
|
+
* (`seedMirror('fill')`: a slice memory already holds is newer and wins).
|
|
157
|
+
* A read that throws propagates — nothing is written — and is logged once
|
|
158
|
+
* per device transition (D49, D391).
|
|
159
|
+
*/
|
|
160
|
+
ensureRowRead(deviceId: number, settings: DeviceManagerSettings): Promise<void>;
|
|
129
161
|
/** An unreadable row is a NAMED refusal (D49): the answer is unknown — a
|
|
130
162
|
* throw could be read as "gone", `not-claimed` as "never was". Logged by `readRowOnce`. */
|
|
131
163
|
private readRowOrRefuse;
|