@camstack/system 1.2.180 → 1.2.181

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.
Files changed (60) hide show
  1. package/dist/addon-runner.js +2 -2
  2. package/dist/addon-runner.mjs +2 -2
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +231 -88
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +231 -88
  17. package/dist/builtins/device-manager/device-meta-actions.d.ts +7 -18
  18. package/dist/builtins/device-manager/device-provider-context.d.ts +4 -0
  19. package/dist/builtins/device-manager/migrate-device.d.ts +14 -3
  20. package/dist/builtins/device-manager/migrate-guard.d.ts +39 -0
  21. package/dist/builtins/device-manager/migrate-reload.d.ts +8 -0
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  23. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  24. package/dist/builtins/hub-forwarder/index.js +1 -1
  25. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  26. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  27. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  28. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  29. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  30. package/dist/builtins/local-network/local-network.addon.js +1 -1
  31. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  32. package/dist/builtins/loki-logging/index.js +1 -1
  33. package/dist/builtins/loki-logging/index.mjs +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  35. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  36. package/dist/builtins/platform-probe/index.js +1 -1
  37. package/dist/builtins/platform-probe/index.mjs +1 -1
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  39. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  40. package/dist/builtins/snapshot/index.js +1 -1
  41. package/dist/builtins/snapshot/index.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  43. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  45. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  46. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  47. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  48. package/dist/builtins/system-config/system-config.addon.js +1 -1
  49. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  50. package/dist/builtins/winston-logging/index.js +1 -1
  51. package/dist/builtins/winston-logging/index.mjs +1 -1
  52. package/dist/{dist-DqNTelT4.mjs → dist-DbnNk4Lw.mjs} +44 -1
  53. package/dist/{dist-TEwQZL3g.js → dist-whe0b4_3.js} +44 -1
  54. package/dist/index.js +2 -2
  55. package/dist/index.mjs +2 -2
  56. package/dist/{manifest-system-deps-Ci-jhrxY.mjs → manifest-system-deps-DOcMuUWK.mjs} +1 -1
  57. package/dist/{manifest-system-deps-BVIr3MYJ.js → manifest-system-deps-y5vZBO6q.js} +1 -1
  58. package/dist/{retired-settings-keys-DauPtbFR.js → retired-settings-keys-BdvmiaK7.js} +1 -1
  59. package/dist/{retired-settings-keys-CeSITpIX.mjs → retired-settings-keys-CyQG8b-A.mjs} +1 -1
  60. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_system_deps = require("./manifest-system-deps-BVIr3MYJ.js");
