@camstack/system 1.2.180 → 1.2.182
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 +2 -2
- package/dist/addon-runner.mjs +2 -2
- 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 +235 -89
- package/dist/builtins/device-manager/device-manager.addon.mjs +235 -89
- package/dist/builtins/device-manager/device-meta-actions.d.ts +7 -18
- package/dist/builtins/device-manager/device-provider-context.d.ts +4 -0
- package/dist/builtins/device-manager/migrate-device.d.ts +14 -3
- package/dist/builtins/device-manager/migrate-guard.d.ts +39 -0
- package/dist/builtins/device-manager/migrate-reload.d.ts +8 -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.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 +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-DqNTelT4.mjs → dist-CVPtxkvV.mjs} +64 -2
- package/dist/{dist-TEwQZL3g.js → dist-D8VIyu_0.js} +64 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{manifest-system-deps-BVIr3MYJ.js → manifest-system-deps-BbP419V1.js} +1 -1
- package/dist/{manifest-system-deps-Ci-jhrxY.mjs → manifest-system-deps-Bdezdcd5.mjs} +1 -1
- package/dist/{retired-settings-keys-CeSITpIX.mjs → retired-settings-keys-qZHMIuI5.mjs} +1 -1
- package/dist/{retired-settings-keys-DauPtbFR.js → retired-settings-keys-wakiINPT.js} +1 -1
- package/package.json +1 -1
package/dist/addon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_manifest_system_deps = require("./manifest-system-deps-
|
|
2
|
+
const require_manifest_system_deps = require("./manifest-system-deps-BbP419V1.js");
|
|
3
3
|
const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -317,7 +317,7 @@ async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNo
|
|
|
317
317
|
};
|
|
318
318
|
}));
|
|
319
319
|
await restoreFn.call(addon, savedDevices);
|
|
320
|
-
log?.info?.(`[worker-restore] "${addonId}":
|
|
320
|
+
log?.info?.(`[worker-restore] "${addonId}": restore pass complete for ${savedDevices.length} persisted device(s)`);
|
|
321
321
|
} catch (err) {
|
|
322
322
|
log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${(0, _camstack_types_addon.errMsg)(err)}`);
|
|
323
323
|
}
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as createUdsLoggerWithControl, Ft as RUNNER_RSS_BUDGET_ENV, Jt as startRunnerHeapWatch, Qt as emitHeapDiagnosticReport, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, j as LocalChildClient, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-
|
|
1
|
+
import { A as createUdsLoggerWithControl, Ft as RUNNER_RSS_BUDGET_ENV, Jt as startRunnerHeapWatch, Qt as emitHeapDiagnosticReport, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, j as LocalChildClient, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-Bdezdcd5.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -314,7 +314,7 @@ async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNo
|
|
|
314
314
|
};
|
|
315
315
|
}));
|
|
316
316
|
await restoreFn.call(addon, savedDevices);
|
|
317
|
-
log?.info?.(`[worker-restore] "${addonId}":
|
|
317
|
+
log?.info?.(`[worker-restore] "${addonId}": restore pass complete for ${savedDevices.length} persisted device(s)`);
|
|
318
318
|
} catch (err) {
|
|
319
319
|
log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${errMsg(err)}`);
|
|
320
320
|
}
|
|
@@ -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-D8VIyu_0.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 { E as addonPagesCapability, Ft as BaseAddon, Pt as errMsg, sn as EventCategory } from "../../dist-
|
|
1
|
+
import { E as addonPagesCapability, Ft as BaseAddon, Pt as errMsg, sn as EventCategory } from "../../dist-CVPtxkvV.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-D8VIyu_0.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 { Ft as BaseAddon, O as addonWidgetsCapability, Pt as errMsg, sn as EventCategory } from "../../dist-
|
|
1
|
+
import { Ft as BaseAddon, O as addonWidgetsCapability, Pt as errMsg, sn as EventCategory } from "../../dist-CVPtxkvV.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-D8VIyu_0.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 { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, k as alertsCapability, n as AlertSchema, sn as EventCategory } from "../../dist-
|
|
1
|
+
import { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, k as alertsCapability, n as AlertSchema, sn as EventCategory } from "../../dist-CVPtxkvV.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-D8VIyu_0.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 { Ft as BaseAddon, j as backupCapability, sn as EventCategory } from "../../dist-
|
|
1
|
+
import { Ft as BaseAddon, j as backupCapability, sn as EventCategory } from "../../dist-CVPtxkvV.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-D8VIyu_0.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 { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-
|
|
1
|
+
import { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-CVPtxkvV.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-D8VIyu_0.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 { Ft as BaseAddon, I as coreBlockAddonId, L as coreBlockIdFromAddonId, Pt as errMsg, R as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-
|
|
1
|
+
import { Ft as BaseAddon, I as coreBlockAddonId, L as coreBlockIdFromAddonId, Pt as errMsg, R as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-CVPtxkvV.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,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-D8VIyu_0.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-wakiINPT.js");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let _camstack_types_node = require("@camstack/types/node");
|
|
10
10
|
//#region src/builtins/device-manager/adoption-job-engine.ts
|
|
@@ -1896,7 +1896,10 @@ async function discoverDevices(pctx, input) {
|
|
|
1896
1896
|
stableId: d.stableId,
|
|
1897
1897
|
type: d.type,
|
|
1898
1898
|
suggestedName: d.suggestedName,
|
|
1899
|
-
prefilledConfig: d.prefilledConfig
|
|
1899
|
+
prefilledConfig: d.prefilledConfig,
|
|
1900
|
+
...d.alreadyOnboarded === true ? { alreadyOnboarded: true } : {},
|
|
1901
|
+
...d.onboardedDeviceId !== void 0 ? { onboardedDeviceId: d.onboardedDeviceId } : {},
|
|
1902
|
+
...d.onboardedName !== void 0 ? { onboardedName: d.onboardedName } : {}
|
|
1900
1903
|
}));
|
|
1901
1904
|
}
|
|
1902
1905
|
async function adoptDevice(pctx, input) {
|
|
@@ -3280,8 +3283,6 @@ var DeviceEventPropagator = class {
|
|
|
3280
3283
|
return chain;
|
|
3281
3284
|
}
|
|
3282
3285
|
};
|
|
3283
|
-
//#endregion
|
|
3284
|
-
//#region src/builtins/device-manager/migrate-device.ts
|
|
3285
3286
|
/** A refusal that names a switch the camera does not offer, rather than one it
|
|
3286
3287
|
* could not be asked about. The cap documents rejecting the former; anything
|
|
3287
3288
|
* else is treated as the camera being out of reach, which is the safe reading:
|
|
@@ -3293,10 +3294,31 @@ function classify(err) {
|
|
|
3293
3294
|
detail
|
|
3294
3295
|
};
|
|
3295
3296
|
}
|
|
3297
|
+
/**
|
|
3298
|
+
* One switch write under the bound. The race, not the write, is what fails on
|
|
3299
|
+
* timeout — the underlying promise is marked handled so a late rejection from
|
|
3300
|
+
* a camera that eventually answers "no" cannot surface as an unhandled
|
|
3301
|
+
* rejection after the migration already reported `unreachable`.
|
|
3302
|
+
*/
|
|
3303
|
+
async function setSwitchBounded(deps, deviceId, switchId, enabled) {
|
|
3304
|
+
const budget = deps.switchTimeoutMs ?? 15e3;
|
|
3305
|
+
const write = deps.setSwitch(deviceId, switchId, enabled);
|
|
3306
|
+
write.catch(() => {});
|
|
3307
|
+
let timer;
|
|
3308
|
+
try {
|
|
3309
|
+
await Promise.race([write, new Promise((_resolve, reject) => {
|
|
3310
|
+
timer = setTimeout(() => {
|
|
3311
|
+
reject(/* @__PURE__ */ new Error(`switch write did not answer within ${budget}ms — reported unreachable; if it lands late, a duplicate write of the same value is a no-op`));
|
|
3312
|
+
}, budget);
|
|
3313
|
+
})]);
|
|
3314
|
+
} finally {
|
|
3315
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3296
3318
|
async function applyAll(deps, deviceId, enabled) {
|
|
3297
3319
|
const out = [];
|
|
3298
3320
|
for (const switchId of require_dist.CAMERA_SWITCH_ORDER) try {
|
|
3299
|
-
await deps
|
|
3321
|
+
await setSwitchBounded(deps, deviceId, switchId, enabled);
|
|
3300
3322
|
out.push({
|
|
3301
3323
|
deviceId,
|
|
3302
3324
|
switchId,
|
|
@@ -3344,7 +3366,6 @@ async function migrateDevice$1(deps, input) {
|
|
|
3344
3366
|
});
|
|
3345
3367
|
}
|
|
3346
3368
|
const up = await applyAll(deps, sourceId, true);
|
|
3347
|
-
await deps.respawnRunners([sourceId, targetId]);
|
|
3348
3369
|
deps.log("camera migrated", {
|
|
3349
3370
|
sourceId,
|
|
3350
3371
|
targetId,
|
|
@@ -3360,6 +3381,52 @@ async function migrateDevice$1(deps, input) {
|
|
|
3360
3381
|
};
|
|
3361
3382
|
}
|
|
3362
3383
|
//#endregion
|
|
3384
|
+
//#region src/builtins/device-manager/migrate-reload.ts
|
|
3385
|
+
async function reloadMigratedDevices(pctx, targets) {
|
|
3386
|
+
const ctx = pctx.host.ctx;
|
|
3387
|
+
for (const target of targets) {
|
|
3388
|
+
pctx.metaStore.idToAddonId.set(target.deviceId, target.addonId);
|
|
3389
|
+
pctx.host.remoteNativeCaps.delete(target.deviceId);
|
|
3390
|
+
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(target.deviceId);
|
|
3391
|
+
}
|
|
3392
|
+
const fallbackAddons = /* @__PURE__ */ new Map();
|
|
3393
|
+
await Promise.all(targets.map(async (target) => {
|
|
3394
|
+
try {
|
|
3395
|
+
const reloaded = await (await pctx.host.requireDeviceProvider(target.addonId)).reloadDevice({ stableId: target.stableId });
|
|
3396
|
+
ctx.logger.info("migration: device reloaded in place — no other device disturbed", {
|
|
3397
|
+
tags: { deviceId: target.deviceId },
|
|
3398
|
+
meta: {
|
|
3399
|
+
addonId: target.addonId,
|
|
3400
|
+
stableId: target.stableId,
|
|
3401
|
+
reloadedAs: reloaded.deviceId
|
|
3402
|
+
}
|
|
3403
|
+
});
|
|
3404
|
+
} catch (err) {
|
|
3405
|
+
ctx.logger.error("migration: in-place reload FAILED — falling back to a provider restart", {
|
|
3406
|
+
tags: { deviceId: target.deviceId },
|
|
3407
|
+
meta: {
|
|
3408
|
+
addonId: target.addonId,
|
|
3409
|
+
stableId: target.stableId,
|
|
3410
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3411
|
+
}
|
|
3412
|
+
});
|
|
3413
|
+
fallbackAddons.set(target.addonId, target.deviceId);
|
|
3414
|
+
}
|
|
3415
|
+
}));
|
|
3416
|
+
if (fallbackAddons.size === 0) return;
|
|
3417
|
+
await Promise.all([...fallbackAddons.entries()].map(async ([addonId, migratedDeviceId]) => {
|
|
3418
|
+
const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
|
|
3419
|
+
ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
|
|
3420
|
+
tags: { deviceId: migratedDeviceId },
|
|
3421
|
+
meta: {
|
|
3422
|
+
addonId,
|
|
3423
|
+
devicesTakenDown: owned
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
await ctx.api.addons.restartAddon.mutate({ addonId });
|
|
3427
|
+
}));
|
|
3428
|
+
}
|
|
3429
|
+
//#endregion
|
|
3363
3430
|
//#region src/builtins/device-manager/migrate-hardware-state.ts
|
|
3364
3431
|
/**
|
|
3365
3432
|
* The per-device state half of a migration (`deviceManager.migrateDevice`).
|
|
@@ -3540,72 +3607,121 @@ async function allocateDeviceId(pctx, input) {
|
|
|
3540
3607
|
* The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
|
|
3541
3608
|
* value a crash could park a row on was never used and will never be reused.
|
|
3542
3609
|
* `swapIds` therefore runs INSIDE the same lock the id was allocated under.
|
|
3610
|
+
*
|
|
3611
|
+
* Retry-safe since 2026-09-04 (`migrate-guard.ts`): a concurrent call touching
|
|
3612
|
+
* either device is REFUSED before it can queue on the lock, and a repeat of a
|
|
3613
|
+
* completed swap — matched against the `addonId`/`stableId` fingerprint the
|
|
3614
|
+
* swap left on the rows — is a no-op returning the original report. A client
|
|
3615
|
+
* retry ran a second full cycle on 2026-09-01 and swapped two cameras back;
|
|
3616
|
+
* neither path can re-run a swap any more.
|
|
3543
3617
|
*/
|
|
3544
3618
|
async function migrateDevice(pctx, input) {
|
|
3545
3619
|
const { sourceId, targetId } = input;
|
|
3546
3620
|
const ctx = pctx.host.ctx;
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
const
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
},
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
}
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
3565
|
-
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
3566
|
-
tags: { deviceId: a },
|
|
3621
|
+
pctx.migrationGuard.begin(sourceId, targetId);
|
|
3622
|
+
try {
|
|
3623
|
+
const outcome = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3624
|
+
const sourceRow = await pctx.metaStore.rows.get(sourceId);
|
|
3625
|
+
if (sourceRow === null) throw new Error(`[device-manager] migrateDevice: device ${sourceId} does not exist`);
|
|
3626
|
+
const targetRow = await pctx.metaStore.rows.get(targetId);
|
|
3627
|
+
if (targetRow === null) throw new Error(`[device-manager] migrateDevice: device ${targetId} does not exist`);
|
|
3628
|
+
const replay = pctx.migrationGuard.matchReplay(sourceId, targetId, {
|
|
3629
|
+
addonId: sourceRow.meta.addonId,
|
|
3630
|
+
stableId: sourceRow.meta.stableId
|
|
3631
|
+
}, {
|
|
3632
|
+
addonId: targetRow.meta.addonId,
|
|
3633
|
+
stableId: targetRow.meta.stableId
|
|
3634
|
+
});
|
|
3635
|
+
if (replay !== null) {
|
|
3636
|
+
ctx.logger.info("migrateDevice: this swap already completed — replaying the original report, NOTHING re-run", {
|
|
3637
|
+
tags: { deviceId: sourceId },
|
|
3567
3638
|
meta: {
|
|
3568
|
-
sourceId
|
|
3569
|
-
targetId
|
|
3570
|
-
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
3571
|
-
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
3572
|
-
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
3573
|
-
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
3639
|
+
sourceId,
|
|
3640
|
+
targetId
|
|
3574
3641
|
}
|
|
3575
3642
|
});
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
if (
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3643
|
+
return {
|
|
3644
|
+
result: replay,
|
|
3645
|
+
reloadTargets: null
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
const tempId = await pctx.metaStore.allocateNextDeviceId();
|
|
3649
|
+
const result = await migrateDevice$1({
|
|
3650
|
+
setSwitch: async (deviceId, switchId, enabled) => {
|
|
3651
|
+
if (switchId === "stream-broker") {
|
|
3652
|
+
const moved = await writeDisabledFlagUnderLock(pctx, deviceId, !enabled);
|
|
3653
|
+
emitDisabledEvents(pctx, deviceId, !enabled, moved);
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
await ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
|
|
3657
|
+
deviceId,
|
|
3658
|
+
switchId,
|
|
3659
|
+
enabled
|
|
3660
|
+
});
|
|
3661
|
+
},
|
|
3662
|
+
swapIds: async (a, b) => {
|
|
3663
|
+
await pctx.metaStore.rows.swapIds(a, b, tempId);
|
|
3664
|
+
},
|
|
3665
|
+
swapHardwareState: async (a, b) => {
|
|
3666
|
+
const swapped = await swapHardwareState(pctx.settings, a, b);
|
|
3667
|
+
pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
|
|
3668
|
+
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
3669
|
+
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
3670
|
+
tags: { deviceId: a },
|
|
3590
3671
|
meta: {
|
|
3591
|
-
|
|
3592
|
-
|
|
3672
|
+
sourceId: a,
|
|
3673
|
+
targetId: b,
|
|
3674
|
+
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
3675
|
+
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
3676
|
+
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
3677
|
+
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
3593
3678
|
}
|
|
3594
3679
|
});
|
|
3595
|
-
|
|
3680
|
+
},
|
|
3681
|
+
forgetStreamState: async (deviceId) => {
|
|
3682
|
+
await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
|
|
3683
|
+
},
|
|
3684
|
+
log: (message, meta) => {
|
|
3685
|
+
ctx.logger.info(message, {
|
|
3686
|
+
tags: { deviceId: sourceId },
|
|
3687
|
+
meta
|
|
3688
|
+
});
|
|
3596
3689
|
}
|
|
3597
|
-
},
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3690
|
+
}, {
|
|
3691
|
+
sourceId,
|
|
3692
|
+
targetId
|
|
3693
|
+
});
|
|
3694
|
+
pctx.migrationGuard.recordCompleted({
|
|
3695
|
+
sourceId,
|
|
3696
|
+
targetId,
|
|
3697
|
+
identityAtSource: {
|
|
3698
|
+
addonId: targetRow.meta.addonId,
|
|
3699
|
+
stableId: targetRow.meta.stableId
|
|
3700
|
+
},
|
|
3701
|
+
identityAtTarget: {
|
|
3702
|
+
addonId: sourceRow.meta.addonId,
|
|
3703
|
+
stableId: sourceRow.meta.stableId
|
|
3704
|
+
},
|
|
3705
|
+
result
|
|
3706
|
+
});
|
|
3707
|
+
return {
|
|
3708
|
+
result,
|
|
3709
|
+
reloadTargets: [{
|
|
3710
|
+
deviceId: sourceId,
|
|
3711
|
+
addonId: targetRow.meta.addonId,
|
|
3712
|
+
stableId: targetRow.meta.stableId
|
|
3713
|
+
}, {
|
|
3714
|
+
deviceId: targetId,
|
|
3715
|
+
addonId: sourceRow.meta.addonId,
|
|
3716
|
+
stableId: sourceRow.meta.stableId
|
|
3717
|
+
}]
|
|
3718
|
+
};
|
|
3607
3719
|
});
|
|
3608
|
-
|
|
3720
|
+
if (outcome.reloadTargets !== null) await reloadMigratedDevices(pctx, outcome.reloadTargets);
|
|
3721
|
+
return outcome.result;
|
|
3722
|
+
} finally {
|
|
3723
|
+
pctx.migrationGuard.end(sourceId, targetId);
|
|
3724
|
+
}
|
|
3609
3725
|
}
|
|
3610
3726
|
async function registerDevice(pctx, input) {
|
|
3611
3727
|
const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
|
|
@@ -4292,35 +4408,23 @@ async function setMetadata(pctx, input) {
|
|
|
4292
4408
|
});
|
|
4293
4409
|
}
|
|
4294
4410
|
/**
|
|
4295
|
-
*
|
|
4296
|
-
*
|
|
4297
|
-
*
|
|
4298
|
-
*
|
|
4299
|
-
*
|
|
4300
|
-
* TWO events leave here, and they are not the same message:
|
|
4301
|
-
*
|
|
4302
|
-
* - `device.meta-changed` fires on EVERY call. It is the meta-surface echo
|
|
4303
|
-
* every UI redraws on, and idempotence is not interesting to it.
|
|
4304
|
-
* - `device.disabled` / `device.enabled` fire ONLY when the flag actually
|
|
4305
|
-
* MOVES. They are the lifecycle announcement — the operator asked to be
|
|
4306
|
-
* told when a camera is switched off, and a write that re-states a flag the
|
|
4307
|
-
* device already had is not that. Telemetry semantics (D8) are fine here: a
|
|
4308
|
-
* dropped announcement costs a notification, never durable state, and the
|
|
4309
|
-
* meta row is the authority either way (D62 — this switch stores nothing of
|
|
4310
|
-
* its own).
|
|
4411
|
+
* Read + patch the `disabled` flag. The caller HOLDS the meta write lock —
|
|
4412
|
+
* this is split out of {@link setDisabled} so `migrateDevice`, which holds the
|
|
4413
|
+
* lock for its whole run, can write the same authority without re-acquiring
|
|
4414
|
+
* it (the self-deadlock documented at its `setSwitch`).
|
|
4311
4415
|
*/
|
|
4312
|
-
async function
|
|
4313
|
-
const
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4416
|
+
async function writeDisabledFlagUnderLock(pctx, deviceId, disabled) {
|
|
4417
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
4418
|
+
if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
|
|
4419
|
+
const { meta: m } = persisted;
|
|
4420
|
+
await pctx.metaStore.rows.patch(deviceId, { disabled });
|
|
4421
|
+
return {
|
|
4422
|
+
changed: (m.disabled ?? false) !== disabled,
|
|
4423
|
+
integrationId: m.integrationId ?? ""
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
/** The two announcements — meta echo always, lifecycle only on a transition. */
|
|
4427
|
+
function emitDisabledEvents(pctx, deviceId, disabled, moved) {
|
|
4324
4428
|
pctx.host.ctx.eventBus.emit({
|
|
4325
4429
|
id: (0, node_crypto.randomUUID)(),
|
|
4326
4430
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -4350,6 +4454,10 @@ async function setDisabled(pctx, input) {
|
|
|
4350
4454
|
}
|
|
4351
4455
|
});
|
|
4352
4456
|
}
|
|
4457
|
+
async function setDisabled(pctx, input) {
|
|
4458
|
+
const { deviceId, disabled } = input;
|
|
4459
|
+
emitDisabledEvents(pctx, deviceId, disabled, await pctx.metaStore.withMetaWriteLock(() => writeDisabledFlagUnderLock(pctx, deviceId, disabled)));
|
|
4460
|
+
}
|
|
4353
4461
|
async function loadRuntimeState(pctx, input) {
|
|
4354
4462
|
const { deviceId } = input;
|
|
4355
4463
|
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
|
|
@@ -5964,6 +6072,43 @@ function resolveNativeCapOwnerSync(pctx, capName, deviceId) {
|
|
|
5964
6072
|
};
|
|
5965
6073
|
return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
|
|
5966
6074
|
}
|
|
6075
|
+
function sameIdentity(a, b) {
|
|
6076
|
+
return a.addonId === b.addonId && a.stableId === b.stableId;
|
|
6077
|
+
}
|
|
6078
|
+
/** One guard per device-manager instance. `now` is injectable for the specs. */
|
|
6079
|
+
function createMigrationGuard(now = Date.now) {
|
|
6080
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
6081
|
+
const completed = /* @__PURE__ */ new Map();
|
|
6082
|
+
const pairKey = (sourceId, targetId) => `${sourceId}->${targetId}`;
|
|
6083
|
+
return {
|
|
6084
|
+
begin(sourceId, targetId) {
|
|
6085
|
+
for (const id of [sourceId, targetId]) if (inFlight.has(id)) throw new Error(`[device-manager] migrateDevice: a migration involving device ${id} is already in flight — this call was refused, not queued. The running migration's report stands; re-read the device rows before retrying.`);
|
|
6086
|
+
inFlight.add(sourceId);
|
|
6087
|
+
inFlight.add(targetId);
|
|
6088
|
+
},
|
|
6089
|
+
end(sourceId, targetId) {
|
|
6090
|
+
inFlight.delete(sourceId);
|
|
6091
|
+
inFlight.delete(targetId);
|
|
6092
|
+
},
|
|
6093
|
+
recordCompleted(record) {
|
|
6094
|
+
completed.set(pairKey(record.sourceId, record.targetId), {
|
|
6095
|
+
...record,
|
|
6096
|
+
completedAt: now()
|
|
6097
|
+
});
|
|
6098
|
+
},
|
|
6099
|
+
matchReplay(sourceId, targetId, atSource, atTarget) {
|
|
6100
|
+
const key = pairKey(sourceId, targetId);
|
|
6101
|
+
const record = completed.get(key);
|
|
6102
|
+
if (record === void 0) return null;
|
|
6103
|
+
if (now() - record.completedAt > 9e5) {
|
|
6104
|
+
completed.delete(key);
|
|
6105
|
+
return null;
|
|
6106
|
+
}
|
|
6107
|
+
if (!sameIdentity(record.identityAtSource, atSource) || !sameIdentity(record.identityAtTarget, atTarget)) return null;
|
|
6108
|
+
return record.result;
|
|
6109
|
+
}
|
|
6110
|
+
};
|
|
6111
|
+
}
|
|
5967
6112
|
//#endregion
|
|
5968
6113
|
//#region src/builtins/device-manager/device-manager.addon.ts
|
|
5969
6114
|
var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
@@ -6204,6 +6349,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
6204
6349
|
settings,
|
|
6205
6350
|
registry,
|
|
6206
6351
|
bindingsDeps: this.bindingsDeps,
|
|
6352
|
+
migrationGuard: createMigrationGuard(),
|
|
6207
6353
|
requireDeviceOps,
|
|
6208
6354
|
stampIntegrationId: stampIntegrationId$1,
|
|
6209
6355
|
get provider() {
|