camstack 1.2.84 → 1.2.85
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.
|
@@ -14522,7 +14522,7 @@ function date4(params) {
|
|
|
14522
14522
|
// ../../node_modules/zod/v4/classic/external.js
|
|
14523
14523
|
config(en_default());
|
|
14524
14524
|
|
|
14525
|
-
// ../types/dist/sleep-
|
|
14525
|
+
// ../types/dist/sleep-B195W080.mjs
|
|
14526
14526
|
var WELL_KNOWN_TABS = [
|
|
14527
14527
|
{
|
|
14528
14528
|
id: "overview",
|
|
@@ -20401,6 +20401,35 @@ var deviceProviderCapability = {
|
|
|
20401
20401
|
name: external_exports.string(),
|
|
20402
20402
|
type: external_exports.string()
|
|
20403
20403
|
}))),
|
|
20404
|
+
/**
|
|
20405
|
+
* Tear down and reconstruct ONE device in place from its persisted rows —
|
|
20406
|
+
* touching no other device this provider owns.
|
|
20407
|
+
*
|
|
20408
|
+
* The primitive `deviceManager.migrateDevice` uses to flush the two
|
|
20409
|
+
* migrated numbers: after `swapIds` the runner's live instance still
|
|
20410
|
+
* carries the PRE-swap numeric id (baked into the object, its native-cap
|
|
20411
|
+
* registrations and its log tags), and a live object cannot be renumbered.
|
|
20412
|
+
* Before this method the only flush was restarting the whole owning addon
|
|
20413
|
+
* — which took every camera the provider owns down with it (28 devices
|
|
20414
|
+
* for one migrated camera, measured 2026-09-04, and the morning of the
|
|
20415
|
+
* same day ~27 devices' native caps did not come back on their own).
|
|
20416
|
+
*
|
|
20417
|
+
* Keyed by `stableId`, deliberately: the numeric id is exactly the thing
|
|
20418
|
+
* that changes. The reply carries the id the device answers on NOW.
|
|
20419
|
+
* Implemented once in `BaseDeviceProvider` — decommission the live
|
|
20420
|
+
* instance (if any), then re-create from the persisted row: the same
|
|
20421
|
+
* teardown/rehydrate pair every graceful shutdown + boot already uses.
|
|
20422
|
+
* An RPC, never an event: a dropped event would leave the runner writing
|
|
20423
|
+
* against the wrong camera (D8).
|
|
20424
|
+
*
|
|
20425
|
+
* Construction can dial hardware, and the migrated source is
|
|
20426
|
+
* characteristically dead — the timeout covers a full activate window
|
|
20427
|
+
* rather than the 60 s default.
|
|
20428
|
+
*/
|
|
20429
|
+
reloadDevice: method(external_exports.object({ stableId: external_exports.string() }), external_exports.object({ deviceId: external_exports.number() }), {
|
|
20430
|
+
kind: "mutation",
|
|
20431
|
+
timeoutMs: 3 * 6e4
|
|
20432
|
+
}),
|
|
20404
20433
|
supportsDiscovery: method(external_exports.object({}), external_exports.boolean()),
|
|
20405
20434
|
/**
|
|
20406
20435
|
* Run a network scan. `params` carries optional provider-specific scan
|
|
@@ -20721,13 +20750,21 @@ var deviceManagerCapability = {
|
|
|
20721
20750
|
* `sourceStillLive` names what stayed live; empty is the only value that
|
|
20722
20751
|
* means the old hardware is quiet. The migration proceeds either way —
|
|
20723
20752
|
* refusing would refuse the case this exists for.
|
|
20753
|
+
*
|
|
20754
|
+
* **Safe to retry.** A duplicate call while a migration involving either
|
|
20755
|
+
* device is in flight is REFUSED, never queued; a repeat of a swap that
|
|
20756
|
+
* already completed (matched against the `addonId`/`stableId` fingerprint
|
|
20757
|
+
* the swap left on the rows, within 15 min) is a no-op returning the
|
|
20758
|
+
* original report. A blind re-run can no longer silently undo the
|
|
20759
|
+
* migration it retried.
|
|
20724
20760
|
*/
|
|
20725
20761
|
migrateDevice: method(external_exports.object({
|
|
20726
20762
|
sourceId: external_exports.number(),
|
|
20727
20763
|
targetId: external_exports.number()
|
|
20728
20764
|
}), MigrateDeviceResultSchema, {
|
|
20729
20765
|
kind: "mutation",
|
|
20730
|
-
auth: "admin"
|
|
20766
|
+
auth: "admin",
|
|
20767
|
+
timeoutMs: 12 * 6e4
|
|
20731
20768
|
}),
|
|
20732
20769
|
/** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
|
|
20733
20770
|
registerDevice: method(DeviceRegisterPayloadSchema, external_exports.void(), { kind: "mutation" }),
|
|
@@ -41144,6 +41181,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
41144
41181
|
addonId: null,
|
|
41145
41182
|
access: "view"
|
|
41146
41183
|
},
|
|
41184
|
+
"deviceProvider.reloadDevice": {
|
|
41185
|
+
capName: "device-provider",
|
|
41186
|
+
capScope: "system",
|
|
41187
|
+
addonId: null,
|
|
41188
|
+
access: "create"
|
|
41189
|
+
},
|
|
41147
41190
|
"deviceProvider.start": {
|
|
41148
41191
|
capName: "device-provider",
|
|
41149
41192
|
capScope: "system",
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runDiscover
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-Y3OL5WYZ.js";
|
|
5
5
|
import "./chunk-LMMQX4CK.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
@@ -38,7 +38,7 @@ async function runServe(args) {
|
|
|
38
38
|
...typeof values.data === "string" ? { data: values.data } : {}
|
|
39
39
|
};
|
|
40
40
|
Object.assign(process.env, buildServeEnv(opts));
|
|
41
|
-
await import("./launcher-
|
|
41
|
+
await import("./launcher-6T2UFXIG.js");
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/commands/agent.ts
|
|
@@ -83,7 +83,7 @@ async function runAgent(args) {
|
|
|
83
83
|
...typeof values.port === "string" ? { port: values.port } : {}
|
|
84
84
|
};
|
|
85
85
|
Object.assign(process.env, buildAgentEnv(opts));
|
|
86
|
-
await import("./launcher-
|
|
86
|
+
await import("./launcher-6T2UFXIG.js");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// src/commands/setup.ts
|
|
@@ -1130,7 +1130,7 @@ function isUnknown(_value) {
|
|
|
1130
1130
|
return true;
|
|
1131
1131
|
}
|
|
1132
1132
|
async function resolveServerInteractive(presetNamespace) {
|
|
1133
|
-
const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-
|
|
1133
|
+
const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-PPTEAP5N.js");
|
|
1134
1134
|
if (presetNamespace) {
|
|
1135
1135
|
const spinner4 = clack.spinner();
|
|
1136
1136
|
spinner4.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
|
|
@@ -23633,9 +23633,9 @@ var require_zod = __commonJS({
|
|
|
23633
23633
|
}
|
|
23634
23634
|
});
|
|
23635
23635
|
|
|
23636
|
-
// ../system/dist/dist-
|
|
23637
|
-
var
|
|
23638
|
-
"../system/dist/dist-
|
|
23636
|
+
// ../system/dist/dist-whe0b4_3.js
|
|
23637
|
+
var require_dist_whe0b4_3 = __commonJS({
|
|
23638
|
+
"../system/dist/dist-whe0b4_3.js"(exports) {
|
|
23639
23639
|
"use strict";
|
|
23640
23640
|
var zod = require_zod();
|
|
23641
23641
|
var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
|
|
@@ -31114,6 +31114,35 @@ var require_dist_TEwQZL3g = __commonJS({
|
|
|
31114
31114
|
name: zod.z.string(),
|
|
31115
31115
|
type: zod.z.string()
|
|
31116
31116
|
}))),
|
|
31117
|
+
/**
|
|
31118
|
+
* Tear down and reconstruct ONE device in place from its persisted rows —
|
|
31119
|
+
* touching no other device this provider owns.
|
|
31120
|
+
*
|
|
31121
|
+
* The primitive `deviceManager.migrateDevice` uses to flush the two
|
|
31122
|
+
* migrated numbers: after `swapIds` the runner's live instance still
|
|
31123
|
+
* carries the PRE-swap numeric id (baked into the object, its native-cap
|
|
31124
|
+
* registrations and its log tags), and a live object cannot be renumbered.
|
|
31125
|
+
* Before this method the only flush was restarting the whole owning addon
|
|
31126
|
+
* — which took every camera the provider owns down with it (28 devices
|
|
31127
|
+
* for one migrated camera, measured 2026-09-04, and the morning of the
|
|
31128
|
+
* same day ~27 devices' native caps did not come back on their own).
|
|
31129
|
+
*
|
|
31130
|
+
* Keyed by `stableId`, deliberately: the numeric id is exactly the thing
|
|
31131
|
+
* that changes. The reply carries the id the device answers on NOW.
|
|
31132
|
+
* Implemented once in `BaseDeviceProvider` — decommission the live
|
|
31133
|
+
* instance (if any), then re-create from the persisted row: the same
|
|
31134
|
+
* teardown/rehydrate pair every graceful shutdown + boot already uses.
|
|
31135
|
+
* An RPC, never an event: a dropped event would leave the runner writing
|
|
31136
|
+
* against the wrong camera (D8).
|
|
31137
|
+
*
|
|
31138
|
+
* Construction can dial hardware, and the migrated source is
|
|
31139
|
+
* characteristically dead — the timeout covers a full activate window
|
|
31140
|
+
* rather than the 60 s default.
|
|
31141
|
+
*/
|
|
31142
|
+
reloadDevice: method(zod.z.object({ stableId: zod.z.string() }), zod.z.object({ deviceId: zod.z.number() }), {
|
|
31143
|
+
kind: "mutation",
|
|
31144
|
+
timeoutMs: 3 * 6e4
|
|
31145
|
+
}),
|
|
31117
31146
|
supportsDiscovery: method(zod.z.object({}), zod.z.boolean()),
|
|
31118
31147
|
/**
|
|
31119
31148
|
* Run a network scan. `params` carries optional provider-specific scan
|
|
@@ -31434,13 +31463,21 @@ var require_dist_TEwQZL3g = __commonJS({
|
|
|
31434
31463
|
* `sourceStillLive` names what stayed live; empty is the only value that
|
|
31435
31464
|
* means the old hardware is quiet. The migration proceeds either way —
|
|
31436
31465
|
* refusing would refuse the case this exists for.
|
|
31466
|
+
*
|
|
31467
|
+
* **Safe to retry.** A duplicate call while a migration involving either
|
|
31468
|
+
* device is in flight is REFUSED, never queued; a repeat of a swap that
|
|
31469
|
+
* already completed (matched against the `addonId`/`stableId` fingerprint
|
|
31470
|
+
* the swap left on the rows, within 15 min) is a no-op returning the
|
|
31471
|
+
* original report. A blind re-run can no longer silently undo the
|
|
31472
|
+
* migration it retried.
|
|
31437
31473
|
*/
|
|
31438
31474
|
migrateDevice: method(zod.z.object({
|
|
31439
31475
|
sourceId: zod.z.number(),
|
|
31440
31476
|
targetId: zod.z.number()
|
|
31441
31477
|
}), MigrateDeviceResultSchema, {
|
|
31442
31478
|
kind: "mutation",
|
|
31443
|
-
auth: "admin"
|
|
31479
|
+
auth: "admin",
|
|
31480
|
+
timeoutMs: 12 * 6e4
|
|
31444
31481
|
}),
|
|
31445
31482
|
/** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
|
|
31446
31483
|
registerDevice: method(DeviceRegisterPayloadSchema, zod.z.void(), { kind: "mutation" }),
|
|
@@ -52508,6 +52545,12 @@ var require_dist_TEwQZL3g = __commonJS({
|
|
|
52508
52545
|
addonId: null,
|
|
52509
52546
|
access: "view"
|
|
52510
52547
|
},
|
|
52548
|
+
"deviceProvider.reloadDevice": {
|
|
52549
|
+
capName: "device-provider",
|
|
52550
|
+
capScope: "system",
|
|
52551
|
+
addonId: null,
|
|
52552
|
+
access: "create"
|
|
52553
|
+
},
|
|
52511
52554
|
"deviceProvider.start": {
|
|
52512
52555
|
capName: "device-provider",
|
|
52513
52556
|
capScope: "system",
|
|
@@ -59999,7 +60042,7 @@ var require_alerts_addon = __commonJS({
|
|
|
59999
60042
|
[Symbol.toStringTag]: { value: "Module" }
|
|
60000
60043
|
});
|
|
60001
60044
|
require_chunk_Cek0wNdY();
|
|
60002
|
-
var require_dist10 =
|
|
60045
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
60003
60046
|
function selectExpired(alerts, cutoffMs) {
|
|
60004
60047
|
return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
|
|
60005
60048
|
}
|
|
@@ -60818,7 +60861,7 @@ var require_console_logging = __commonJS({
|
|
|
60818
60861
|
[Symbol.toStringTag]: { value: "Module" }
|
|
60819
60862
|
});
|
|
60820
60863
|
require_chunk_Cek0wNdY();
|
|
60821
|
-
var require_dist10 =
|
|
60864
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
60822
60865
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
60823
60866
|
var LEVEL_RANK = {
|
|
60824
60867
|
debug: 0,
|
|
@@ -60912,7 +60955,7 @@ var require_core_blocks_addon = __commonJS({
|
|
|
60912
60955
|
"use strict";
|
|
60913
60956
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
60914
60957
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
60915
|
-
var require_dist10 =
|
|
60958
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
60916
60959
|
var node_crypto = __require("crypto");
|
|
60917
60960
|
var node_fs_promises = __require("fs/promises");
|
|
60918
60961
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -61809,11 +61852,11 @@ var require_core_blocks = __commonJS({
|
|
|
61809
61852
|
}
|
|
61810
61853
|
});
|
|
61811
61854
|
|
|
61812
|
-
// ../system/dist/retired-settings-keys-
|
|
61813
|
-
var
|
|
61814
|
-
"../system/dist/retired-settings-keys-
|
|
61855
|
+
// ../system/dist/retired-settings-keys-BdvmiaK7.js
|
|
61856
|
+
var require_retired_settings_keys_BdvmiaK7 = __commonJS({
|
|
61857
|
+
"../system/dist/retired-settings-keys-BdvmiaK7.js"(exports) {
|
|
61815
61858
|
"use strict";
|
|
61816
|
-
var require_dist10 =
|
|
61859
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
61817
61860
|
function settingsStoreIsAuthoritativeHere(env) {
|
|
61818
61861
|
const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
|
|
61819
61862
|
return raw === "" || raw === "hub";
|
|
@@ -64027,8 +64070,8 @@ var require_device_manager_addon = __commonJS({
|
|
|
64027
64070
|
[Symbol.toStringTag]: { value: "Module" }
|
|
64028
64071
|
});
|
|
64029
64072
|
require_chunk_Cek0wNdY();
|
|
64030
|
-
var require_dist10 =
|
|
64031
|
-
var require_retired_settings_keys =
|
|
64073
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
64074
|
+
var require_retired_settings_keys = require_retired_settings_keys_BdvmiaK7();
|
|
64032
64075
|
var node_crypto = __require("crypto");
|
|
64033
64076
|
var _camstack_types_node = require_node();
|
|
64034
64077
|
var JOB_HISTORY = 20;
|
|
@@ -66527,10 +66570,26 @@ var require_device_manager_addon = __commonJS({
|
|
|
66527
66570
|
detail
|
|
66528
66571
|
};
|
|
66529
66572
|
}
|
|
66573
|
+
async function setSwitchBounded(deps, deviceId, switchId, enabled) {
|
|
66574
|
+
const budget = deps.switchTimeoutMs ?? 15e3;
|
|
66575
|
+
const write = deps.setSwitch(deviceId, switchId, enabled);
|
|
66576
|
+
write.catch(() => {
|
|
66577
|
+
});
|
|
66578
|
+
let timer;
|
|
66579
|
+
try {
|
|
66580
|
+
await Promise.race([write, new Promise((_resolve, reject) => {
|
|
66581
|
+
timer = setTimeout(() => {
|
|
66582
|
+
reject(/* @__PURE__ */ new Error(`switch write did not answer within ${budget}ms \u2014 reported unreachable; if it lands late, a duplicate write of the same value is a no-op`));
|
|
66583
|
+
}, budget);
|
|
66584
|
+
})]);
|
|
66585
|
+
} finally {
|
|
66586
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
66587
|
+
}
|
|
66588
|
+
}
|
|
66530
66589
|
async function applyAll(deps, deviceId, enabled) {
|
|
66531
66590
|
const out = [];
|
|
66532
66591
|
for (const switchId of require_dist10.CAMERA_SWITCH_ORDER) try {
|
|
66533
|
-
await deps
|
|
66592
|
+
await setSwitchBounded(deps, deviceId, switchId, enabled);
|
|
66534
66593
|
out.push({
|
|
66535
66594
|
deviceId,
|
|
66536
66595
|
switchId,
|
|
@@ -66571,7 +66630,6 @@ var require_device_manager_addon = __commonJS({
|
|
|
66571
66630
|
});
|
|
66572
66631
|
}
|
|
66573
66632
|
const up = await applyAll(deps, sourceId, true);
|
|
66574
|
-
await deps.respawnRunners([sourceId, targetId]);
|
|
66575
66633
|
deps.log("camera migrated", {
|
|
66576
66634
|
sourceId,
|
|
66577
66635
|
targetId,
|
|
@@ -66586,6 +66644,50 @@ var require_device_manager_addon = __commonJS({
|
|
|
66586
66644
|
swapped: true
|
|
66587
66645
|
};
|
|
66588
66646
|
}
|
|
66647
|
+
async function reloadMigratedDevices(pctx, targets) {
|
|
66648
|
+
const ctx = pctx.host.ctx;
|
|
66649
|
+
for (const target of targets) {
|
|
66650
|
+
pctx.metaStore.idToAddonId.set(target.deviceId, target.addonId);
|
|
66651
|
+
pctx.host.remoteNativeCaps.delete(target.deviceId);
|
|
66652
|
+
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(target.deviceId);
|
|
66653
|
+
}
|
|
66654
|
+
const fallbackAddons = /* @__PURE__ */ new Map();
|
|
66655
|
+
await Promise.all(targets.map(async (target) => {
|
|
66656
|
+
try {
|
|
66657
|
+
const reloaded = await (await pctx.host.requireDeviceProvider(target.addonId)).reloadDevice({ stableId: target.stableId });
|
|
66658
|
+
ctx.logger.info("migration: device reloaded in place \u2014 no other device disturbed", {
|
|
66659
|
+
tags: { deviceId: target.deviceId },
|
|
66660
|
+
meta: {
|
|
66661
|
+
addonId: target.addonId,
|
|
66662
|
+
stableId: target.stableId,
|
|
66663
|
+
reloadedAs: reloaded.deviceId
|
|
66664
|
+
}
|
|
66665
|
+
});
|
|
66666
|
+
} catch (err) {
|
|
66667
|
+
ctx.logger.error("migration: in-place reload FAILED \u2014 falling back to a provider restart", {
|
|
66668
|
+
tags: { deviceId: target.deviceId },
|
|
66669
|
+
meta: {
|
|
66670
|
+
addonId: target.addonId,
|
|
66671
|
+
stableId: target.stableId,
|
|
66672
|
+
error: err instanceof Error ? err.message : String(err)
|
|
66673
|
+
}
|
|
66674
|
+
});
|
|
66675
|
+
fallbackAddons.set(target.addonId, target.deviceId);
|
|
66676
|
+
}
|
|
66677
|
+
}));
|
|
66678
|
+
if (fallbackAddons.size === 0) return;
|
|
66679
|
+
await Promise.all([...fallbackAddons.entries()].map(async ([addonId, migratedDeviceId]) => {
|
|
66680
|
+
const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
|
|
66681
|
+
ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
|
|
66682
|
+
tags: { deviceId: migratedDeviceId },
|
|
66683
|
+
meta: {
|
|
66684
|
+
addonId,
|
|
66685
|
+
devicesTakenDown: owned
|
|
66686
|
+
}
|
|
66687
|
+
});
|
|
66688
|
+
await ctx.api.addons.restartAddon.mutate({ addonId });
|
|
66689
|
+
}));
|
|
66690
|
+
}
|
|
66589
66691
|
var PROFILE_MAP_CONFIG_KEY = "_profileMap";
|
|
66590
66692
|
var ROLE_SCOPED_CONFIG_KEYS = new Set(DEVICE_STORE_SECTIONS.flatMap((section) => [...section.keys]));
|
|
66591
66693
|
function splitConfig(config) {
|
|
@@ -66674,68 +66776,110 @@ var require_device_manager_addon = __commonJS({
|
|
|
66674
66776
|
async function migrateDevice(pctx, input) {
|
|
66675
66777
|
const { sourceId, targetId } = input;
|
|
66676
66778
|
const ctx = pctx.host.ctx;
|
|
66677
|
-
|
|
66678
|
-
|
|
66679
|
-
const
|
|
66680
|
-
|
|
66681
|
-
|
|
66682
|
-
|
|
66683
|
-
|
|
66684
|
-
|
|
66685
|
-
|
|
66686
|
-
|
|
66687
|
-
},
|
|
66688
|
-
|
|
66689
|
-
|
|
66690
|
-
}
|
|
66691
|
-
|
|
66692
|
-
|
|
66693
|
-
|
|
66694
|
-
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
66695
|
-
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
66696
|
-
tags: { deviceId: a },
|
|
66779
|
+
pctx.migrationGuard.begin(sourceId, targetId);
|
|
66780
|
+
try {
|
|
66781
|
+
const outcome = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
66782
|
+
const sourceRow = await pctx.metaStore.rows.get(sourceId);
|
|
66783
|
+
if (sourceRow === null) throw new Error(`[device-manager] migrateDevice: device ${sourceId} does not exist`);
|
|
66784
|
+
const targetRow = await pctx.metaStore.rows.get(targetId);
|
|
66785
|
+
if (targetRow === null) throw new Error(`[device-manager] migrateDevice: device ${targetId} does not exist`);
|
|
66786
|
+
const replay = pctx.migrationGuard.matchReplay(sourceId, targetId, {
|
|
66787
|
+
addonId: sourceRow.meta.addonId,
|
|
66788
|
+
stableId: sourceRow.meta.stableId
|
|
66789
|
+
}, {
|
|
66790
|
+
addonId: targetRow.meta.addonId,
|
|
66791
|
+
stableId: targetRow.meta.stableId
|
|
66792
|
+
});
|
|
66793
|
+
if (replay !== null) {
|
|
66794
|
+
ctx.logger.info("migrateDevice: this swap already completed \u2014 replaying the original report, NOTHING re-run", {
|
|
66795
|
+
tags: { deviceId: sourceId },
|
|
66697
66796
|
meta: {
|
|
66698
|
-
sourceId
|
|
66699
|
-
targetId
|
|
66700
|
-
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
66701
|
-
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
66702
|
-
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
66703
|
-
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
66797
|
+
sourceId,
|
|
66798
|
+
targetId
|
|
66704
66799
|
}
|
|
66705
66800
|
});
|
|
66706
|
-
|
|
66707
|
-
|
|
66708
|
-
|
|
66709
|
-
|
|
66710
|
-
|
|
66711
|
-
|
|
66712
|
-
|
|
66713
|
-
|
|
66714
|
-
if (
|
|
66715
|
-
|
|
66716
|
-
|
|
66717
|
-
|
|
66718
|
-
|
|
66719
|
-
|
|
66801
|
+
return {
|
|
66802
|
+
result: replay,
|
|
66803
|
+
reloadTargets: null
|
|
66804
|
+
};
|
|
66805
|
+
}
|
|
66806
|
+
const tempId = await pctx.metaStore.allocateNextDeviceId();
|
|
66807
|
+
const result = await migrateDevice$1({
|
|
66808
|
+
setSwitch: async (deviceId, switchId, enabled) => {
|
|
66809
|
+
if (switchId === "stream-broker") {
|
|
66810
|
+
const moved = await writeDisabledFlagUnderLock(pctx, deviceId, !enabled);
|
|
66811
|
+
emitDisabledEvents(pctx, deviceId, !enabled, moved);
|
|
66812
|
+
return;
|
|
66813
|
+
}
|
|
66814
|
+
await ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
|
|
66815
|
+
deviceId,
|
|
66816
|
+
switchId,
|
|
66817
|
+
enabled
|
|
66818
|
+
});
|
|
66819
|
+
},
|
|
66820
|
+
swapIds: async (a, b) => {
|
|
66821
|
+
await pctx.metaStore.rows.swapIds(a, b, tempId);
|
|
66822
|
+
},
|
|
66823
|
+
swapHardwareState: async (a, b) => {
|
|
66824
|
+
const swapped = await swapHardwareState(pctx.settings, a, b);
|
|
66825
|
+
pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
|
|
66826
|
+
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
66827
|
+
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
66828
|
+
tags: { deviceId: a },
|
|
66720
66829
|
meta: {
|
|
66721
|
-
|
|
66722
|
-
|
|
66830
|
+
sourceId: a,
|
|
66831
|
+
targetId: b,
|
|
66832
|
+
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
66833
|
+
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
66834
|
+
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
66835
|
+
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
66723
66836
|
}
|
|
66724
66837
|
});
|
|
66725
|
-
|
|
66838
|
+
},
|
|
66839
|
+
forgetStreamState: async (deviceId) => {
|
|
66840
|
+
await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
|
|
66841
|
+
},
|
|
66842
|
+
log: (message, meta) => {
|
|
66843
|
+
ctx.logger.info(message, {
|
|
66844
|
+
tags: { deviceId: sourceId },
|
|
66845
|
+
meta
|
|
66846
|
+
});
|
|
66726
66847
|
}
|
|
66727
|
-
},
|
|
66728
|
-
|
|
66729
|
-
|
|
66730
|
-
|
|
66731
|
-
|
|
66732
|
-
|
|
66733
|
-
|
|
66734
|
-
|
|
66735
|
-
|
|
66736
|
-
|
|
66848
|
+
}, {
|
|
66849
|
+
sourceId,
|
|
66850
|
+
targetId
|
|
66851
|
+
});
|
|
66852
|
+
pctx.migrationGuard.recordCompleted({
|
|
66853
|
+
sourceId,
|
|
66854
|
+
targetId,
|
|
66855
|
+
identityAtSource: {
|
|
66856
|
+
addonId: targetRow.meta.addonId,
|
|
66857
|
+
stableId: targetRow.meta.stableId
|
|
66858
|
+
},
|
|
66859
|
+
identityAtTarget: {
|
|
66860
|
+
addonId: sourceRow.meta.addonId,
|
|
66861
|
+
stableId: sourceRow.meta.stableId
|
|
66862
|
+
},
|
|
66863
|
+
result
|
|
66864
|
+
});
|
|
66865
|
+
return {
|
|
66866
|
+
result,
|
|
66867
|
+
reloadTargets: [{
|
|
66868
|
+
deviceId: sourceId,
|
|
66869
|
+
addonId: targetRow.meta.addonId,
|
|
66870
|
+
stableId: targetRow.meta.stableId
|
|
66871
|
+
}, {
|
|
66872
|
+
deviceId: targetId,
|
|
66873
|
+
addonId: sourceRow.meta.addonId,
|
|
66874
|
+
stableId: sourceRow.meta.stableId
|
|
66875
|
+
}]
|
|
66876
|
+
};
|
|
66737
66877
|
});
|
|
66738
|
-
|
|
66878
|
+
if (outcome.reloadTargets !== null) await reloadMigratedDevices(pctx, outcome.reloadTargets);
|
|
66879
|
+
return outcome.result;
|
|
66880
|
+
} finally {
|
|
66881
|
+
pctx.migrationGuard.end(sourceId, targetId);
|
|
66882
|
+
}
|
|
66739
66883
|
}
|
|
66740
66884
|
async function registerDevice(pctx, input) {
|
|
66741
66885
|
const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
|
|
@@ -67256,18 +67400,17 @@ var require_device_manager_addon = __commonJS({
|
|
|
67256
67400
|
}
|
|
67257
67401
|
});
|
|
67258
67402
|
}
|
|
67259
|
-
async function
|
|
67260
|
-
const
|
|
67261
|
-
|
|
67262
|
-
|
|
67263
|
-
|
|
67264
|
-
|
|
67265
|
-
|
|
67266
|
-
|
|
67267
|
-
|
|
67268
|
-
|
|
67269
|
-
|
|
67270
|
-
});
|
|
67403
|
+
async function writeDisabledFlagUnderLock(pctx, deviceId, disabled) {
|
|
67404
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
67405
|
+
if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
|
|
67406
|
+
const { meta: m } = persisted;
|
|
67407
|
+
await pctx.metaStore.rows.patch(deviceId, { disabled });
|
|
67408
|
+
return {
|
|
67409
|
+
changed: (m.disabled ?? false) !== disabled,
|
|
67410
|
+
integrationId: m.integrationId ?? ""
|
|
67411
|
+
};
|
|
67412
|
+
}
|
|
67413
|
+
function emitDisabledEvents(pctx, deviceId, disabled, moved) {
|
|
67271
67414
|
pctx.host.ctx.eventBus.emit({
|
|
67272
67415
|
id: (0, node_crypto.randomUUID)(),
|
|
67273
67416
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -67297,6 +67440,10 @@ var require_device_manager_addon = __commonJS({
|
|
|
67297
67440
|
}
|
|
67298
67441
|
});
|
|
67299
67442
|
}
|
|
67443
|
+
async function setDisabled(pctx, input) {
|
|
67444
|
+
const { deviceId, disabled } = input;
|
|
67445
|
+
emitDisabledEvents(pctx, deviceId, disabled, await pctx.metaStore.withMetaWriteLock(() => writeDisabledFlagUnderLock(pctx, deviceId, disabled)));
|
|
67446
|
+
}
|
|
67300
67447
|
async function loadRuntimeState(pctx, input) {
|
|
67301
67448
|
const { deviceId } = input;
|
|
67302
67449
|
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
|
|
@@ -68677,6 +68824,42 @@ var require_device_manager_addon = __commonJS({
|
|
|
68677
68824
|
};
|
|
68678
68825
|
return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
|
|
68679
68826
|
}
|
|
68827
|
+
function sameIdentity(a, b) {
|
|
68828
|
+
return a.addonId === b.addonId && a.stableId === b.stableId;
|
|
68829
|
+
}
|
|
68830
|
+
function createMigrationGuard(now = Date.now) {
|
|
68831
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
68832
|
+
const completed = /* @__PURE__ */ new Map();
|
|
68833
|
+
const pairKey = (sourceId, targetId) => `${sourceId}->${targetId}`;
|
|
68834
|
+
return {
|
|
68835
|
+
begin(sourceId, targetId) {
|
|
68836
|
+
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 \u2014 this call was refused, not queued. The running migration's report stands; re-read the device rows before retrying.`);
|
|
68837
|
+
inFlight.add(sourceId);
|
|
68838
|
+
inFlight.add(targetId);
|
|
68839
|
+
},
|
|
68840
|
+
end(sourceId, targetId) {
|
|
68841
|
+
inFlight.delete(sourceId);
|
|
68842
|
+
inFlight.delete(targetId);
|
|
68843
|
+
},
|
|
68844
|
+
recordCompleted(record) {
|
|
68845
|
+
completed.set(pairKey(record.sourceId, record.targetId), {
|
|
68846
|
+
...record,
|
|
68847
|
+
completedAt: now()
|
|
68848
|
+
});
|
|
68849
|
+
},
|
|
68850
|
+
matchReplay(sourceId, targetId, atSource, atTarget) {
|
|
68851
|
+
const key = pairKey(sourceId, targetId);
|
|
68852
|
+
const record = completed.get(key);
|
|
68853
|
+
if (record === void 0) return null;
|
|
68854
|
+
if (now() - record.completedAt > 9e5) {
|
|
68855
|
+
completed.delete(key);
|
|
68856
|
+
return null;
|
|
68857
|
+
}
|
|
68858
|
+
if (!sameIdentity(record.identityAtSource, atSource) || !sameIdentity(record.identityAtTarget, atTarget)) return null;
|
|
68859
|
+
return record.result;
|
|
68860
|
+
}
|
|
68861
|
+
};
|
|
68862
|
+
}
|
|
68680
68863
|
var DeviceManagerAddon = class extends require_dist10.BaseAddon {
|
|
68681
68864
|
constructor() {
|
|
68682
68865
|
super({});
|
|
@@ -68915,6 +69098,7 @@ var require_device_manager_addon = __commonJS({
|
|
|
68915
69098
|
settings,
|
|
68916
69099
|
registry,
|
|
68917
69100
|
bindingsDeps: this.bindingsDeps,
|
|
69101
|
+
migrationGuard: createMigrationGuard(),
|
|
68918
69102
|
requireDeviceOps,
|
|
68919
69103
|
stampIntegrationId: stampIntegrationId$1,
|
|
68920
69104
|
get provider() {
|
|
@@ -69156,7 +69340,7 @@ var require_hub_forwarder = __commonJS({
|
|
|
69156
69340
|
[Symbol.toStringTag]: { value: "Module" }
|
|
69157
69341
|
});
|
|
69158
69342
|
require_chunk_Cek0wNdY();
|
|
69159
|
-
var require_dist10 =
|
|
69343
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
69160
69344
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
69161
69345
|
var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
|
|
69162
69346
|
var HubForwarderDestination = class {
|
|
@@ -69293,7 +69477,7 @@ var require_liveness_monitor_addon = __commonJS({
|
|
|
69293
69477
|
"use strict";
|
|
69294
69478
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
69295
69479
|
require_chunk_Cek0wNdY();
|
|
69296
|
-
var require_dist10 =
|
|
69480
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
69297
69481
|
var NO_DEVICES = "liveness:no-devices";
|
|
69298
69482
|
var ALL_OFFLINE = "liveness:all-devices-offline";
|
|
69299
69483
|
var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
|
|
@@ -69483,7 +69667,7 @@ var require_local_auth_addon = __commonJS({
|
|
|
69483
69667
|
[Symbol.toStringTag]: { value: "Module" }
|
|
69484
69668
|
});
|
|
69485
69669
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
69486
|
-
var require_dist10 =
|
|
69670
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
69487
69671
|
var node_crypto = __require("crypto");
|
|
69488
69672
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
69489
69673
|
var crypto$1 = __require("crypto");
|
|
@@ -77296,7 +77480,7 @@ var require_loki_logging = __commonJS({
|
|
|
77296
77480
|
[Symbol.toStringTag]: { value: "Module" }
|
|
77297
77481
|
});
|
|
77298
77482
|
require_chunk_Cek0wNdY();
|
|
77299
|
-
var require_dist10 =
|
|
77483
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
77300
77484
|
function sanitizeLabelName(raw) {
|
|
77301
77485
|
const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
|
|
77302
77486
|
return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
|
|
@@ -77861,7 +78045,7 @@ var require_native_metrics_addon = __commonJS({
|
|
|
77861
78045
|
[Symbol.toStringTag]: { value: "Module" }
|
|
77862
78046
|
});
|
|
77863
78047
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
77864
|
-
var require_dist10 =
|
|
78048
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
77865
78049
|
var node_fs_promises = __require("fs/promises");
|
|
77866
78050
|
var node_child_process = __require("child_process");
|
|
77867
78051
|
var node_util = __require("util");
|
|
@@ -80445,7 +80629,7 @@ var require_filesystem_storage_addon = __commonJS({
|
|
|
80445
80629
|
[Symbol.toStringTag]: { value: "Module" }
|
|
80446
80630
|
});
|
|
80447
80631
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
80448
|
-
var require_dist10 =
|
|
80632
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
80449
80633
|
var node_crypto = __require("crypto");
|
|
80450
80634
|
var node_fs_promises = __require("fs/promises");
|
|
80451
80635
|
var node_path = __require("path");
|
|
@@ -81561,8 +81745,8 @@ var require_sqlite_settings_addon = __commonJS({
|
|
|
81561
81745
|
[Symbol.toStringTag]: { value: "Module" }
|
|
81562
81746
|
});
|
|
81563
81747
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
81564
|
-
var require_dist10 =
|
|
81565
|
-
var require_retired_settings_keys =
|
|
81748
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
81749
|
+
var require_retired_settings_keys = require_retired_settings_keys_BdvmiaK7();
|
|
81566
81750
|
var node_crypto = __require("crypto");
|
|
81567
81751
|
var node_fs = __require("fs");
|
|
81568
81752
|
var node_module = __require("module");
|
|
@@ -84052,7 +84236,7 @@ var require_storage_orchestrator_addon = __commonJS({
|
|
|
84052
84236
|
[Symbol.toStringTag]: { value: "Module" }
|
|
84053
84237
|
});
|
|
84054
84238
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
84055
|
-
var require_dist10 =
|
|
84239
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
84056
84240
|
var require_hub_cap_forward = require_hub_cap_forward_DmHNjbB0();
|
|
84057
84241
|
var zod = require_zod();
|
|
84058
84242
|
var node_crypto = __require("crypto");
|
|
@@ -87003,7 +87187,7 @@ var require_system_config_addon = __commonJS({
|
|
|
87003
87187
|
[Symbol.toStringTag]: { value: "Module" }
|
|
87004
87188
|
});
|
|
87005
87189
|
require_chunk_Cek0wNdY();
|
|
87006
|
-
var require_dist10 =
|
|
87190
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
87007
87191
|
var SECTION_TITLES = {
|
|
87008
87192
|
server: "Server",
|
|
87009
87193
|
auth: "Authentication"
|
|
@@ -105064,7 +105248,7 @@ var require_winston_logging = __commonJS({
|
|
|
105064
105248
|
[Symbol.toStringTag]: { value: "Module" }
|
|
105065
105249
|
});
|
|
105066
105250
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
105067
|
-
var require_dist10 =
|
|
105251
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
105068
105252
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
105069
105253
|
var node_path = __require("path");
|
|
105070
105254
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -107007,9 +107191,9 @@ var require_event_category_BaEgqJNv = __commonJS({
|
|
|
107007
107191
|
}
|
|
107008
107192
|
});
|
|
107009
107193
|
|
|
107010
|
-
// ../types/dist/sleep-
|
|
107011
|
-
var
|
|
107012
|
-
"../types/dist/sleep-
|
|
107194
|
+
// ../types/dist/sleep-jdpPltQH.js
|
|
107195
|
+
var require_sleep_jdpPltQH = __commonJS({
|
|
107196
|
+
"../types/dist/sleep-jdpPltQH.js"(exports) {
|
|
107013
107197
|
"use strict";
|
|
107014
107198
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
107015
107199
|
var zod = require_zod();
|
|
@@ -109933,6 +110117,7 @@ var require_sleep_BDmIj1HV = __commonJS({
|
|
|
109933
110117
|
getDeviceLiveContribution: (input) => dispatchSystem("deviceManager", "getDeviceLiveContribution", "query", input),
|
|
109934
110118
|
applyDeviceSettingsPatch: (input) => dispatchSystem("deviceManager", "applyDeviceSettingsPatch", "mutation", input)
|
|
109935
110119
|
},
|
|
110120
|
+
deviceProvider: { reloadDevice: (input) => dispatchSystem("deviceProvider", "reloadDevice", "mutation", input) },
|
|
109936
110121
|
deviceState: {
|
|
109937
110122
|
getSnapshot: (input) => dispatchSystem("deviceState", "getSnapshot", "query", input),
|
|
109938
110123
|
getCapSlice: (input) => dispatchSystem("deviceState", "getCapSlice", "query", input),
|
|
@@ -110682,7 +110867,7 @@ var require_addon = __commonJS({
|
|
|
110682
110867
|
"use strict";
|
|
110683
110868
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
110684
110869
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
110685
|
-
var require_sleep =
|
|
110870
|
+
var require_sleep = require_sleep_jdpPltQH();
|
|
110686
110871
|
var require_err_msg = require_err_msg_COpsHMw2();
|
|
110687
110872
|
var CAP_INPUT_DEFAULTS = Object.freeze({
|
|
110688
110873
|
"addons": { "getLogs": { "limit": 100 } },
|
|
@@ -117565,12 +117750,12 @@ var require_dist2 = __commonJS({
|
|
|
117565
117750
|
}
|
|
117566
117751
|
});
|
|
117567
117752
|
|
|
117568
|
-
// ../system/dist/manifest-system-deps-
|
|
117569
|
-
var
|
|
117570
|
-
"../system/dist/manifest-system-deps-
|
|
117753
|
+
// ../system/dist/manifest-system-deps-y5vZBO6q.js
|
|
117754
|
+
var require_manifest_system_deps_y5vZBO6q = __commonJS({
|
|
117755
|
+
"../system/dist/manifest-system-deps-y5vZBO6q.js"(exports) {
|
|
117571
117756
|
"use strict";
|
|
117572
117757
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
117573
|
-
|
|
117758
|
+
require_dist_whe0b4_3();
|
|
117574
117759
|
require_hub_cap_forward_DmHNjbB0();
|
|
117575
117760
|
var node_crypto = __require("crypto");
|
|
117576
117761
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
@@ -129637,7 +129822,7 @@ var require_dist3 = __commonJS({
|
|
|
129637
129822
|
"use strict";
|
|
129638
129823
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
129639
129824
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
129640
|
-
var require_dist10 =
|
|
129825
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
129641
129826
|
var require_builtins_alerts_alerts_addon = require_alerts_addon();
|
|
129642
129827
|
require_alerts();
|
|
129643
129828
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
@@ -129664,7 +129849,7 @@ var require_dist3 = __commonJS({
|
|
|
129664
129849
|
var require_builtins_winston_logging_index = require_winston_logging();
|
|
129665
129850
|
var require_file_data_plane = require_file_data_plane_DO8KbxCe();
|
|
129666
129851
|
var require_tls$1 = require_tls_BxQlomxd();
|
|
129667
|
-
var require_manifest_system_deps =
|
|
129852
|
+
var require_manifest_system_deps = require_manifest_system_deps_y5vZBO6q();
|
|
129668
129853
|
var require_resource_monitor = require_resource_monitor_CdnzxBLP();
|
|
129669
129854
|
var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
|
|
129670
129855
|
var zod = require_zod();
|
|
@@ -210754,7 +210939,7 @@ var require_dist4 = __commonJS({
|
|
|
210754
210939
|
"use strict";
|
|
210755
210940
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
210756
210941
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
210757
|
-
var require_sleep =
|
|
210942
|
+
var require_sleep = require_sleep_jdpPltQH();
|
|
210758
210943
|
var require_canonical_hash = require_canonical_hash_DNV8S5ET();
|
|
210759
210944
|
var require_enums2 = require_enums();
|
|
210760
210945
|
var require_err_msg = require_err_msg_COpsHMw2();
|
|
@@ -218099,6 +218284,35 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
218099
218284
|
name: zod.z.string(),
|
|
218100
218285
|
type: zod.z.string()
|
|
218101
218286
|
}))),
|
|
218287
|
+
/**
|
|
218288
|
+
* Tear down and reconstruct ONE device in place from its persisted rows —
|
|
218289
|
+
* touching no other device this provider owns.
|
|
218290
|
+
*
|
|
218291
|
+
* The primitive `deviceManager.migrateDevice` uses to flush the two
|
|
218292
|
+
* migrated numbers: after `swapIds` the runner's live instance still
|
|
218293
|
+
* carries the PRE-swap numeric id (baked into the object, its native-cap
|
|
218294
|
+
* registrations and its log tags), and a live object cannot be renumbered.
|
|
218295
|
+
* Before this method the only flush was restarting the whole owning addon
|
|
218296
|
+
* — which took every camera the provider owns down with it (28 devices
|
|
218297
|
+
* for one migrated camera, measured 2026-09-04, and the morning of the
|
|
218298
|
+
* same day ~27 devices' native caps did not come back on their own).
|
|
218299
|
+
*
|
|
218300
|
+
* Keyed by `stableId`, deliberately: the numeric id is exactly the thing
|
|
218301
|
+
* that changes. The reply carries the id the device answers on NOW.
|
|
218302
|
+
* Implemented once in `BaseDeviceProvider` — decommission the live
|
|
218303
|
+
* instance (if any), then re-create from the persisted row: the same
|
|
218304
|
+
* teardown/rehydrate pair every graceful shutdown + boot already uses.
|
|
218305
|
+
* An RPC, never an event: a dropped event would leave the runner writing
|
|
218306
|
+
* against the wrong camera (D8).
|
|
218307
|
+
*
|
|
218308
|
+
* Construction can dial hardware, and the migrated source is
|
|
218309
|
+
* characteristically dead — the timeout covers a full activate window
|
|
218310
|
+
* rather than the 60 s default.
|
|
218311
|
+
*/
|
|
218312
|
+
reloadDevice: require_sleep.method(zod.z.object({ stableId: zod.z.string() }), zod.z.object({ deviceId: zod.z.number() }), {
|
|
218313
|
+
kind: "mutation",
|
|
218314
|
+
timeoutMs: 3 * 6e4
|
|
218315
|
+
}),
|
|
218102
218316
|
supportsDiscovery: require_sleep.method(zod.z.object({}), zod.z.boolean()),
|
|
218103
218317
|
/**
|
|
218104
218318
|
* Run a network scan. `params` carries optional provider-specific scan
|
|
@@ -218420,13 +218634,21 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
218420
218634
|
* `sourceStillLive` names what stayed live; empty is the only value that
|
|
218421
218635
|
* means the old hardware is quiet. The migration proceeds either way —
|
|
218422
218636
|
* refusing would refuse the case this exists for.
|
|
218637
|
+
*
|
|
218638
|
+
* **Safe to retry.** A duplicate call while a migration involving either
|
|
218639
|
+
* device is in flight is REFUSED, never queued; a repeat of a swap that
|
|
218640
|
+
* already completed (matched against the `addonId`/`stableId` fingerprint
|
|
218641
|
+
* the swap left on the rows, within 15 min) is a no-op returning the
|
|
218642
|
+
* original report. A blind re-run can no longer silently undo the
|
|
218643
|
+
* migration it retried.
|
|
218423
218644
|
*/
|
|
218424
218645
|
migrateDevice: require_sleep.method(zod.z.object({
|
|
218425
218646
|
sourceId: zod.z.number(),
|
|
218426
218647
|
targetId: zod.z.number()
|
|
218427
218648
|
}), MigrateDeviceResultSchema, {
|
|
218428
218649
|
kind: "mutation",
|
|
218429
|
-
auth: "admin"
|
|
218650
|
+
auth: "admin",
|
|
218651
|
+
timeoutMs: 12 * 6e4
|
|
218430
218652
|
}),
|
|
218431
218653
|
/** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
|
|
218432
218654
|
registerDevice: require_sleep.method(DeviceRegisterPayloadSchema, zod.z.void(), { kind: "mutation" }),
|
|
@@ -239111,6 +239333,138 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239111
239333
|
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
239112
239334
|
}
|
|
239113
239335
|
};
|
|
239336
|
+
var DEVICE_RESTORE_RETRY_DELAYS_MS = [
|
|
239337
|
+
1e4,
|
|
239338
|
+
3e4,
|
|
239339
|
+
9e4
|
|
239340
|
+
];
|
|
239341
|
+
var DEVICE_RESTORE_RETRY_CONCURRENCY = 4;
|
|
239342
|
+
function sleep$1(ms, signal) {
|
|
239343
|
+
return new Promise((resolve) => {
|
|
239344
|
+
if (signal.aborted) {
|
|
239345
|
+
resolve();
|
|
239346
|
+
return;
|
|
239347
|
+
}
|
|
239348
|
+
const onAbort = () => {
|
|
239349
|
+
clearTimeout(timer);
|
|
239350
|
+
resolve();
|
|
239351
|
+
};
|
|
239352
|
+
const timer = setTimeout(() => {
|
|
239353
|
+
signal.removeEventListener("abort", onAbort);
|
|
239354
|
+
resolve();
|
|
239355
|
+
}, ms);
|
|
239356
|
+
timer.unref?.();
|
|
239357
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
239358
|
+
});
|
|
239359
|
+
}
|
|
239360
|
+
async function runWithConcurrency(items, width, fn) {
|
|
239361
|
+
const queue = [...items];
|
|
239362
|
+
const laneCount = Math.max(1, Math.min(width, queue.length));
|
|
239363
|
+
const lane = async () => {
|
|
239364
|
+
for (; ; ) {
|
|
239365
|
+
const item = queue.shift();
|
|
239366
|
+
if (item === void 0) return;
|
|
239367
|
+
await fn(item);
|
|
239368
|
+
}
|
|
239369
|
+
};
|
|
239370
|
+
await Promise.all(Array.from({ length: laneCount }, lane));
|
|
239371
|
+
}
|
|
239372
|
+
var DeviceRestoreRetryScheduler = class {
|
|
239373
|
+
#logger;
|
|
239374
|
+
#attempt;
|
|
239375
|
+
#onPermanentFailure;
|
|
239376
|
+
#delaysMs;
|
|
239377
|
+
#concurrency;
|
|
239378
|
+
#now;
|
|
239379
|
+
#abort = new AbortController();
|
|
239380
|
+
constructor(options) {
|
|
239381
|
+
this.#logger = options.logger;
|
|
239382
|
+
this.#attempt = options.attempt;
|
|
239383
|
+
this.#onPermanentFailure = options.onPermanentFailure;
|
|
239384
|
+
this.#delaysMs = options.delaysMs ?? DEVICE_RESTORE_RETRY_DELAYS_MS;
|
|
239385
|
+
this.#concurrency = options.concurrency ?? 4;
|
|
239386
|
+
this.#now = options.now ?? Date.now;
|
|
239387
|
+
}
|
|
239388
|
+
/** Stop retrying (shutdown). Pending entries are NOT marked
|
|
239389
|
+
* permanently failed — the next boot restores them from disk. */
|
|
239390
|
+
cancel() {
|
|
239391
|
+
this.#abort.abort();
|
|
239392
|
+
}
|
|
239393
|
+
/**
|
|
239394
|
+
* Run the bounded retry rounds. Resolves when every entry has either
|
|
239395
|
+
* restored, been marked permanently failed, or the scheduler was
|
|
239396
|
+
* cancelled. Never rejects.
|
|
239397
|
+
*/
|
|
239398
|
+
async run(initialFailures) {
|
|
239399
|
+
let pending = initialFailures.map((failure) => ({
|
|
239400
|
+
saved: failure.saved,
|
|
239401
|
+
lastError: failure.error,
|
|
239402
|
+
attempts: 1
|
|
239403
|
+
}));
|
|
239404
|
+
for (let round = 0; round < this.#delaysMs.length; round += 1) {
|
|
239405
|
+
if (pending.length === 0 || this.#abort.signal.aborted) break;
|
|
239406
|
+
await sleep$1(this.#delaysMs[round] ?? 0, this.#abort.signal);
|
|
239407
|
+
if (this.#abort.signal.aborted) break;
|
|
239408
|
+
pending = await this.#runRound(pending, round);
|
|
239409
|
+
}
|
|
239410
|
+
if (this.#abort.signal.aborted) return [];
|
|
239411
|
+
const terminal = pending.map((entry) => ({
|
|
239412
|
+
deviceId: entry.saved.id,
|
|
239413
|
+
stableId: entry.saved.stableId,
|
|
239414
|
+
type: String(entry.saved.type),
|
|
239415
|
+
attempts: entry.attempts,
|
|
239416
|
+
lastError: entry.lastError,
|
|
239417
|
+
failedAt: this.#now()
|
|
239418
|
+
}));
|
|
239419
|
+
for (const failure of terminal) this.#onPermanentFailure(failure);
|
|
239420
|
+
return terminal;
|
|
239421
|
+
}
|
|
239422
|
+
/** One retry round: parents first (phase 0), then hub-adopted
|
|
239423
|
+
* children (phase 1) — a child's attempt depends on its parent
|
|
239424
|
+
* having landed, exactly like the initial two-pass restore. */
|
|
239425
|
+
async #runRound(pending, round) {
|
|
239426
|
+
const next = [];
|
|
239427
|
+
const parents = pending.filter((entry) => entry.saved.parentDeviceId === null);
|
|
239428
|
+
const children = pending.filter((entry) => entry.saved.parentDeviceId !== null);
|
|
239429
|
+
for (const phase of [parents, children]) await runWithConcurrency(phase, this.#concurrency, async (entry) => {
|
|
239430
|
+
if (this.#abort.signal.aborted) {
|
|
239431
|
+
next.push(entry);
|
|
239432
|
+
return;
|
|
239433
|
+
}
|
|
239434
|
+
const attemptNo = entry.attempts + 1;
|
|
239435
|
+
try {
|
|
239436
|
+
await this.#attempt(entry.saved);
|
|
239437
|
+
this.#logger.info("Device restored on retry", {
|
|
239438
|
+
tags: {
|
|
239439
|
+
deviceId: entry.saved.id,
|
|
239440
|
+
stableId: entry.saved.stableId
|
|
239441
|
+
},
|
|
239442
|
+
meta: { attempt: attemptNo }
|
|
239443
|
+
});
|
|
239444
|
+
} catch (err) {
|
|
239445
|
+
const lastError = err instanceof Error ? err.message : String(err);
|
|
239446
|
+
const remainingRetries = this.#delaysMs.length - (round + 1);
|
|
239447
|
+
this.#logger.warn("Device restore retry failed", {
|
|
239448
|
+
tags: {
|
|
239449
|
+
deviceId: entry.saved.id,
|
|
239450
|
+
stableId: entry.saved.stableId
|
|
239451
|
+
},
|
|
239452
|
+
meta: {
|
|
239453
|
+
attempt: attemptNo,
|
|
239454
|
+
remainingRetries,
|
|
239455
|
+
error: lastError
|
|
239456
|
+
}
|
|
239457
|
+
});
|
|
239458
|
+
next.push({
|
|
239459
|
+
saved: entry.saved,
|
|
239460
|
+
lastError,
|
|
239461
|
+
attempts: attemptNo
|
|
239462
|
+
});
|
|
239463
|
+
}
|
|
239464
|
+
});
|
|
239465
|
+
return next;
|
|
239466
|
+
}
|
|
239467
|
+
};
|
|
239114
239468
|
function toDeviceSummary(device2, addonId) {
|
|
239115
239469
|
const config = {};
|
|
239116
239470
|
for (const entry of device2.config.entries()) config[entry.key] = entry.value;
|
|
@@ -239136,6 +239490,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239136
239490
|
}];
|
|
239137
239491
|
}
|
|
239138
239492
|
async onShutdown() {
|
|
239493
|
+
this.cancelRestoreRetries();
|
|
239139
239494
|
const devices = await this.ctx.kernel.devices?.getAll() ?? [];
|
|
239140
239495
|
for (const device2 of devices) try {
|
|
239141
239496
|
await this.ctx.kernel.devices?.decommission(device2.id);
|
|
@@ -239155,9 +239510,16 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239155
239510
|
async stop() {
|
|
239156
239511
|
}
|
|
239157
239512
|
async getStatus() {
|
|
239513
|
+
const all = await this.ctx.kernel.devices?.getAll() ?? [];
|
|
239514
|
+
const summary = this.restoreFailureSummary();
|
|
239515
|
+
if (summary === null) return {
|
|
239516
|
+
connected: true,
|
|
239517
|
+
deviceCount: all.length
|
|
239518
|
+
};
|
|
239158
239519
|
return {
|
|
239159
239520
|
connected: true,
|
|
239160
|
-
deviceCount:
|
|
239521
|
+
deviceCount: all.length,
|
|
239522
|
+
error: summary
|
|
239161
239523
|
};
|
|
239162
239524
|
}
|
|
239163
239525
|
async getDevices() {
|
|
@@ -239247,8 +239609,137 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239247
239609
|
};
|
|
239248
239610
|
}
|
|
239249
239611
|
async restoreDevices(savedDevices) {
|
|
239250
|
-
await this.onRestoreDevices(savedDevices);
|
|
239251
|
-
if (savedDevices.length
|
|
239612
|
+
const report = await this.onRestoreDevices(savedDevices);
|
|
239613
|
+
if (savedDevices.length === 0) return;
|
|
239614
|
+
if (report && report.failedCount > 0) {
|
|
239615
|
+
this.ctx.logger.warn(`Restored ${report.restoredCount}/${savedDevices.length} ${this.providerName} device(s) \u2014 ${report.failedCount} failed, bounded retry scheduled`);
|
|
239616
|
+
return;
|
|
239617
|
+
}
|
|
239618
|
+
const restoredCount = report ? report.restoredCount : savedDevices.length;
|
|
239619
|
+
this.ctx.logger.info(`Restored ${restoredCount} ${this.providerName} device(s)`);
|
|
239620
|
+
}
|
|
239621
|
+
/** Retry schedule. Overridable (tests use millisecond delays). */
|
|
239622
|
+
restoreRetryDelaysMs = DEVICE_RESTORE_RETRY_DELAYS_MS;
|
|
239623
|
+
/** Retry lane width. See `device-restore-retry.ts` for why retries
|
|
239624
|
+
* never re-stampede full-width while the initial pass does (D167). */
|
|
239625
|
+
restoreRetryConcurrency = 4;
|
|
239626
|
+
_restoreRetryScheduler = null;
|
|
239627
|
+
_restoreRetryCompletion = null;
|
|
239628
|
+
_permanentRestoreFailures = /* @__PURE__ */ new Map();
|
|
239629
|
+
/** Settles when the background retry rounds finish (or `null` when
|
|
239630
|
+
* nothing failed). Exposed for tests and subclass diagnostics —
|
|
239631
|
+
* boot NEVER awaits this: the runner's post-init handshake goes out
|
|
239632
|
+
* with the devices that restored, and a late success is announced
|
|
239633
|
+
* through the `native-cap-change` → `updateCaps` path. */
|
|
239634
|
+
get restoreRetryCompletion() {
|
|
239635
|
+
return this._restoreRetryCompletion;
|
|
239636
|
+
}
|
|
239637
|
+
/** Devices that exhausted the retry bound this process lifetime. */
|
|
239638
|
+
get permanentRestoreFailures() {
|
|
239639
|
+
return [...this._permanentRestoreFailures.values()];
|
|
239640
|
+
}
|
|
239641
|
+
/** One-line operator-facing summary for `getStatus().error`, or
|
|
239642
|
+
* `null` when every device restored. */
|
|
239643
|
+
restoreFailureSummary() {
|
|
239644
|
+
if (this._permanentRestoreFailures.size === 0) return null;
|
|
239645
|
+
const ids = [...this._permanentRestoreFailures.keys()].join(", ");
|
|
239646
|
+
return `${this._permanentRestoreFailures.size} device(s) permanently failed restore (deviceIds: ${ids}) \u2014 restart the ${this.providerName} provider to retry`;
|
|
239647
|
+
}
|
|
239648
|
+
cancelRestoreRetries() {
|
|
239649
|
+
this._restoreRetryScheduler?.cancel();
|
|
239650
|
+
this._restoreRetryScheduler = null;
|
|
239651
|
+
}
|
|
239652
|
+
recordPermanentRestoreFailure(failure) {
|
|
239653
|
+
this._permanentRestoreFailures.set(failure.deviceId, failure);
|
|
239654
|
+
this.ctx.logger.error("Device restore permanently failed \u2014 its capabilities will not register until the provider restarts", {
|
|
239655
|
+
tags: {
|
|
239656
|
+
deviceId: failure.deviceId,
|
|
239657
|
+
stableId: failure.stableId
|
|
239658
|
+
},
|
|
239659
|
+
meta: {
|
|
239660
|
+
type: failure.type,
|
|
239661
|
+
attempts: failure.attempts,
|
|
239662
|
+
error: failure.lastError
|
|
239663
|
+
}
|
|
239664
|
+
});
|
|
239665
|
+
}
|
|
239666
|
+
scheduleRestoreRetries(failures, attempt) {
|
|
239667
|
+
const scheduler2 = new DeviceRestoreRetryScheduler({
|
|
239668
|
+
logger: this.ctx.logger,
|
|
239669
|
+
delaysMs: this.restoreRetryDelaysMs,
|
|
239670
|
+
concurrency: this.restoreRetryConcurrency,
|
|
239671
|
+
attempt,
|
|
239672
|
+
onPermanentFailure: (failure) => this.recordPermanentRestoreFailure(failure)
|
|
239673
|
+
});
|
|
239674
|
+
this._restoreRetryScheduler = scheduler2;
|
|
239675
|
+
this._restoreRetryCompletion = scheduler2.run(failures).then(() => void 0).catch((err) => {
|
|
239676
|
+
this.ctx.logger.error("Restore retry scheduler crashed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
239677
|
+
});
|
|
239678
|
+
}
|
|
239679
|
+
/**
|
|
239680
|
+
* Tear down and reconstruct ONE device from its persisted rows — the
|
|
239681
|
+
* `deviceProvider.reloadDevice` cap method. Persistence is never touched,
|
|
239682
|
+
* and no other device this provider owns is disturbed.
|
|
239683
|
+
*
|
|
239684
|
+
* Keyed by `stableId` because the caller's whole reason to be here is that
|
|
239685
|
+
* the NUMERIC id changed (`deviceManager.migrateDevice` swapped it): the
|
|
239686
|
+
* fresh instance resolves its id through `allocateDeviceId`, which returns
|
|
239687
|
+
* whatever number the row carries NOW. The teardown is `decommission` —
|
|
239688
|
+
* exactly what a graceful shutdown runs per device (fires `removeDevice()`,
|
|
239689
|
+
* unregisters native caps, drops the registry entry) — and the rebuild is
|
|
239690
|
+
* the boot restore's own `create()` path, including its pass 2: first-class
|
|
239691
|
+
* children (hub-adopted cameras under an NVR) are decommissioned with the
|
|
239692
|
+
* parent by the cascade and must be re-created explicitly, because only
|
|
239693
|
+
* accessory children come back through `getAccessoryChildren()`.
|
|
239694
|
+
*
|
|
239695
|
+
* Reloading an accessory child directly is refused (no device class) —
|
|
239696
|
+
* reload its parent instead.
|
|
239697
|
+
*/
|
|
239698
|
+
async reloadDevice(input) {
|
|
239699
|
+
const { stableId } = input;
|
|
239700
|
+
const devices = this.ctx.kernel.devices;
|
|
239701
|
+
if (!devices) throw new Error(`${this.providerName}: kernel.devices unavailable \u2014 cannot reload`);
|
|
239702
|
+
const live = (await devices.getAll()).find((d) => d.stableId === stableId);
|
|
239703
|
+
if (live) await devices.decommission(live.id);
|
|
239704
|
+
const { id } = await this.ctx.api.deviceManager.allocateDeviceId.mutate({
|
|
239705
|
+
addonId: this.addonId,
|
|
239706
|
+
stableId
|
|
239707
|
+
});
|
|
239708
|
+
const meta = await this.ctx.api.deviceManager.loadMeta.query({ deviceId: id });
|
|
239709
|
+
if (meta === null) throw new Error(`${this.providerName}: no persisted meta for "${stableId}" (id ${id}) \u2014 cannot reload`);
|
|
239710
|
+
const deviceType = Object.values(require_sleep.DeviceType).find((t) => t === meta.type);
|
|
239711
|
+
const Class = deviceType !== void 0 ? this.deviceClasses[deviceType] : void 0;
|
|
239712
|
+
if (!Class) throw new Error(`${this.providerName}: no device class for type "${meta.type}" \u2014 "${stableId}" is an accessory child; reload its parent instead`);
|
|
239713
|
+
await devices.create(stableId, Class, {}, meta.parentDeviceId ?? null);
|
|
239714
|
+
const rows = await this.ctx.api.deviceManager.listPersistedByAddon.query({ addonId: this.addonId });
|
|
239715
|
+
for (const row of rows) {
|
|
239716
|
+
if (row.parentDeviceId !== id) continue;
|
|
239717
|
+
const childType = Object.values(require_sleep.DeviceType).find((t) => t === row.type);
|
|
239718
|
+
const ChildClass = childType !== void 0 ? this.deviceClasses[childType] : void 0;
|
|
239719
|
+
if (!ChildClass) continue;
|
|
239720
|
+
try {
|
|
239721
|
+
await devices.create(row.stableId, ChildClass, {}, id);
|
|
239722
|
+
} catch (err) {
|
|
239723
|
+
this.ctx.logger.warn("reloadDevice: failed to re-create first-class child", {
|
|
239724
|
+
tags: {
|
|
239725
|
+
deviceId: row.id,
|
|
239726
|
+
stableId: row.stableId
|
|
239727
|
+
},
|
|
239728
|
+
meta: {
|
|
239729
|
+
parentDeviceId: id,
|
|
239730
|
+
error: err instanceof Error ? err.message : String(err)
|
|
239731
|
+
}
|
|
239732
|
+
});
|
|
239733
|
+
}
|
|
239734
|
+
}
|
|
239735
|
+
this.ctx.logger.info("device reloaded in place from persisted rows", {
|
|
239736
|
+
tags: { deviceId: id },
|
|
239737
|
+
meta: {
|
|
239738
|
+
stableId,
|
|
239739
|
+
type: meta.type
|
|
239740
|
+
}
|
|
239741
|
+
});
|
|
239742
|
+
return { deviceId: id };
|
|
239252
239743
|
}
|
|
239253
239744
|
/**
|
|
239254
239745
|
* Restore devices from persisted state. Two-pass:
|
|
@@ -239274,55 +239765,108 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239274
239765
|
* accessory-spawn flow handles via the parent's
|
|
239275
239766
|
* `getAccessoryChildren()`. Override only when the default doesn't
|
|
239276
239767
|
* fit.
|
|
239768
|
+
*
|
|
239769
|
+
* A row that fails either pass is NOT terminal (D347): it is handed
|
|
239770
|
+
* to a bounded background retry (`DeviceRestoreRetryScheduler`).
|
|
239771
|
+
* Only after the bound is exhausted is the device marked permanently
|
|
239772
|
+
* failed — logged at ERROR with `tags.deviceId` and surfaced via
|
|
239773
|
+
* `getStatus().error`.
|
|
239277
239774
|
*/
|
|
239278
239775
|
async onRestoreDevices(savedDevices) {
|
|
239279
239776
|
const restored = /* @__PURE__ */ new Set();
|
|
239777
|
+
const failures = [];
|
|
239778
|
+
const attemptRestore = async (saved) => {
|
|
239779
|
+
if (restored.has(saved.id)) return;
|
|
239780
|
+
const Class = this.deviceClasses[saved.type];
|
|
239781
|
+
if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
|
|
239782
|
+
if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
|
|
239783
|
+
await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
|
|
239784
|
+
restored.add(saved.id);
|
|
239785
|
+
};
|
|
239280
239786
|
const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
|
|
239281
239787
|
const restoreOne = async (saved) => {
|
|
239282
|
-
|
|
239283
|
-
if (!Class) {
|
|
239788
|
+
if (!this.deviceClasses[saved.type]) {
|
|
239284
239789
|
this.ctx.logger.warn("No device class registered for restored type \u2014 skipping", {
|
|
239285
|
-
tags: {
|
|
239790
|
+
tags: {
|
|
239791
|
+
deviceId: saved.id,
|
|
239792
|
+
stableId: saved.stableId
|
|
239793
|
+
},
|
|
239286
239794
|
meta: { type: saved.type }
|
|
239287
239795
|
});
|
|
239288
239796
|
return;
|
|
239289
239797
|
}
|
|
239290
239798
|
try {
|
|
239291
|
-
await
|
|
239292
|
-
restored.add(saved.id);
|
|
239799
|
+
await attemptRestore(saved);
|
|
239293
239800
|
} catch (err) {
|
|
239294
|
-
|
|
239295
|
-
|
|
239801
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
239802
|
+
this.ctx.logger.warn("Failed to restore device \u2014 bounded retry scheduled", {
|
|
239803
|
+
tags: {
|
|
239804
|
+
deviceId: saved.id,
|
|
239805
|
+
stableId: saved.stableId
|
|
239806
|
+
},
|
|
239296
239807
|
meta: {
|
|
239297
239808
|
type: saved.type,
|
|
239298
|
-
|
|
239809
|
+
attempt: 1,
|
|
239810
|
+
error
|
|
239299
239811
|
}
|
|
239300
239812
|
});
|
|
239813
|
+
failures.push({
|
|
239814
|
+
saved,
|
|
239815
|
+
error
|
|
239816
|
+
});
|
|
239301
239817
|
}
|
|
239302
239818
|
};
|
|
239303
239819
|
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
239820
|
+
const failedTopLevelIds = new Set(failures.map((failure) => failure.saved.id));
|
|
239304
239821
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
239305
239822
|
for (const saved of childRows) {
|
|
239306
|
-
|
|
239307
|
-
if (!Class) continue;
|
|
239823
|
+
if (!this.deviceClasses[saved.type]) continue;
|
|
239308
239824
|
if (saved.parentDeviceId === null) continue;
|
|
239309
|
-
if (
|
|
239310
|
-
|
|
239311
|
-
|
|
239312
|
-
|
|
239313
|
-
|
|
239314
|
-
|
|
239825
|
+
if (restored.has(saved.parentDeviceId)) {
|
|
239826
|
+
try {
|
|
239827
|
+
await attemptRestore(saved);
|
|
239828
|
+
} catch (err) {
|
|
239829
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
239830
|
+
this.ctx.logger.warn("Failed to restore hub-adopted child \u2014 bounded retry scheduled", {
|
|
239831
|
+
tags: {
|
|
239832
|
+
deviceId: saved.id,
|
|
239833
|
+
stableId: saved.stableId,
|
|
239834
|
+
parentDeviceId: saved.parentDeviceId
|
|
239835
|
+
},
|
|
239836
|
+
meta: {
|
|
239837
|
+
type: saved.type,
|
|
239838
|
+
attempt: 1,
|
|
239839
|
+
error
|
|
239840
|
+
}
|
|
239841
|
+
});
|
|
239842
|
+
failures.push({
|
|
239843
|
+
saved,
|
|
239844
|
+
error
|
|
239845
|
+
});
|
|
239846
|
+
}
|
|
239847
|
+
continue;
|
|
239848
|
+
}
|
|
239849
|
+
if (failedTopLevelIds.has(saved.parentDeviceId)) {
|
|
239850
|
+
this.ctx.logger.warn("Hub-adopted child deferred \u2014 parent failed initial restore", {
|
|
239315
239851
|
tags: {
|
|
239852
|
+
deviceId: saved.id,
|
|
239316
239853
|
stableId: saved.stableId,
|
|
239317
239854
|
parentDeviceId: saved.parentDeviceId
|
|
239318
239855
|
},
|
|
239319
|
-
meta: {
|
|
239320
|
-
|
|
239321
|
-
|
|
239322
|
-
|
|
239856
|
+
meta: { type: saved.type }
|
|
239857
|
+
});
|
|
239858
|
+
failures.push({
|
|
239859
|
+
saved,
|
|
239860
|
+
error: `parent device ${saved.parentDeviceId} not restored`
|
|
239323
239861
|
});
|
|
239862
|
+
continue;
|
|
239324
239863
|
}
|
|
239325
239864
|
}
|
|
239865
|
+
if (failures.length > 0) this.scheduleRestoreRetries(failures, attemptRestore);
|
|
239866
|
+
return {
|
|
239867
|
+
restoredCount: restored.size,
|
|
239868
|
+
failedCount: failures.length
|
|
239869
|
+
};
|
|
239326
239870
|
}
|
|
239327
239871
|
/** Convert an IDevice to the flat DeviceSummary for the cap router. */
|
|
239328
239872
|
toSummary(device2) {
|
|
@@ -243418,6 +243962,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
243418
243962
|
addonId: null,
|
|
243419
243963
|
access: "view"
|
|
243420
243964
|
},
|
|
243965
|
+
"deviceProvider.reloadDevice": {
|
|
243966
|
+
capName: "device-provider",
|
|
243967
|
+
capScope: "system",
|
|
243968
|
+
addonId: null,
|
|
243969
|
+
access: "create"
|
|
243970
|
+
},
|
|
243421
243971
|
"deviceProvider.start": {
|
|
243422
243972
|
capName: "device-provider",
|
|
243423
243973
|
capScope: "system",
|
|
@@ -253822,6 +254372,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
253822
254372
|
exports.DEVICE_CAP_NAMES = DEVICE_CAP_NAMES;
|
|
253823
254373
|
exports.DEVICE_CHILDREN_BATCH_MAX = DEVICE_CHILDREN_BATCH_MAX;
|
|
253824
254374
|
exports.DEVICE_PROFILES = DEVICE_PROFILES;
|
|
254375
|
+
exports.DEVICE_RESTORE_RETRY_CONCURRENCY = DEVICE_RESTORE_RETRY_CONCURRENCY;
|
|
254376
|
+
exports.DEVICE_RESTORE_RETRY_DELAYS_MS = DEVICE_RESTORE_RETRY_DELAYS_MS;
|
|
253825
254377
|
exports.DEVICE_SCOPED_CAPS = require_sleep.DEVICE_SCOPED_CAPS;
|
|
253826
254378
|
exports.DEVICE_SETTINGS_CONTRIBUTION_METHODS = require_sleep.DEVICE_SETTINGS_CONTRIBUTION_METHODS;
|
|
253827
254379
|
exports.DEVICE_STATE_READERS = DEVICE_STATE_READERS;
|
|
@@ -253851,6 +254403,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
253851
254403
|
exports.DeviceFeature = require_sleep.DeviceFeature;
|
|
253852
254404
|
exports.DeviceInfoSchema = DeviceInfoSchema;
|
|
253853
254405
|
exports.DeviceNetworkStatsSchema = DeviceNetworkStatsSchema;
|
|
254406
|
+
exports.DeviceRestoreRetryScheduler = DeviceRestoreRetryScheduler;
|
|
253854
254407
|
exports.DeviceRole = require_sleep.DeviceRole;
|
|
253855
254408
|
exports.DeviceRuntimeState = DeviceRuntimeState;
|
|
253856
254409
|
exports.DeviceSelectorSchema = DeviceSelectorSchema;
|