2
+ const require_manifest_system_deps = require("./manifest-system-deps-y5vZBO6q.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}": restored ${savedDevices.length} device(s)`);
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
  }
@@ -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-Ci-jhrxY.mjs";
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-DOcMuUWK.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}": restored ${savedDevices.length} device(s)`);
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-TEwQZL3g.js");
6
+ const require_dist = require("../../dist-whe0b4_3.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-DqNTelT4.mjs";
1
+ import { E as addonPagesCapability, Ft as BaseAddon, Pt as errMsg, sn as EventCategory } from "../../dist-DbnNk4Lw.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-TEwQZL3g.js");
6
+ const require_dist = require("../../dist-whe0b4_3.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-DqNTelT4.mjs";
1
+ import { Ft as BaseAddon, O as addonWidgetsCapability, Pt as errMsg, sn as EventCategory } from "../../dist-DbnNk4Lw.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-TEwQZL3g.js");
6
+ const require_dist = require("../../dist-whe0b4_3.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-DqNTelT4.mjs";
1
+ import { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, k as alertsCapability, n as AlertSchema, sn as EventCategory } from "../../dist-DbnNk4Lw.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-TEwQZL3g.js");
6
+ const require_dist = require("../../dist-whe0b4_3.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-DqNTelT4.mjs";
1
+ import { Ft as BaseAddon, j as backupCapability, sn as EventCategory } from "../../dist-DbnNk4Lw.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-TEwQZL3g.js");
6
+ const require_dist = require("../../dist-whe0b4_3.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-DqNTelT4.mjs";
1
+ import { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-DbnNk4Lw.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-TEwQZL3g.js");
3
+ const require_dist = require("../../dist-whe0b4_3.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-DqNTelT4.mjs";
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-DbnNk4Lw.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-TEwQZL3g.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-DauPtbFR.js");
6
+ const require_dist = require("../../dist-whe0b4_3.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-BdvmiaK7.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
@@ -3280,8 +3280,6 @@ var DeviceEventPropagator = class {
3280
3280
  return chain;
3281
3281
  }
3282
3282
  };
3283
- //#endregion
3284
- //#region src/builtins/device-manager/migrate-device.ts
3285
3283
  /** A refusal that names a switch the camera does not offer, rather than one it
3286
3284
  * could not be asked about. The cap documents rejecting the former; anything
3287
3285
  * else is treated as the camera being out of reach, which is the safe reading:
@@ -3293,10 +3291,31 @@ function classify(err) {
3293
3291
  detail
3294
3292
  };
3295
3293
  }
3294
+ /**
3295
+ * One switch write under the bound. The race, not the write, is what fails on
3296
+ * timeout — the underlying promise is marked handled so a late rejection from
3297
+ * a camera that eventually answers "no" cannot surface as an unhandled
3298
+ * rejection after the migration already reported `unreachable`.
3299
+ */
3300
+ async function setSwitchBounded(deps, deviceId, switchId, enabled) {
3301
+ const budget = deps.switchTimeoutMs ?? 15e3;
3302
+ const write = deps.setSwitch(deviceId, switchId, enabled);
3303
+ write.catch(() => {});
3304
+ let timer;
3305
+ try {
3306
+ await Promise.race([write, new Promise((_resolve, reject) => {
3307
+ timer = setTimeout(() => {
3308
+ 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`));
3309
+ }, budget);
3310
+ })]);
3311
+ } finally {
3312
+ if (timer !== void 0) clearTimeout(timer);
3313
+ }
3314
+ }
3296
3315
  async function applyAll(deps, deviceId, enabled) {
3297
3316
  const out = [];
3298
3317
  for (const switchId of require_dist.CAMERA_SWITCH_ORDER) try {
3299
- await deps.setSwitch(deviceId, switchId, enabled);
3318
+ await setSwitchBounded(deps, deviceId, switchId, enabled);
3300
3319
  out.push({
3301
3320
  deviceId,
3302
3321
  switchId,
@@ -3344,7 +3363,6 @@ async function migrateDevice$1(deps, input) {
3344
3363
  });
3345
3364
  }
3346
3365
  const up = await applyAll(deps, sourceId, true);
3347
- await deps.respawnRunners([sourceId, targetId]);
3348
3366
  deps.log("camera migrated", {
3349
3367
  sourceId,
3350
3368
  targetId,
@@ -3360,6 +3378,52 @@ async function migrateDevice$1(deps, input) {
3360
3378
  };
3361
3379
  }
3362
3380
  //#endregion
3381
+ //#region src/builtins/device-manager/migrate-reload.ts
3382
+ async function reloadMigratedDevices(pctx, targets) {
3383
+ const ctx = pctx.host.ctx;
3384
+ for (const target of targets) {
3385
+ pctx.metaStore.idToAddonId.set(target.deviceId, target.addonId);
3386
+ pctx.host.remoteNativeCaps.delete(target.deviceId);
3387
+ pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(target.deviceId);
3388
+ }
3389
+ const fallbackAddons = /* @__PURE__ */ new Map();
3390
+ await Promise.all(targets.map(async (target) => {
3391
+ try {
3392
+ const reloaded = await (await pctx.host.requireDeviceProvider(target.addonId)).reloadDevice({ stableId: target.stableId });
3393
+ ctx.logger.info("migration: device reloaded in place — no other device disturbed", {
3394
+ tags: { deviceId: target.deviceId },
3395
+ meta: {
3396
+ addonId: target.addonId,
3397
+ stableId: target.stableId,
3398
+ reloadedAs: reloaded.deviceId
3399
+ }
3400
+ });
3401
+ } catch (err) {
3402
+ ctx.logger.error("migration: in-place reload FAILED — falling back to a provider restart", {
3403
+ tags: { deviceId: target.deviceId },
3404
+ meta: {
3405
+ addonId: target.addonId,
3406
+ stableId: target.stableId,
3407
+ error: err instanceof Error ? err.message : String(err)
3408
+ }
3409
+ });
3410
+ fallbackAddons.set(target.addonId, target.deviceId);
3411
+ }
3412
+ }));
3413
+ if (fallbackAddons.size === 0) return;
3414
+ await Promise.all([...fallbackAddons.entries()].map(async ([addonId, migratedDeviceId]) => {
3415
+ const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
3416
+ ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
3417
+ tags: { deviceId: migratedDeviceId },
3418
+ meta: {
3419
+ addonId,
3420
+ devicesTakenDown: owned
3421
+ }
3422
+ });
3423
+ await ctx.api.addons.restartAddon.mutate({ addonId });
3424
+ }));
3425
+ }
3426
+ //#endregion
3363
3427
  //#region src/builtins/device-manager/migrate-hardware-state.ts
3364
3428
  /**
3365
3429
  * The per-device state half of a migration (`deviceManager.migrateDevice`).
@@ -3540,72 +3604,121 @@ async function allocateDeviceId(pctx, input) {
3540
3604
  * The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
3541
3605
  * value a crash could park a row on was never used and will never be reused.
3542
3606
  * `swapIds` therefore runs INSIDE the same lock the id was allocated under.
3607
+ *
3608
+ * Retry-safe since 2026-09-04 (`migrate-guard.ts`): a concurrent call touching
3609
+ * either device is REFUSED before it can queue on the lock, and a repeat of a
3610
+ * completed swap — matched against the `addonId`/`stableId` fingerprint the
3611
+ * swap left on the rows — is a no-op returning the original report. A client
3612
+ * retry ran a second full cycle on 2026-09-01 and swapped two cameras back;
3613
+ * neither path can re-run a swap any more.
3543
3614
  */
3544
3615
  async function migrateDevice(pctx, input) {
3545
3616
  const { sourceId, targetId } = input;
3546
3617
  const ctx = pctx.host.ctx;
3547
- return await pctx.metaStore.withMetaWriteLock(async () => {
3548
- for (const id of [sourceId, targetId]) if (await pctx.metaStore.rows.get(id) === null) throw new Error(`[device-manager] migrateDevice: device ${id} does not exist`);
3549
- const tempId = await pctx.metaStore.allocateNextDeviceId();
3550
- return await migrateDevice$1({
3551
- setSwitch: async (deviceId, switchId, enabled) => {
3552
- await ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
3553
- deviceId,
3554
- switchId,
3555
- enabled
3556
- });
3557
- },
3558
- swapIds: async (a, b) => {
3559
- await pctx.metaStore.rows.swapIds(a, b, tempId);
3560
- },
3561
- swapHardwareState: async (a, b) => {
3562
- const swapped = await swapHardwareState(pctx.settings, a, b);
3563
- pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
3564
- pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
3565
- ctx.logger.info("migration: hardware-scoped device state swapped", {
3566
- tags: { deviceId: a },
3618
+ pctx.migrationGuard.begin(sourceId, targetId);
3619
+ try {
3620
+ const outcome = await pctx.metaStore.withMetaWriteLock(async () => {
3621
+ const sourceRow = await pctx.metaStore.rows.get(sourceId);
3622
+ if (sourceRow === null) throw new Error(`[device-manager] migrateDevice: device ${sourceId} does not exist`);
3623
+ const targetRow = await pctx.metaStore.rows.get(targetId);
3624
+ if (targetRow === null) throw new Error(`[device-manager] migrateDevice: device ${targetId} does not exist`);
3625
+ const replay = pctx.migrationGuard.matchReplay(sourceId, targetId, {
3626
+ addonId: sourceRow.meta.addonId,
3627
+ stableId: sourceRow.meta.stableId
3628
+ }, {
3629
+ addonId: targetRow.meta.addonId,
3630
+ stableId: targetRow.meta.stableId
3631
+ });
3632
+ if (replay !== null) {
3633
+ ctx.logger.info("migrateDevice: this swap already completed — replaying the original report, NOTHING re-run", {
3634
+ tags: { deviceId: sourceId },
3567
3635
  meta: {
3568
- sourceId: a,
3569
- targetId: b,
3570
- hardwareKeysToSource: swapped.hardwareKeysToSource,
3571
- hardwareKeysToTarget: swapped.hardwareKeysToTarget,
3572
- roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
3573
- roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
3636
+ sourceId,
3637
+ targetId
3574
3638
  }
3575
3639
  });
3576
- },
3577
- forgetStreamState: async (deviceId) => {
3578
- await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
3579
- },
3580
- respawnRunners: async (deviceIds) => {
3581
- const owners = /* @__PURE__ */ new Set();
3582
- for (const deviceId of deviceIds) {
3583
- const addonId = pctx.metaStore.idToAddonId.get(deviceId);
3584
- if (addonId !== void 0) owners.add(addonId);
3585
- }
3586
- for (const addonId of owners) {
3587
- const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
3588
- ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
3589
- tags: { deviceId: sourceId },
3640
+ return {
3641
+ result: replay,
3642
+ reloadTargets: null
3643
+ };
3644
+ }
3645
+ const tempId = await pctx.metaStore.allocateNextDeviceId();
3646
+ const result = await migrateDevice$1({
3647
+ setSwitch: async (deviceId, switchId, enabled) => {
3648
+ if (switchId === "stream-broker") {
3649
+ const moved = await writeDisabledFlagUnderLock(pctx, deviceId, !enabled);
3650
+ emitDisabledEvents(pctx, deviceId, !enabled, moved);
3651
+ return;
3652
+ }
3653
+ await ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
3654
+ deviceId,
3655
+ switchId,
3656
+ enabled
3657
+ });
3658
+ },
3659
+ swapIds: async (a, b) => {
3660
+ await pctx.metaStore.rows.swapIds(a, b, tempId);
3661
+ },
3662
+ swapHardwareState: async (a, b) => {
3663
+ const swapped = await swapHardwareState(pctx.settings, a, b);
3664
+ pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
3665
+ pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
3666
+ ctx.logger.info("migration: hardware-scoped device state swapped", {
3667
+ tags: { deviceId: a },
3590
3668
  meta: {
3591
- addonId,
3592
- devicesTakenDown: owned
3669
+ sourceId: a,
3670
+ targetId: b,
3671
+ hardwareKeysToSource: swapped.hardwareKeysToSource,
3672
+ hardwareKeysToTarget: swapped.hardwareKeysToTarget,
3673
+ roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
3674
+ roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
3593
3675
  }
3594
3676
  });
3595
- await ctx.api.addons.restartAddon.mutate({ addonId });
3677
+ },
3678
+ forgetStreamState: async (deviceId) => {
3679
+ await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
3680
+ },
3681
+ log: (message, meta) => {
3682
+ ctx.logger.info(message, {
3683
+ tags: { deviceId: sourceId },
3684
+ meta
3685
+ });
3596
3686
  }
3597
- },
3598
- log: (message, meta) => {
3599
- ctx.logger.info(message, {
3600
- tags: { deviceId: sourceId },
3601
- meta
3602
- });
3603
- }
3604
- }, {
3605
- sourceId,
3606
- targetId
3687
+ }, {
3688
+ sourceId,
3689
+ targetId
3690
+ });
3691
+ pctx.migrationGuard.recordCompleted({
3692
+ sourceId,
3693
+ targetId,
3694
+ identityAtSource: {
3695
+ addonId: targetRow.meta.addonId,
3696
+ stableId: targetRow.meta.stableId
3697
+ },
3698
+ identityAtTarget: {
3699
+ addonId: sourceRow.meta.addonId,
3700
+ stableId: sourceRow.meta.stableId
3701
+ },
3702
+ result
3703
+ });
3704
+ return {
3705
+ result,
3706
+ reloadTargets: [{
3707
+ deviceId: sourceId,
3708
+ addonId: targetRow.meta.addonId,
3709
+ stableId: targetRow.meta.stableId
3710
+ }, {
3711
+ deviceId: targetId,
3712
+ addonId: sourceRow.meta.addonId,
3713
+ stableId: sourceRow.meta.stableId
3714
+ }]
3715
+ };
3607
3716
  });
3608
- });
3717
+ if (outcome.reloadTargets !== null) await reloadMigratedDevices(pctx, outcome.reloadTargets);
3718
+ return outcome.result;
3719
+ } finally {
3720
+ pctx.migrationGuard.end(sourceId, targetId);
3721
+ }
3609
3722
  }
3610
3723
  async function registerDevice(pctx, input) {
3611
3724
  const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
@@ -4292,35 +4405,23 @@ async function setMetadata(pctx, input) {
4292
4405
  });
4293
4406
  }
4294
4407
  /**
4295
- * Soft-disable the device. Persisted on the meta row;
4296
- * lifecycle gating is the driver's responsibility (BaseDevice
4297
- * exposes `this.disabled` for the driver to consult at the top
4298
- * of its lifecycle methods).
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).
4408
+ * Read + patch the `disabled` flag. The caller HOLDS the meta write lock —
4409
+ * this is split out of {@link setDisabled} so `migrateDevice`, which holds the
4410
+ * lock for its whole run, can write the same authority without re-acquiring
4411
+ * it (the self-deadlock documented at its `setSwitch`).
4311
4412
  */
4312
- async function setDisabled(pctx, input) {
4313
- const { deviceId, disabled } = input;
4314
- const moved = await pctx.metaStore.withMetaWriteLock(async () => {
4315
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
4316
- if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
4317
- const { meta: m } = persisted;
4318
- await pctx.metaStore.rows.patch(deviceId, { disabled });
4319
- return {
4320
- changed: (m.disabled ?? false) !== disabled,
4321
- integrationId: m.integrationId ?? ""
4322
- };
4323
- });
4413
+ async function writeDisabledFlagUnderLock(pctx, deviceId, disabled) {
4414
+ const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
4415
+ if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
4416
+ const { meta: m } = persisted;
4417
+ await pctx.metaStore.rows.patch(deviceId, { disabled });
4418
+ return {
4419
+ changed: (m.disabled ?? false) !== disabled,
4420
+ integrationId: m.integrationId ?? ""
4421
+ };
4422
+ }
4423
+ /** The two announcements — meta echo always, lifecycle only on a transition. */
4424
+ function emitDisabledEvents(pctx, deviceId, disabled, moved) {
4324
4425
  pctx.host.ctx.eventBus.emit({
4325
4426
  id: (0, node_crypto.randomUUID)(),
4326
4427
  timestamp: /* @__PURE__ */ new Date(),
@@ -4350,6 +4451,10 @@ async function setDisabled(pctx, input) {
4350
4451
  }
4351
4452
  });
4352
4453
  }
4454
+ async function setDisabled(pctx, input) {
4455
+ const { deviceId, disabled } = input;
4456
+ emitDisabledEvents(pctx, deviceId, disabled, await pctx.metaStore.withMetaWriteLock(() => writeDisabledFlagUnderLock(pctx, deviceId, disabled)));
4457
+ }
4353
4458
  async function loadRuntimeState(pctx, input) {
4354
4459
  const { deviceId } = input;
4355
4460
  if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
@@ -5964,6 +6069,43 @@ function resolveNativeCapOwnerSync(pctx, capName, deviceId) {
5964
6069
  };
5965
6070
  return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
5966
6071
  }
6072
+ function sameIdentity(a, b) {
6073
+ return a.addonId === b.addonId && a.stableId === b.stableId;
6074
+ }
6075
+ /** One guard per device-manager instance. `now` is injectable for the specs. */
6076
+ function createMigrationGuard(now = Date.now) {
6077
+ const inFlight = /* @__PURE__ */ new Set();
6078
+ const completed = /* @__PURE__ */ new Map();
6079
+ const pairKey = (sourceId, targetId) => `${sourceId}->${targetId}`;
6080
+ return {
6081
+ begin(sourceId, targetId) {
6082
+ 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.`);
6083
+ inFlight.add(sourceId);
6084
+ inFlight.add(targetId);
6085
+ },
6086
+ end(sourceId, targetId) {
6087
+ inFlight.delete(sourceId);
6088
+ inFlight.delete(targetId);
6089
+ },
6090
+ recordCompleted(record) {
6091
+ completed.set(pairKey(record.sourceId, record.targetId), {
6092
+ ...record,
6093
+ completedAt: now()
6094
+ });
6095
+ },
6096
+ matchReplay(sourceId, targetId, atSource, atTarget) {
6097
+ const key = pairKey(sourceId, targetId);
6098
+ const record = completed.get(key);
6099
+ if (record === void 0) return null;
6100
+ if (now() - record.completedAt > 9e5) {
6101
+ completed.delete(key);
6102
+ return null;
6103
+ }
6104
+ if (!sameIdentity(record.identityAtSource, atSource) || !sameIdentity(record.identityAtTarget, atTarget)) return null;
6105
+ return record.result;
6106
+ }
6107
+ };
6108
+ }
5967
6109
  //#endregion
5968
6110
  //#region src/builtins/device-manager/device-manager.addon.ts
5969
6111
  var DeviceManagerAddon = class extends require_dist.BaseAddon {
@@ -6204,6 +6346,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
6204
6346
  settings,
6205
6347
  registry,
6206
6348
  bindingsDeps: this.bindingsDeps,
6349
+ migrationGuard: createMigrationGuard(),
6207
6350
  requireDeviceOps,
6208
6351
  stampIntegrationId: stampIntegrationId$1,
6209
6352
  get provider() {