camstack 1.2.83 → 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.
|
@@ -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) {
|
|
@@ -28488,6 +28488,68 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
28488
28488
|
fileCount: zod.z.number(),
|
|
28489
28489
|
present: zod.z.boolean()
|
|
28490
28490
|
});
|
|
28491
|
+
var BackupRunStateSchema = zod.z.enum([
|
|
28492
|
+
"queued",
|
|
28493
|
+
"running",
|
|
28494
|
+
"succeeded",
|
|
28495
|
+
"failed",
|
|
28496
|
+
"cancelled"
|
|
28497
|
+
]);
|
|
28498
|
+
var BackupRunPhaseSchema = zod.z.enum([
|
|
28499
|
+
"queued",
|
|
28500
|
+
"building",
|
|
28501
|
+
"uploading",
|
|
28502
|
+
"done"
|
|
28503
|
+
]);
|
|
28504
|
+
var BackupRunSchema = zod.z.object({
|
|
28505
|
+
/** Stable run id — the handle `backup.cancel` takes. */
|
|
28506
|
+
id: zod.z.string(),
|
|
28507
|
+
state: BackupRunStateSchema,
|
|
28508
|
+
phase: BackupRunPhaseSchema,
|
|
28509
|
+
/**
|
|
28510
|
+
* Resolved destination location ids. Empty while queued (targets are
|
|
28511
|
+
* resolved when the run starts, against the then-current policies).
|
|
28512
|
+
*/
|
|
28513
|
+
destinationIds: zod.z.array(zod.z.string()).readonly(),
|
|
28514
|
+
label: zod.z.string().optional(),
|
|
28515
|
+
/** ms-epoch when the run was submitted (trigger call / schedule fire). */
|
|
28516
|
+
requestedAt: zod.z.number(),
|
|
28517
|
+
/** ms-epoch when the run left the queue and started building. */
|
|
28518
|
+
startedAt: zod.z.number().optional(),
|
|
28519
|
+
/** ms-epoch when the run reached a terminal state. */
|
|
28520
|
+
finishedAt: zod.z.number().optional(),
|
|
28521
|
+
/** Compressed bytes of the staging archive written so far. */
|
|
28522
|
+
stagedBytes: zod.z.number(),
|
|
28523
|
+
/** Final staged archive size, once the build phase completes. */
|
|
28524
|
+
archiveSizeBytes: zod.z.number().optional(),
|
|
28525
|
+
/** Bytes pushed to the destination currently uploading. */
|
|
28526
|
+
uploadedBytes: zod.z.number(),
|
|
28527
|
+
/** Destinations where the archive fully landed (uploaded + indexed). */
|
|
28528
|
+
completedDestinationIds: zod.z.array(zod.z.string()).readonly(),
|
|
28529
|
+
/** Destinations that failed during the fan-out. */
|
|
28530
|
+
failedDestinationIds: zod.z.array(zod.z.string()).readonly(),
|
|
28531
|
+
/** Failure message when `state === 'failed'`. */
|
|
28532
|
+
error: zod.z.string().optional(),
|
|
28533
|
+
/**
|
|
28534
|
+
* 1-based place in the execution queue — 1 = runs next. Present only
|
|
28535
|
+
* while `state === 'queued'`. Stamped by the orchestrator from the
|
|
28536
|
+
* queue's OWN pending order, never derived from timestamps, so the
|
|
28537
|
+
* UI cannot show an order the executor will not honour.
|
|
28538
|
+
*/
|
|
28539
|
+
queuePosition: zod.z.number().int().min(1).optional()
|
|
28540
|
+
});
|
|
28541
|
+
var BackupTriggerResultSchema = zod.z.object({
|
|
28542
|
+
/** The run this trigger mapped to — poll it via `listRuns`, stop it via `cancel`. */
|
|
28543
|
+
runId: zod.z.string(),
|
|
28544
|
+
/** True when the run waited behind an in-flight run instead of starting immediately. */
|
|
28545
|
+
queued: zod.z.boolean(),
|
|
28546
|
+
/** True when this trigger was coalesced onto an identical already-queued run. */
|
|
28547
|
+
joined: zod.z.boolean(),
|
|
28548
|
+
/** True when the run was cancelled before completing every destination. */
|
|
28549
|
+
cancelled: zod.z.boolean(),
|
|
28550
|
+
/** One entry per destination the archive landed at (partial on cancel). */
|
|
28551
|
+
entries: zod.z.array(BackupEntrySchema).readonly()
|
|
28552
|
+
});
|
|
28491
28553
|
var BackupScheduleSchema = zod.z.object({
|
|
28492
28554
|
/** Stable id. Generated by the orchestrator on first upsert if absent. */
|
|
28493
28555
|
id: zod.z.string(),
|
|
@@ -28524,6 +28586,11 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
28524
28586
|
* Trigger a backup. Without `destinations` the orchestrator fans
|
|
28525
28587
|
* out to every destination flagged as enabled in the routing
|
|
28526
28588
|
* config; with it, only the listed addons receive the archive.
|
|
28589
|
+
*
|
|
28590
|
+
* At most ONE backup run executes at a time — the source tree and
|
|
28591
|
+
* the staging disk are shared by every run, so a second trigger
|
|
28592
|
+
* while one is in flight is enqueued (or joined onto an identical
|
|
28593
|
+
* queued run) and the result says so. See D342.
|
|
28527
28594
|
*/
|
|
28528
28595
|
trigger: method(zod.z.object({
|
|
28529
28596
|
/** Subset of registered `backup-destination` addon ids to write to. */
|
|
@@ -28537,7 +28604,30 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
28537
28604
|
* retention (manual runs).
|
|
28538
28605
|
*/
|
|
28539
28606
|
retentionCount: zod.z.number().int().min(1).max(1e3).optional()
|
|
28540
|
-
}).optional(),
|
|
28607
|
+
}).optional(), BackupTriggerResultSchema, {
|
|
28608
|
+
kind: "mutation",
|
|
28609
|
+
auth: "admin"
|
|
28610
|
+
}),
|
|
28611
|
+
/**
|
|
28612
|
+
* Every run the orchestrator knows about, in EXECUTION order: the
|
|
28613
|
+
* running run first, then queued runs in the exact order they will
|
|
28614
|
+
* execute (each with `queuePosition`, 1 = next), then the bounded
|
|
28615
|
+
* finished history newest-first. Only one run executes at a time
|
|
28616
|
+
* (D342) — the queued section IS the line. Each run carries live
|
|
28617
|
+
* phase + byte counters so a runaway build is visible in seconds,
|
|
28618
|
+
* not via `du`.
|
|
28619
|
+
*/
|
|
28620
|
+
listRuns: method(zod.z.void(), zod.z.array(BackupRunSchema).readonly(), { auth: "admin" }),
|
|
28621
|
+
/**
|
|
28622
|
+
* Stop a backup run. Mirrors `storage-migration.cancel` semantics:
|
|
28623
|
+
* id in, `{ cancelled }` out — `false` when the run is unknown or
|
|
28624
|
+
* already terminal. A QUEUED run is removed before it ever starts;
|
|
28625
|
+
* the RUNNING run has its tar/upload stream actually aborted, the
|
|
28626
|
+
* half-written staging archive is deleted, and the in-flight
|
|
28627
|
+
* destination upload is aborted server-side (partial discarded).
|
|
28628
|
+
* Destinations that already completed keep their archive.
|
|
28629
|
+
*/
|
|
28630
|
+
cancel: method(zod.z.object({ runId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
|
|
28541
28631
|
kind: "mutation",
|
|
28542
28632
|
auth: "admin"
|
|
28543
28633
|
}),
|
|
@@ -29277,6 +29367,38 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
29277
29367
|
auth: "admin"
|
|
29278
29368
|
}),
|
|
29279
29369
|
/**
|
|
29370
|
+
* The HARDWARE behind a device number was replaced
|
|
29371
|
+
* (`deviceManager.migrateDevice`). Forget every piece of broker state that
|
|
29372
|
+
* described the old box, so the next catalog pull derives everything from
|
|
29373
|
+
* the camera that is actually there:
|
|
29374
|
+
*
|
|
29375
|
+
* - every `derived:*` stream definition — a derived is authored against a
|
|
29376
|
+
* specific profile layout, and against the wrong hardware its feeder
|
|
29377
|
+
* respawns forever (observed at attempt 2732 on the live hub,
|
|
29378
|
+
* 2026-09-01);
|
|
29379
|
+
* - the profile-slot assignment entry, PURGED (not unassigned — unassign
|
|
29380
|
+
* marks the slot manual, which would pin the stale choice instead of
|
|
29381
|
+
* letting `computeInitialAssignment` re-derive it);
|
|
29382
|
+
* - the probe snapshots (`<deviceId>/…` — probed codec/resolution of the
|
|
29383
|
+
* old hardware);
|
|
29384
|
+
* - the persisted RTSP token rows for the device's brokers (keyed
|
|
29385
|
+
* `<deviceId>/<camStreamId>`; the stream ids change with the hardware,
|
|
29386
|
+
* so the rows are dead URLs).
|
|
29387
|
+
*
|
|
29388
|
+
* An RPC, deliberately — an event is telemetry and may be dropped (D8),
|
|
29389
|
+
* and a dropped forget leaves a feeder respawning against a stream that
|
|
29390
|
+
* does not exist.
|
|
29391
|
+
*/
|
|
29392
|
+
forgetDeviceHardware: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.object({
|
|
29393
|
+
derivedStreamsDeleted: zod.z.array(zod.z.string()).readonly(),
|
|
29394
|
+
assignmentsPurged: zod.z.boolean(),
|
|
29395
|
+
probeSnapshotsDropped: zod.z.number().int().nonnegative(),
|
|
29396
|
+
rtspTokenRowsDeleted: zod.z.number().int().nonnegative()
|
|
29397
|
+
}), {
|
|
29398
|
+
kind: "mutation",
|
|
29399
|
+
auth: "admin"
|
|
29400
|
+
}),
|
|
29401
|
+
/**
|
|
29280
29402
|
* Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
|
|
29281
29403
|
*
|
|
29282
29404
|
* The pre-buffer is the only source that already holds the seconds BEFORE
|
|
@@ -30992,6 +31114,35 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
30992
31114
|
name: zod.z.string(),
|
|
30993
31115
|
type: zod.z.string()
|
|
30994
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
|
+
}),
|
|
30995
31146
|
supportsDiscovery: method(zod.z.object({}), zod.z.boolean()),
|
|
30996
31147
|
/**
|
|
30997
31148
|
* Run a network scan. `params` carries optional provider-specific scan
|
|
@@ -31312,13 +31463,21 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
31312
31463
|
* `sourceStillLive` names what stayed live; empty is the only value that
|
|
31313
31464
|
* means the old hardware is quiet. The migration proceeds either way —
|
|
31314
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.
|
|
31315
31473
|
*/
|
|
31316
31474
|
migrateDevice: method(zod.z.object({
|
|
31317
31475
|
sourceId: zod.z.number(),
|
|
31318
31476
|
targetId: zod.z.number()
|
|
31319
31477
|
}), MigrateDeviceResultSchema, {
|
|
31320
31478
|
kind: "mutation",
|
|
31321
|
-
auth: "admin"
|
|
31479
|
+
auth: "admin",
|
|
31480
|
+
timeoutMs: 12 * 6e4
|
|
31322
31481
|
}),
|
|
31323
31482
|
/** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
|
|
31324
31483
|
registerDevice: method(DeviceRegisterPayloadSchema, zod.z.void(), { kind: "mutation" }),
|
|
@@ -51132,6 +51291,12 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
51132
51291
|
addonId: null,
|
|
51133
51292
|
access: "create"
|
|
51134
51293
|
},
|
|
51294
|
+
"backup.cancel": {
|
|
51295
|
+
capName: "backup",
|
|
51296
|
+
capScope: "system",
|
|
51297
|
+
addonId: null,
|
|
51298
|
+
access: "create"
|
|
51299
|
+
},
|
|
51135
51300
|
"backup.delete": {
|
|
51136
51301
|
capName: "backup",
|
|
51137
51302
|
capScope: "system",
|
|
@@ -51174,6 +51339,12 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
51174
51339
|
addonId: null,
|
|
51175
51340
|
access: "view"
|
|
51176
51341
|
},
|
|
51342
|
+
"backup.listRuns": {
|
|
51343
|
+
capName: "backup",
|
|
51344
|
+
capScope: "system",
|
|
51345
|
+
addonId: null,
|
|
51346
|
+
access: "view"
|
|
51347
|
+
},
|
|
51177
51348
|
"backup.listSchedules": {
|
|
51178
51349
|
capName: "backup",
|
|
51179
51350
|
capScope: "system",
|
|
@@ -52374,6 +52545,12 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
52374
52545
|
addonId: null,
|
|
52375
52546
|
access: "view"
|
|
52376
52547
|
},
|
|
52548
|
+
"deviceProvider.reloadDevice": {
|
|
52549
|
+
capName: "device-provider",
|
|
52550
|
+
capScope: "system",
|
|
52551
|
+
addonId: null,
|
|
52552
|
+
access: "create"
|
|
52553
|
+
},
|
|
52377
52554
|
"deviceProvider.start": {
|
|
52378
52555
|
capName: "device-provider",
|
|
52379
52556
|
capScope: "system",
|
|
@@ -55740,6 +55917,12 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
55740
55917
|
addonId: null,
|
|
55741
55918
|
access: "create"
|
|
55742
55919
|
},
|
|
55920
|
+
"streamBroker.forgetDeviceHardware": {
|
|
55921
|
+
capName: "stream-broker",
|
|
55922
|
+
capScope: "system",
|
|
55923
|
+
addonId: null,
|
|
55924
|
+
access: "delete"
|
|
55925
|
+
},
|
|
55743
55926
|
"streamBroker.getAllRtspEntries": {
|
|
55744
55927
|
capName: "stream-broker",
|
|
55745
55928
|
capScope: "system",
|
|
@@ -58198,6 +58381,11 @@ var require_dist_C7qWwrK1 = __commonJS({
|
|
|
58198
58381
|
form: "single",
|
|
58199
58382
|
optional: false
|
|
58200
58383
|
}],
|
|
58384
|
+
"streamBroker.forgetDeviceHardware": [{
|
|
58385
|
+
name: "deviceId",
|
|
58386
|
+
form: "single",
|
|
58387
|
+
optional: false
|
|
58388
|
+
}],
|
|
58201
58389
|
"streamBroker.getDeviceAudioMute": [{
|
|
58202
58390
|
name: "deviceId",
|
|
58203
58391
|
form: "single",
|
|
@@ -59854,7 +60042,7 @@ var require_alerts_addon = __commonJS({
|
|
|
59854
60042
|
[Symbol.toStringTag]: { value: "Module" }
|
|
59855
60043
|
});
|
|
59856
60044
|
require_chunk_Cek0wNdY();
|
|
59857
|
-
var require_dist10 =
|
|
60045
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
59858
60046
|
function selectExpired(alerts, cutoffMs) {
|
|
59859
60047
|
return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
|
|
59860
60048
|
}
|
|
@@ -60673,7 +60861,7 @@ var require_console_logging = __commonJS({
|
|
|
60673
60861
|
[Symbol.toStringTag]: { value: "Module" }
|
|
60674
60862
|
});
|
|
60675
60863
|
require_chunk_Cek0wNdY();
|
|
60676
|
-
var require_dist10 =
|
|
60864
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
60677
60865
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
60678
60866
|
var LEVEL_RANK = {
|
|
60679
60867
|
debug: 0,
|
|
@@ -60767,7 +60955,7 @@ var require_core_blocks_addon = __commonJS({
|
|
|
60767
60955
|
"use strict";
|
|
60768
60956
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
60769
60957
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
60770
|
-
var require_dist10 =
|
|
60958
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
60771
60959
|
var node_crypto = __require("crypto");
|
|
60772
60960
|
var node_fs_promises = __require("fs/promises");
|
|
60773
60961
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -61664,11 +61852,11 @@ var require_core_blocks = __commonJS({
|
|
|
61664
61852
|
}
|
|
61665
61853
|
});
|
|
61666
61854
|
|
|
61667
|
-
// ../system/dist/retired-settings-keys-
|
|
61668
|
-
var
|
|
61669
|
-
"../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) {
|
|
61670
61858
|
"use strict";
|
|
61671
|
-
var require_dist10 =
|
|
61859
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
61672
61860
|
function settingsStoreIsAuthoritativeHere(env) {
|
|
61673
61861
|
const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
|
|
61674
61862
|
return raw === "" || raw === "hub";
|
|
@@ -63882,8 +64070,8 @@ var require_device_manager_addon = __commonJS({
|
|
|
63882
64070
|
[Symbol.toStringTag]: { value: "Module" }
|
|
63883
64071
|
});
|
|
63884
64072
|
require_chunk_Cek0wNdY();
|
|
63885
|
-
var require_dist10 =
|
|
63886
|
-
var require_retired_settings_keys =
|
|
64073
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
64074
|
+
var require_retired_settings_keys = require_retired_settings_keys_BdvmiaK7();
|
|
63887
64075
|
var node_crypto = __require("crypto");
|
|
63888
64076
|
var _camstack_types_node = require_node();
|
|
63889
64077
|
var JOB_HISTORY = 20;
|
|
@@ -65546,7 +65734,13 @@ var require_device_manager_addon = __commonJS({
|
|
|
65546
65734
|
if (config.mode === "auto") {
|
|
65547
65735
|
if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
|
|
65548
65736
|
if (c.type === require_dist10.DeviceType.Container) continue;
|
|
65549
|
-
if (c.location
|
|
65737
|
+
if (c.location !== selfLocation) continue;
|
|
65738
|
+
const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
|
|
65739
|
+
if (parent !== void 0 && parent.type === require_dist10.DeviceType.Container) {
|
|
65740
|
+
expand(parent.id);
|
|
65741
|
+
continue;
|
|
65742
|
+
}
|
|
65743
|
+
ids.add(c.id);
|
|
65550
65744
|
}
|
|
65551
65745
|
} else for (const id of config.manualIds) if (byId.has(id)) expand(id);
|
|
65552
65746
|
ids.delete(selfId);
|
|
@@ -66376,10 +66570,26 @@ var require_device_manager_addon = __commonJS({
|
|
|
66376
66570
|
detail
|
|
66377
66571
|
};
|
|
66378
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
|
+
}
|
|
66379
66589
|
async function applyAll(deps, deviceId, enabled) {
|
|
66380
66590
|
const out = [];
|
|
66381
66591
|
for (const switchId of require_dist10.CAMERA_SWITCH_ORDER) try {
|
|
66382
|
-
await deps
|
|
66592
|
+
await setSwitchBounded(deps, deviceId, switchId, enabled);
|
|
66383
66593
|
out.push({
|
|
66384
66594
|
deviceId,
|
|
66385
66595
|
switchId,
|
|
@@ -66409,8 +66619,17 @@ var require_device_manager_addon = __commonJS({
|
|
|
66409
66619
|
const down = [...await applyAll(deps, sourceId, false), ...await applyAll(deps, targetId, false)];
|
|
66410
66620
|
const sourceStillLive = down.filter((r) => r.deviceId === sourceId && r.outcome === "unreachable").map((r) => r.switchId);
|
|
66411
66621
|
await deps.swapIds(sourceId, targetId);
|
|
66622
|
+
await deps.swapHardwareState(sourceId, targetId);
|
|
66623
|
+
for (const deviceId of [sourceId, targetId]) try {
|
|
66624
|
+
await deps.forgetStreamState(deviceId);
|
|
66625
|
+
} catch (err) {
|
|
66626
|
+
deps.log("migration: broker hardware state NOT forgotten \u2014 stale stream state survives", {
|
|
66627
|
+
deviceId,
|
|
66628
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66629
|
+
remedy: "delete the device's derived streams and reassign its stream profiles, or update the stream-broker addon and re-run streamBroker.forgetDeviceHardware"
|
|
66630
|
+
});
|
|
66631
|
+
}
|
|
66412
66632
|
const up = await applyAll(deps, sourceId, true);
|
|
66413
|
-
await deps.respawnRunners([sourceId, targetId]);
|
|
66414
66633
|
deps.log("camera migrated", {
|
|
66415
66634
|
sourceId,
|
|
66416
66635
|
targetId,
|
|
@@ -66425,6 +66644,93 @@ var require_device_manager_addon = __commonJS({
|
|
|
66425
66644
|
swapped: true
|
|
66426
66645
|
};
|
|
66427
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
|
+
}
|
|
66691
|
+
var PROFILE_MAP_CONFIG_KEY = "_profileMap";
|
|
66692
|
+
var ROLE_SCOPED_CONFIG_KEYS = new Set(DEVICE_STORE_SECTIONS.flatMap((section) => [...section.keys]));
|
|
66693
|
+
function splitConfig(config) {
|
|
66694
|
+
const hardware = {};
|
|
66695
|
+
const role = {};
|
|
66696
|
+
for (const [key, value] of Object.entries(config)) {
|
|
66697
|
+
if (key === PROFILE_MAP_CONFIG_KEY) continue;
|
|
66698
|
+
if (ROLE_SCOPED_CONFIG_KEYS.has(key)) role[key] = value;
|
|
66699
|
+
else hardware[key] = value;
|
|
66700
|
+
}
|
|
66701
|
+
return {
|
|
66702
|
+
hardware,
|
|
66703
|
+
role
|
|
66704
|
+
};
|
|
66705
|
+
}
|
|
66706
|
+
async function swapHardwareState(settings, sourceId, targetId) {
|
|
66707
|
+
const sourceConfig = await settings.readDeviceStore(sourceId);
|
|
66708
|
+
const targetConfig = await settings.readDeviceStore(targetId);
|
|
66709
|
+
const source = splitConfig(sourceConfig);
|
|
66710
|
+
const target = splitConfig(targetConfig);
|
|
66711
|
+
await settings.clearDeviceStore(sourceId);
|
|
66712
|
+
await settings.writeDeviceStore(sourceId, {
|
|
66713
|
+
...target.hardware,
|
|
66714
|
+
...source.role
|
|
66715
|
+
});
|
|
66716
|
+
await settings.clearDeviceStore(targetId);
|
|
66717
|
+
await settings.writeDeviceStore(targetId, {
|
|
66718
|
+
...source.hardware,
|
|
66719
|
+
...target.role
|
|
66720
|
+
});
|
|
66721
|
+
const sourceRuntimeState = await settings.readDeviceRuntimeState(sourceId);
|
|
66722
|
+
const targetRuntimeState = await settings.readDeviceRuntimeState(targetId);
|
|
66723
|
+
await settings.writeDeviceRuntimeState(sourceId, targetRuntimeState);
|
|
66724
|
+
await settings.writeDeviceRuntimeState(targetId, sourceRuntimeState);
|
|
66725
|
+
return {
|
|
66726
|
+
runtimeStateNowAtSource: targetRuntimeState,
|
|
66727
|
+
runtimeStateNowAtTarget: sourceRuntimeState,
|
|
66728
|
+
hardwareKeysToSource: Object.keys(target.hardware).toSorted(),
|
|
66729
|
+
hardwareKeysToTarget: Object.keys(source.hardware).toSorted(),
|
|
66730
|
+
roleKeysKeptAtSource: Object.keys(source.role).toSorted(),
|
|
66731
|
+
roleKeysKeptAtTarget: Object.keys(target.role).toSorted()
|
|
66732
|
+
};
|
|
66733
|
+
}
|
|
66428
66734
|
async function stampIntegrationId(metaStore, ctx, deviceId, integrationId) {
|
|
66429
66735
|
await metaStore.withMetaWriteLock(async () => {
|
|
66430
66736
|
if (!await metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
|
|
@@ -66470,39 +66776,110 @@ var require_device_manager_addon = __commonJS({
|
|
|
66470
66776
|
async function migrateDevice(pctx, input) {
|
|
66471
66777
|
const { sourceId, targetId } = input;
|
|
66472
66778
|
const ctx = pctx.host.ctx;
|
|
66473
|
-
|
|
66474
|
-
|
|
66475
|
-
const
|
|
66476
|
-
|
|
66477
|
-
|
|
66478
|
-
|
|
66479
|
-
|
|
66480
|
-
|
|
66481
|
-
|
|
66482
|
-
|
|
66483
|
-
},
|
|
66484
|
-
|
|
66485
|
-
|
|
66486
|
-
}
|
|
66487
|
-
|
|
66488
|
-
|
|
66489
|
-
for (const deviceId of deviceIds) {
|
|
66490
|
-
const addonId = pctx.metaStore.idToAddonId.get(deviceId);
|
|
66491
|
-
if (addonId !== void 0) owners.add(addonId);
|
|
66492
|
-
}
|
|
66493
|
-
for (const addonId of owners) await ctx.api.addons.restartAddon.mutate({ addonId });
|
|
66494
|
-
},
|
|
66495
|
-
log: (message, meta) => {
|
|
66496
|
-
ctx.logger.info(message, {
|
|
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", {
|
|
66497
66795
|
tags: { deviceId: sourceId },
|
|
66498
|
-
meta
|
|
66796
|
+
meta: {
|
|
66797
|
+
sourceId,
|
|
66798
|
+
targetId
|
|
66799
|
+
}
|
|
66499
66800
|
});
|
|
66801
|
+
return {
|
|
66802
|
+
result: replay,
|
|
66803
|
+
reloadTargets: null
|
|
66804
|
+
};
|
|
66500
66805
|
}
|
|
66501
|
-
|
|
66502
|
-
|
|
66503
|
-
|
|
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 },
|
|
66829
|
+
meta: {
|
|
66830
|
+
sourceId: a,
|
|
66831
|
+
targetId: b,
|
|
66832
|
+
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
66833
|
+
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
66834
|
+
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
66835
|
+
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
66836
|
+
}
|
|
66837
|
+
});
|
|
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
|
+
});
|
|
66847
|
+
}
|
|
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
|
+
};
|
|
66504
66877
|
});
|
|
66505
|
-
|
|
66878
|
+
if (outcome.reloadTargets !== null) await reloadMigratedDevices(pctx, outcome.reloadTargets);
|
|
66879
|
+
return outcome.result;
|
|
66880
|
+
} finally {
|
|
66881
|
+
pctx.migrationGuard.end(sourceId, targetId);
|
|
66882
|
+
}
|
|
66506
66883
|
}
|
|
66507
66884
|
async function registerDevice(pctx, input) {
|
|
66508
66885
|
const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
|
|
@@ -67023,18 +67400,17 @@ var require_device_manager_addon = __commonJS({
|
|
|
67023
67400
|
}
|
|
67024
67401
|
});
|
|
67025
67402
|
}
|
|
67026
|
-
async function
|
|
67027
|
-
const
|
|
67028
|
-
|
|
67029
|
-
|
|
67030
|
-
|
|
67031
|
-
|
|
67032
|
-
|
|
67033
|
-
|
|
67034
|
-
|
|
67035
|
-
|
|
67036
|
-
|
|
67037
|
-
});
|
|
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) {
|
|
67038
67414
|
pctx.host.ctx.eventBus.emit({
|
|
67039
67415
|
id: (0, node_crypto.randomUUID)(),
|
|
67040
67416
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -67064,6 +67440,10 @@ var require_device_manager_addon = __commonJS({
|
|
|
67064
67440
|
}
|
|
67065
67441
|
});
|
|
67066
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
|
+
}
|
|
67067
67447
|
async function loadRuntimeState(pctx, input) {
|
|
67068
67448
|
const { deviceId } = input;
|
|
67069
67449
|
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
|
|
@@ -68444,6 +68824,42 @@ var require_device_manager_addon = __commonJS({
|
|
|
68444
68824
|
};
|
|
68445
68825
|
return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
|
|
68446
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
|
+
}
|
|
68447
68863
|
var DeviceManagerAddon = class extends require_dist10.BaseAddon {
|
|
68448
68864
|
constructor() {
|
|
68449
68865
|
super({});
|
|
@@ -68682,6 +69098,7 @@ var require_device_manager_addon = __commonJS({
|
|
|
68682
69098
|
settings,
|
|
68683
69099
|
registry,
|
|
68684
69100
|
bindingsDeps: this.bindingsDeps,
|
|
69101
|
+
migrationGuard: createMigrationGuard(),
|
|
68685
69102
|
requireDeviceOps,
|
|
68686
69103
|
stampIntegrationId: stampIntegrationId$1,
|
|
68687
69104
|
get provider() {
|
|
@@ -68923,7 +69340,7 @@ var require_hub_forwarder = __commonJS({
|
|
|
68923
69340
|
[Symbol.toStringTag]: { value: "Module" }
|
|
68924
69341
|
});
|
|
68925
69342
|
require_chunk_Cek0wNdY();
|
|
68926
|
-
var require_dist10 =
|
|
69343
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
68927
69344
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
68928
69345
|
var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
|
|
68929
69346
|
var HubForwarderDestination = class {
|
|
@@ -69060,7 +69477,7 @@ var require_liveness_monitor_addon = __commonJS({
|
|
|
69060
69477
|
"use strict";
|
|
69061
69478
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
69062
69479
|
require_chunk_Cek0wNdY();
|
|
69063
|
-
var require_dist10 =
|
|
69480
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
69064
69481
|
var NO_DEVICES = "liveness:no-devices";
|
|
69065
69482
|
var ALL_OFFLINE = "liveness:all-devices-offline";
|
|
69066
69483
|
var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
|
|
@@ -69250,7 +69667,7 @@ var require_local_auth_addon = __commonJS({
|
|
|
69250
69667
|
[Symbol.toStringTag]: { value: "Module" }
|
|
69251
69668
|
});
|
|
69252
69669
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
69253
|
-
var require_dist10 =
|
|
69670
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
69254
69671
|
var node_crypto = __require("crypto");
|
|
69255
69672
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
69256
69673
|
var crypto$1 = __require("crypto");
|
|
@@ -77063,7 +77480,7 @@ var require_loki_logging = __commonJS({
|
|
|
77063
77480
|
[Symbol.toStringTag]: { value: "Module" }
|
|
77064
77481
|
});
|
|
77065
77482
|
require_chunk_Cek0wNdY();
|
|
77066
|
-
var require_dist10 =
|
|
77483
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
77067
77484
|
function sanitizeLabelName(raw) {
|
|
77068
77485
|
const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
|
|
77069
77486
|
return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
|
|
@@ -77628,7 +78045,7 @@ var require_native_metrics_addon = __commonJS({
|
|
|
77628
78045
|
[Symbol.toStringTag]: { value: "Module" }
|
|
77629
78046
|
});
|
|
77630
78047
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
77631
|
-
var require_dist10 =
|
|
78048
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
77632
78049
|
var node_fs_promises = __require("fs/promises");
|
|
77633
78050
|
var node_child_process = __require("child_process");
|
|
77634
78051
|
var node_util = __require("util");
|
|
@@ -80212,7 +80629,7 @@ var require_filesystem_storage_addon = __commonJS({
|
|
|
80212
80629
|
[Symbol.toStringTag]: { value: "Module" }
|
|
80213
80630
|
});
|
|
80214
80631
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
80215
|
-
var require_dist10 =
|
|
80632
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
80216
80633
|
var node_crypto = __require("crypto");
|
|
80217
80634
|
var node_fs_promises = __require("fs/promises");
|
|
80218
80635
|
var node_path = __require("path");
|
|
@@ -81328,8 +81745,8 @@ var require_sqlite_settings_addon = __commonJS({
|
|
|
81328
81745
|
[Symbol.toStringTag]: { value: "Module" }
|
|
81329
81746
|
});
|
|
81330
81747
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
81331
|
-
var require_dist10 =
|
|
81332
|
-
var require_retired_settings_keys =
|
|
81748
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
81749
|
+
var require_retired_settings_keys = require_retired_settings_keys_BdvmiaK7();
|
|
81333
81750
|
var node_crypto = __require("crypto");
|
|
81334
81751
|
var node_fs = __require("fs");
|
|
81335
81752
|
var node_module = __require("module");
|
|
@@ -83819,7 +84236,7 @@ var require_storage_orchestrator_addon = __commonJS({
|
|
|
83819
84236
|
[Symbol.toStringTag]: { value: "Module" }
|
|
83820
84237
|
});
|
|
83821
84238
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
83822
|
-
var require_dist10 =
|
|
84239
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
83823
84240
|
var require_hub_cap_forward = require_hub_cap_forward_DmHNjbB0();
|
|
83824
84241
|
var zod = require_zod();
|
|
83825
84242
|
var node_crypto = __require("crypto");
|
|
@@ -86770,7 +87187,7 @@ var require_system_config_addon = __commonJS({
|
|
|
86770
87187
|
[Symbol.toStringTag]: { value: "Module" }
|
|
86771
87188
|
});
|
|
86772
87189
|
require_chunk_Cek0wNdY();
|
|
86773
|
-
var require_dist10 =
|
|
87190
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
86774
87191
|
var SECTION_TITLES = {
|
|
86775
87192
|
server: "Server",
|
|
86776
87193
|
auth: "Authentication"
|
|
@@ -104831,7 +105248,7 @@ var require_winston_logging = __commonJS({
|
|
|
104831
105248
|
[Symbol.toStringTag]: { value: "Module" }
|
|
104832
105249
|
});
|
|
104833
105250
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
104834
|
-
var require_dist10 =
|
|
105251
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
104835
105252
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
104836
105253
|
var node_path = __require("path");
|
|
104837
105254
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -106774,9 +107191,9 @@ var require_event_category_BaEgqJNv = __commonJS({
|
|
|
106774
107191
|
}
|
|
106775
107192
|
});
|
|
106776
107193
|
|
|
106777
|
-
// ../types/dist/sleep-
|
|
106778
|
-
var
|
|
106779
|
-
"../types/dist/sleep-
|
|
107194
|
+
// ../types/dist/sleep-jdpPltQH.js
|
|
107195
|
+
var require_sleep_jdpPltQH = __commonJS({
|
|
107196
|
+
"../types/dist/sleep-jdpPltQH.js"(exports) {
|
|
106780
107197
|
"use strict";
|
|
106781
107198
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
106782
107199
|
var zod = require_zod();
|
|
@@ -109700,6 +110117,7 @@ var require_sleep_BwGJ_wL = __commonJS({
|
|
|
109700
110117
|
getDeviceLiveContribution: (input) => dispatchSystem("deviceManager", "getDeviceLiveContribution", "query", input),
|
|
109701
110118
|
applyDeviceSettingsPatch: (input) => dispatchSystem("deviceManager", "applyDeviceSettingsPatch", "mutation", input)
|
|
109702
110119
|
},
|
|
110120
|
+
deviceProvider: { reloadDevice: (input) => dispatchSystem("deviceProvider", "reloadDevice", "mutation", input) },
|
|
109703
110121
|
deviceState: {
|
|
109704
110122
|
getSnapshot: (input) => dispatchSystem("deviceState", "getSnapshot", "query", input),
|
|
109705
110123
|
getCapSlice: (input) => dispatchSystem("deviceState", "getCapSlice", "query", input),
|
|
@@ -109784,6 +110202,7 @@ var require_sleep_BwGJ_wL = __commonJS({
|
|
|
109784
110202
|
retractCameraStream: (input) => dispatchSystem("streamBroker", "retractCameraStream", "mutation", input),
|
|
109785
110203
|
assignProfile: (input) => dispatchSystem("streamBroker", "assignProfile", "mutation", input),
|
|
109786
110204
|
unassignProfile: (input) => dispatchSystem("streamBroker", "unassignProfile", "mutation", input),
|
|
110205
|
+
forgetDeviceHardware: (input) => dispatchSystem("streamBroker", "forgetDeviceHardware", "mutation", input),
|
|
109787
110206
|
renderPreBufferClip: (input) => dispatchSystem("streamBroker", "renderPreBufferClip", "mutation", input),
|
|
109788
110207
|
produceEventMedia: (input) => dispatchSystem("streamBroker", "produceEventMedia", "mutation", input),
|
|
109789
110208
|
restartProfile: (input) => dispatchSystem("streamBroker", "restartProfile", "mutation", input),
|
|
@@ -110448,7 +110867,7 @@ var require_addon = __commonJS({
|
|
|
110448
110867
|
"use strict";
|
|
110449
110868
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
110450
110869
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
110451
|
-
var require_sleep =
|
|
110870
|
+
var require_sleep = require_sleep_jdpPltQH();
|
|
110452
110871
|
var require_err_msg = require_err_msg_COpsHMw2();
|
|
110453
110872
|
var CAP_INPUT_DEFAULTS = Object.freeze({
|
|
110454
110873
|
"addons": { "getLogs": { "limit": 100 } },
|
|
@@ -117331,12 +117750,12 @@ var require_dist2 = __commonJS({
|
|
|
117331
117750
|
}
|
|
117332
117751
|
});
|
|
117333
117752
|
|
|
117334
|
-
// ../system/dist/manifest-system-deps-
|
|
117335
|
-
var
|
|
117336
|
-
"../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) {
|
|
117337
117756
|
"use strict";
|
|
117338
117757
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
117339
|
-
|
|
117758
|
+
require_dist_whe0b4_3();
|
|
117340
117759
|
require_hub_cap_forward_DmHNjbB0();
|
|
117341
117760
|
var node_crypto = __require("crypto");
|
|
117342
117761
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
@@ -129403,7 +129822,7 @@ var require_dist3 = __commonJS({
|
|
|
129403
129822
|
"use strict";
|
|
129404
129823
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
129405
129824
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
129406
|
-
var require_dist10 =
|
|
129825
|
+
var require_dist10 = require_dist_whe0b4_3();
|
|
129407
129826
|
var require_builtins_alerts_alerts_addon = require_alerts_addon();
|
|
129408
129827
|
require_alerts();
|
|
129409
129828
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
@@ -129430,7 +129849,7 @@ var require_dist3 = __commonJS({
|
|
|
129430
129849
|
var require_builtins_winston_logging_index = require_winston_logging();
|
|
129431
129850
|
var require_file_data_plane = require_file_data_plane_DO8KbxCe();
|
|
129432
129851
|
var require_tls$1 = require_tls_BxQlomxd();
|
|
129433
|
-
var require_manifest_system_deps =
|
|
129852
|
+
var require_manifest_system_deps = require_manifest_system_deps_y5vZBO6q();
|
|
129434
129853
|
var require_resource_monitor = require_resource_monitor_CdnzxBLP();
|
|
129435
129854
|
var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
|
|
129436
129855
|
var zod = require_zod();
|
|
@@ -132468,7 +132887,265 @@ var require_dist3 = __commonJS({
|
|
|
132468
132887
|
};
|
|
132469
132888
|
}
|
|
132470
132889
|
};
|
|
132890
|
+
function parseVersion(value) {
|
|
132891
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value.trim());
|
|
132892
|
+
if (!m) return null;
|
|
132893
|
+
return {
|
|
132894
|
+
major: Number(m[1]),
|
|
132895
|
+
minor: Number(m[2]),
|
|
132896
|
+
patch: Number(m[3]),
|
|
132897
|
+
prerelease: m[4] ?? null
|
|
132898
|
+
};
|
|
132899
|
+
}
|
|
132900
|
+
function compareTriple(a, b) {
|
|
132901
|
+
if (a.major !== b.major) return a.major < b.major ? -1 : 1;
|
|
132902
|
+
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
|
|
132903
|
+
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
|
|
132904
|
+
return 0;
|
|
132905
|
+
}
|
|
132906
|
+
function closureSatisfiesRange(range, version2) {
|
|
132907
|
+
const trimmedRange = range.trim();
|
|
132908
|
+
const v2 = parseVersion(version2);
|
|
132909
|
+
if (v2 === null) return false;
|
|
132910
|
+
if (v2.prerelease !== null) return trimmedRange === version2.trim() || trimmedRange === `=${version2.trim()}`;
|
|
132911
|
+
const exactBase = parseVersion(trimmedRange.startsWith("=") ? trimmedRange.slice(1) : trimmedRange);
|
|
132912
|
+
if (exactBase !== null) return exactBase.prerelease === null && compareTriple(v2, exactBase) === 0;
|
|
132913
|
+
const operator = trimmedRange[0];
|
|
132914
|
+
if (operator !== "^" && operator !== "~") return false;
|
|
132915
|
+
const base = parseVersion(trimmedRange.slice(1));
|
|
132916
|
+
if (base === null || base.prerelease !== null) return false;
|
|
132917
|
+
if (operator === "~") return v2.major === base.major && v2.minor === base.minor && compareTriple(v2, base) >= 0;
|
|
132918
|
+
if (base.major > 0) return v2.major === base.major && compareTriple(v2, base) >= 0;
|
|
132919
|
+
if (base.minor > 0) return v2.major === 0 && v2.minor === base.minor && compareTriple(v2, base) >= 0;
|
|
132920
|
+
return compareTriple(v2, base) === 0;
|
|
132921
|
+
}
|
|
132922
|
+
function sameCompatLine(a, b) {
|
|
132923
|
+
const pa = parseVersion(a);
|
|
132924
|
+
const pb = parseVersion(b);
|
|
132925
|
+
if (pa === null || pb === null) return false;
|
|
132926
|
+
if (pa.major !== pb.major) return false;
|
|
132927
|
+
if (pa.major === 0) return pa.minor === pb.minor;
|
|
132928
|
+
return true;
|
|
132929
|
+
}
|
|
132930
|
+
function planClosureDedupe(input) {
|
|
132931
|
+
const links = [];
|
|
132932
|
+
const keeps = [];
|
|
132933
|
+
for (const candidate of input.candidates) {
|
|
132934
|
+
if (candidate.name.startsWith("@camstack/")) {
|
|
132935
|
+
keeps.push({
|
|
132936
|
+
name: candidate.name,
|
|
132937
|
+
reason: "camstack-package"
|
|
132938
|
+
});
|
|
132939
|
+
continue;
|
|
132940
|
+
}
|
|
132941
|
+
if (input.neverLink.has(candidate.name)) {
|
|
132942
|
+
keeps.push({
|
|
132943
|
+
name: candidate.name,
|
|
132944
|
+
reason: "never-link"
|
|
132945
|
+
});
|
|
132946
|
+
continue;
|
|
132947
|
+
}
|
|
132948
|
+
const closureVersion = input.closureVersions.get(candidate.name);
|
|
132949
|
+
if (closureVersion === void 0) {
|
|
132950
|
+
keeps.push({
|
|
132951
|
+
name: candidate.name,
|
|
132952
|
+
reason: "not-in-closure"
|
|
132953
|
+
});
|
|
132954
|
+
continue;
|
|
132955
|
+
}
|
|
132956
|
+
if (closureVersion === candidate.version) {
|
|
132957
|
+
links.push({
|
|
132958
|
+
name: candidate.name,
|
|
132959
|
+
closureVersion
|
|
132960
|
+
});
|
|
132961
|
+
continue;
|
|
132962
|
+
}
|
|
132963
|
+
const ranges = input.requirements.get(candidate.name) ?? [];
|
|
132964
|
+
if (ranges.length > 0 && ranges.every((range) => closureSatisfiesRange(range, closureVersion)) && sameCompatLine(closureVersion, candidate.version)) links.push({
|
|
132965
|
+
name: candidate.name,
|
|
132966
|
+
closureVersion
|
|
132967
|
+
});
|
|
132968
|
+
else keeps.push({
|
|
132969
|
+
name: candidate.name,
|
|
132970
|
+
reason: "version-incompatible"
|
|
132971
|
+
});
|
|
132972
|
+
}
|
|
132973
|
+
return {
|
|
132974
|
+
links,
|
|
132975
|
+
keeps
|
|
132976
|
+
};
|
|
132977
|
+
}
|
|
132978
|
+
function readTreeManifest(pkgJsonPath) {
|
|
132979
|
+
let raw;
|
|
132980
|
+
try {
|
|
132981
|
+
raw = require_dist10.asJsonObject(require_dist10.parseJsonUnknown(node_fs.readFileSync(pkgJsonPath, "utf-8")));
|
|
132982
|
+
} catch {
|
|
132983
|
+
return null;
|
|
132984
|
+
}
|
|
132985
|
+
if (raw === null) return null;
|
|
132986
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
132987
|
+
for (const key of [
|
|
132988
|
+
"dependencies",
|
|
132989
|
+
"optionalDependencies",
|
|
132990
|
+
"peerDependencies"
|
|
132991
|
+
]) {
|
|
132992
|
+
const deps = require_dist10.asJsonObject(raw[key]);
|
|
132993
|
+
if (deps === null) continue;
|
|
132994
|
+
for (const [name2, range] of Object.entries(deps)) if (typeof range === "string" && !ranges.has(name2)) ranges.set(name2, range);
|
|
132995
|
+
}
|
|
132996
|
+
const version2 = require_dist10.asString(raw["version"]);
|
|
132997
|
+
return {
|
|
132998
|
+
version: version2.length > 0 ? version2 : null,
|
|
132999
|
+
ranges
|
|
133000
|
+
};
|
|
133001
|
+
}
|
|
133002
|
+
function listTopLevelEntries(nodeModulesDir) {
|
|
133003
|
+
let entries;
|
|
133004
|
+
try {
|
|
133005
|
+
entries = node_fs.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
133006
|
+
} catch {
|
|
133007
|
+
return [];
|
|
133008
|
+
}
|
|
133009
|
+
const names = [];
|
|
133010
|
+
for (const entry of entries) {
|
|
133011
|
+
if (entry.name.startsWith(".")) continue;
|
|
133012
|
+
if (entry.name.startsWith("@")) {
|
|
133013
|
+
let inner;
|
|
133014
|
+
try {
|
|
133015
|
+
inner = node_fs.readdirSync(node_path.join(nodeModulesDir, entry.name), { withFileTypes: true });
|
|
133016
|
+
} catch {
|
|
133017
|
+
continue;
|
|
133018
|
+
}
|
|
133019
|
+
for (const sub of inner) if (!sub.name.startsWith(".")) names.push(`${entry.name}/${sub.name}`);
|
|
133020
|
+
} else names.push(entry.name);
|
|
133021
|
+
}
|
|
133022
|
+
return names;
|
|
133023
|
+
}
|
|
133024
|
+
function collectAddonTreeState(addonDir) {
|
|
133025
|
+
const nodeModulesDir = node_path.join(addonDir, "node_modules");
|
|
133026
|
+
const requirements = /* @__PURE__ */ new Map();
|
|
133027
|
+
const addRange = (name2, range) => {
|
|
133028
|
+
const existing = requirements.get(name2);
|
|
133029
|
+
if (existing === void 0) requirements.set(name2, [range]);
|
|
133030
|
+
else existing.push(range);
|
|
133031
|
+
};
|
|
133032
|
+
const rootManifest = readTreeManifest(node_path.join(addonDir, "package.json"));
|
|
133033
|
+
if (rootManifest !== null) for (const [name2, range] of rootManifest.ranges) addRange(name2, range);
|
|
133034
|
+
const candidates = [];
|
|
133035
|
+
const existingLinks = [];
|
|
133036
|
+
for (const name2 of listTopLevelEntries(nodeModulesDir)) {
|
|
133037
|
+
const entryDir = node_path.join(nodeModulesDir, name2);
|
|
133038
|
+
let isLink = false;
|
|
133039
|
+
try {
|
|
133040
|
+
isLink = node_fs.lstatSync(entryDir).isSymbolicLink();
|
|
133041
|
+
} catch {
|
|
133042
|
+
continue;
|
|
133043
|
+
}
|
|
133044
|
+
if (isLink) {
|
|
133045
|
+
let target = "";
|
|
133046
|
+
try {
|
|
133047
|
+
target = node_fs.readlinkSync(entryDir);
|
|
133048
|
+
} catch {
|
|
133049
|
+
}
|
|
133050
|
+
existingLinks.push({
|
|
133051
|
+
name: name2,
|
|
133052
|
+
target
|
|
133053
|
+
});
|
|
133054
|
+
}
|
|
133055
|
+
const manifest = readTreeManifest(node_path.join(entryDir, "package.json"));
|
|
133056
|
+
if (manifest === null) continue;
|
|
133057
|
+
if (!isLink && manifest.version !== null) candidates.push({
|
|
133058
|
+
name: name2,
|
|
133059
|
+
version: manifest.version
|
|
133060
|
+
});
|
|
133061
|
+
for (const [depName, range] of manifest.ranges) {
|
|
133062
|
+
if (node_fs.existsSync(node_path.join(entryDir, "node_modules", depName))) continue;
|
|
133063
|
+
addRange(depName, range);
|
|
133064
|
+
}
|
|
133065
|
+
}
|
|
133066
|
+
return {
|
|
133067
|
+
candidates,
|
|
133068
|
+
requirements,
|
|
133069
|
+
existingLinks
|
|
133070
|
+
};
|
|
133071
|
+
}
|
|
133072
|
+
function readClosureVersions(closureNodeModulesDir, names) {
|
|
133073
|
+
const versions = /* @__PURE__ */ new Map();
|
|
133074
|
+
for (const name2 of names) {
|
|
133075
|
+
const manifest = readTreeManifest(node_path.join(closureNodeModulesDir, name2, "package.json"));
|
|
133076
|
+
if (manifest !== null && manifest.version !== null) versions.set(name2, manifest.version);
|
|
133077
|
+
}
|
|
133078
|
+
return versions;
|
|
133079
|
+
}
|
|
133080
|
+
function resolveClosureNodeModulesDir(explicitDir) {
|
|
133081
|
+
const frameworkDir = explicitDir ?? process.env["CAMSTACK_FRAMEWORK_DIR"];
|
|
133082
|
+
if (frameworkDir === void 0 || frameworkDir.length === 0) return null;
|
|
133083
|
+
const nodeModulesDir = explicitDir ?? node_path.join(frameworkDir, "node_modules");
|
|
133084
|
+
return node_fs.existsSync(nodeModulesDir) ? nodeModulesDir : null;
|
|
133085
|
+
}
|
|
133086
|
+
function symlinkType() {
|
|
133087
|
+
return process.platform === "win32" ? "junction" : "dir";
|
|
133088
|
+
}
|
|
133089
|
+
function executeClosureDedupe(addonDir, closureNodeModulesDir, plan, logger) {
|
|
133090
|
+
if (plan.links.length === 0) return [];
|
|
133091
|
+
const nodeModulesDir = node_path.join(addonDir, "node_modules");
|
|
133092
|
+
const probePath = node_path.join(nodeModulesDir, ".closure-link-probe");
|
|
133093
|
+
try {
|
|
133094
|
+
node_fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
133095
|
+
node_fs.rmSync(probePath, { force: true });
|
|
133096
|
+
node_fs.symlinkSync(closureNodeModulesDir, probePath, symlinkType());
|
|
133097
|
+
node_fs.rmSync(probePath, { force: true });
|
|
133098
|
+
} catch (err) {
|
|
133099
|
+
logger.warn("closure dedupe skipped \u2014 symlinks unavailable on this volume", { meta: {
|
|
133100
|
+
addonDir,
|
|
133101
|
+
error: require_dist10.errMsg(err)
|
|
133102
|
+
} });
|
|
133103
|
+
return [];
|
|
133104
|
+
}
|
|
133105
|
+
const linked = [];
|
|
133106
|
+
for (const link of plan.links) {
|
|
133107
|
+
const target = node_path.join(closureNodeModulesDir, link.name);
|
|
133108
|
+
const manifest = readTreeManifest(node_path.join(target, "package.json"));
|
|
133109
|
+
if (manifest === null || manifest.version !== link.closureVersion) {
|
|
133110
|
+
logger.warn("closure dedupe \u2014 closure changed under the plan; keeping own copy", { meta: {
|
|
133111
|
+
name: link.name,
|
|
133112
|
+
planned: link.closureVersion,
|
|
133113
|
+
found: manifest?.version ?? null
|
|
133114
|
+
} });
|
|
133115
|
+
continue;
|
|
133116
|
+
}
|
|
133117
|
+
const entryDir = node_path.join(nodeModulesDir, link.name);
|
|
133118
|
+
try {
|
|
133119
|
+
node_fs.rmSync(entryDir, {
|
|
133120
|
+
recursive: true,
|
|
133121
|
+
force: true
|
|
133122
|
+
});
|
|
133123
|
+
node_fs.mkdirSync(node_path.dirname(entryDir), { recursive: true });
|
|
133124
|
+
node_fs.symlinkSync(target, entryDir, symlinkType());
|
|
133125
|
+
linked.push(link.name);
|
|
133126
|
+
} catch (err) {
|
|
133127
|
+
logger.warn("closure dedupe \u2014 link failed; entry removed copy may need reinstall", { meta: {
|
|
133128
|
+
name: link.name,
|
|
133129
|
+
error: require_dist10.errMsg(err)
|
|
133130
|
+
} });
|
|
133131
|
+
}
|
|
133132
|
+
}
|
|
133133
|
+
return linked;
|
|
133134
|
+
}
|
|
133135
|
+
function isClosureLinkTarget(linkTarget, closureNodeModulesDir) {
|
|
133136
|
+
if (linkTarget.length === 0) return false;
|
|
133137
|
+
const normalized = node_path.resolve(linkTarget);
|
|
133138
|
+
const root = node_path.resolve(closureNodeModulesDir);
|
|
133139
|
+
return normalized === root || normalized.startsWith(`${root}${node_path.sep}`);
|
|
133140
|
+
}
|
|
132471
133141
|
var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
133142
|
+
function readNativeDependencyNames(pkg) {
|
|
133143
|
+
if (pkg === void 0) return /* @__PURE__ */ new Set();
|
|
133144
|
+
const camstack = require_dist10.asJsonObject(pkg["camstack"]);
|
|
133145
|
+
if (camstack === null) return /* @__PURE__ */ new Set();
|
|
133146
|
+
const native = require_dist10.asJsonObject(camstack["nativeDependencies"]);
|
|
133147
|
+
return native === null ? /* @__PURE__ */ new Set() : new Set(Object.keys(native));
|
|
133148
|
+
}
|
|
132472
133149
|
function parseInstallSource(value) {
|
|
132473
133150
|
switch (value) {
|
|
132474
133151
|
case "npm":
|
|
@@ -132530,6 +133207,8 @@ var require_dist3 = __commonJS({
|
|
|
132530
133207
|
installSource;
|
|
132531
133208
|
logger;
|
|
132532
133209
|
isPackageResolvable;
|
|
133210
|
+
/** Test seam — production resolves via `CAMSTACK_FRAMEWORK_DIR`. */
|
|
133211
|
+
closureNodeModulesDir;
|
|
132533
133212
|
/**
|
|
132534
133213
|
* Central manifest of installed addons. Tracks version + source +
|
|
132535
133214
|
* timestamps + last-backup pointer per package, written atomically
|
|
@@ -132551,6 +133230,7 @@ var require_dist3 = __commonJS({
|
|
|
132551
133230
|
this.logger = logger;
|
|
132552
133231
|
this.manifest = new AddonManifest(config.addonsDir);
|
|
132553
133232
|
this.isPackageResolvable = config.isPackageResolvable;
|
|
133233
|
+
this.closureNodeModulesDir = config.closureNodeModulesDir;
|
|
132554
133234
|
if (config.installSource) this.installSource = config.installSource;
|
|
132555
133235
|
else this.installSource = "npm";
|
|
132556
133236
|
}
|
|
@@ -132738,6 +133418,140 @@ var require_dist3 = __commonJS({
|
|
|
132738
133418
|
return changes;
|
|
132739
133419
|
}
|
|
132740
133420
|
/**
|
|
133421
|
+
* Make every INSTALLED addon's runtime dependencies resolvable — doing the
|
|
133422
|
+
* least work that achieves it. Runs at boot on hub and agent, which is what
|
|
133423
|
+
* turns a restore back into a working node: a system backup archives no
|
|
133424
|
+
* node_modules (D338), so before this pass every restored non-bootstrap
|
|
133425
|
+
* addon failed its first import until someone redeployed it by hand.
|
|
133426
|
+
*
|
|
133427
|
+
* Per addon, in order:
|
|
133428
|
+
* 1. drop closure links that no longer hold (dangling, or the closure
|
|
133429
|
+
* moved to a version the addon tree's recorded ranges refuse) — those
|
|
133430
|
+
* addons get a reinstall;
|
|
133431
|
+
* 2. a declared dep that already resolves in the addon tree costs nothing;
|
|
133432
|
+
* 3. a missing declared dep the closure provides at a COMPATIBLE version
|
|
133433
|
+
* becomes a link — no npm, no network;
|
|
133434
|
+
* 4. only an addon left with a genuinely unresolvable dep gets an npm
|
|
133435
|
+
* install (then dedupe + native ensure, same tail as a fresh install).
|
|
133436
|
+
*
|
|
133437
|
+
* "Only when necessary" is the contract: an addon whose deps are all
|
|
133438
|
+
* satisfied is not touched at all. Failures are per-addon and non-fatal —
|
|
133439
|
+
* a dep that cannot be re-fetched leaves that addon exactly as broken as
|
|
133440
|
+
* it already was, but LOUDLY, and never blocks the rest of the boot.
|
|
133441
|
+
*/
|
|
133442
|
+
async ensureAddonRuntimeDeps() {
|
|
133443
|
+
const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
|
|
133444
|
+
const linked = [];
|
|
133445
|
+
const reinstalled = [];
|
|
133446
|
+
const failed = [];
|
|
133447
|
+
const installedPackages = this.listInstalled();
|
|
133448
|
+
for (const pkg of installedPackages) {
|
|
133449
|
+
try {
|
|
133450
|
+
if (node_fs.lstatSync(pkg.dir).isSymbolicLink()) continue;
|
|
133451
|
+
} catch {
|
|
133452
|
+
continue;
|
|
133453
|
+
}
|
|
133454
|
+
try {
|
|
133455
|
+
const outcome = this.ensureOneAddonDeps(pkg.dir, pkg.name, closureNodeModules);
|
|
133456
|
+
for (const dep of outcome.linked) linked.push(`${pkg.name}:${dep}`);
|
|
133457
|
+
if (outcome.needsInstall) {
|
|
133458
|
+
await this.reinstallAddonDeps(pkg.dir, pkg.name);
|
|
133459
|
+
reinstalled.push(pkg.name);
|
|
133460
|
+
}
|
|
133461
|
+
} catch (err) {
|
|
133462
|
+
failed.push(pkg.name);
|
|
133463
|
+
this.logger.error(`${pkg.name} \u2014 runtime dependencies could NOT be ensured; the addon will fail to load until its deps install`, { meta: {
|
|
133464
|
+
dir: pkg.dir,
|
|
133465
|
+
error: require_dist10.errMsg(err)
|
|
133466
|
+
} });
|
|
133467
|
+
}
|
|
133468
|
+
}
|
|
133469
|
+
return {
|
|
133470
|
+
checked: installedPackages.length,
|
|
133471
|
+
linked,
|
|
133472
|
+
reinstalled,
|
|
133473
|
+
failed
|
|
133474
|
+
};
|
|
133475
|
+
}
|
|
133476
|
+
/**
|
|
133477
|
+
* One addon's dependency audit (steps 1–3 of `ensureAddonRuntimeDeps`).
|
|
133478
|
+
* Pure decision + link creation; the npm fallback is the caller's.
|
|
133479
|
+
*/
|
|
133480
|
+
ensureOneAddonDeps(addonDir, packageName, closureNodeModules) {
|
|
133481
|
+
const pkgView = readPackageJson(node_path.join(addonDir, "package.json"));
|
|
133482
|
+
const declared = pkgView === null ? null : require_dist10.asJsonObject(pkgView.raw["dependencies"]);
|
|
133483
|
+
const declaredEntries = declared === null ? [] : Object.entries(declared);
|
|
133484
|
+
const state = collectAddonTreeState(addonDir);
|
|
133485
|
+
const nodeModulesDir = node_path.join(addonDir, "node_modules");
|
|
133486
|
+
let needsInstall = false;
|
|
133487
|
+
if (closureNodeModules !== null) for (const link of state.existingLinks) {
|
|
133488
|
+
if (!isClosureLinkTarget(link.target, closureNodeModules)) continue;
|
|
133489
|
+
const linkDir = node_path.join(nodeModulesDir, link.name);
|
|
133490
|
+
const targetManifest = node_fs.existsSync(node_path.join(linkDir, "package.json"));
|
|
133491
|
+
const ranges = state.requirements.get(link.name) ?? [];
|
|
133492
|
+
let holds = targetManifest;
|
|
133493
|
+
if (holds && ranges.length > 0) {
|
|
133494
|
+
const version2 = readClosureVersions(closureNodeModules, [link.name]).get(link.name);
|
|
133495
|
+
holds = version2 !== void 0 && ranges.every((range) => closureSatisfiesRange(range, version2));
|
|
133496
|
+
}
|
|
133497
|
+
if (!holds) {
|
|
133498
|
+
this.logger.warn(`${packageName} \u2014 closure link for ${link.name} no longer holds; reinstalling this addon's deps`, { meta: { target: link.target } });
|
|
133499
|
+
node_fs.rmSync(linkDir, { force: true });
|
|
133500
|
+
needsInstall = true;
|
|
133501
|
+
}
|
|
133502
|
+
}
|
|
133503
|
+
const linked = [];
|
|
133504
|
+
for (const [depName, range] of declaredEntries) {
|
|
133505
|
+
if (typeof range !== "string") continue;
|
|
133506
|
+
if (node_fs.existsSync(node_path.join(nodeModulesDir, depName, "package.json"))) continue;
|
|
133507
|
+
const closureVersion = closureNodeModules === null ? void 0 : readClosureVersions(closureNodeModules, [depName]).get(depName);
|
|
133508
|
+
if (closureNodeModules !== null && closureVersion !== void 0 && closureSatisfiesRange(range, closureVersion)) {
|
|
133509
|
+
if (executeClosureDedupe(addonDir, closureNodeModules, {
|
|
133510
|
+
links: [{
|
|
133511
|
+
name: depName,
|
|
133512
|
+
closureVersion
|
|
133513
|
+
}],
|
|
133514
|
+
keeps: []
|
|
133515
|
+
}, this.logger).length === 1) {
|
|
133516
|
+
this.logger.info(`${packageName} \u2014 ${depName} linked from the server closure`, { meta: { closureVersion } });
|
|
133517
|
+
linked.push(depName);
|
|
133518
|
+
continue;
|
|
133519
|
+
}
|
|
133520
|
+
}
|
|
133521
|
+
needsInstall = true;
|
|
133522
|
+
}
|
|
133523
|
+
return {
|
|
133524
|
+
linked,
|
|
133525
|
+
needsInstall
|
|
133526
|
+
};
|
|
133527
|
+
}
|
|
133528
|
+
/**
|
|
133529
|
+
* npm-install an installed addon's runtime deps IN PLACE (the restore /
|
|
133530
|
+
* repair path — the tree is not yet running at boot), then the same tail
|
|
133531
|
+
* every fresh install gets: closure dedupe + native deps + prebuild ensure.
|
|
133532
|
+
* Closure links are stripped first so npm reifies against plain missing
|
|
133533
|
+
* dirs instead of symlinks it does not own.
|
|
133534
|
+
*/
|
|
133535
|
+
async reinstallAddonDeps(addonDir, packageName) {
|
|
133536
|
+
const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
|
|
133537
|
+
if (closureNodeModules !== null) {
|
|
133538
|
+
const state = collectAddonTreeState(addonDir);
|
|
133539
|
+
for (const link of state.existingLinks) if (isClosureLinkTarget(link.target, closureNodeModules)) node_fs.rmSync(node_path.join(addonDir, "node_modules", link.name), { force: true });
|
|
133540
|
+
}
|
|
133541
|
+
this.logger.info(`${packageName} \u2014 reinstalling runtime dependencies`, { meta: { addonDir } });
|
|
133542
|
+
await require_manifest_system_deps.runNpm([
|
|
133543
|
+
"install",
|
|
133544
|
+
"--omit=dev",
|
|
133545
|
+
"--omit=peer",
|
|
133546
|
+
"--no-audit",
|
|
133547
|
+
"--no-fund",
|
|
133548
|
+
"--no-package-lock",
|
|
133549
|
+
...this.registry ? ["--registry", this.registry] : []
|
|
133550
|
+
], this.npmRunOptions(addonDir, 24e4));
|
|
133551
|
+
this.dedupeTreeAgainstClosure(addonDir, packageName);
|
|
133552
|
+
await this.installNativeDependencies(addonDir, packageName);
|
|
133553
|
+
}
|
|
133554
|
+
/**
|
|
132741
133555
|
* Install a single addon — on-demand installer used by AddonPackageService
|
|
132742
133556
|
* (admin UI "Install addon" / "Reinstall" buttons).
|
|
132743
133557
|
*
|
|
@@ -132845,6 +133659,7 @@ var require_dist3 = __commonJS({
|
|
|
132845
133659
|
logger: this.logger,
|
|
132846
133660
|
bundledSourceDir: sourceDir
|
|
132847
133661
|
});
|
|
133662
|
+
this.dedupeTreeAgainstClosure(targetDir, packageName);
|
|
132848
133663
|
this.logger.info(`${packageName} \u2014 copied from ${sourceDir}`);
|
|
132849
133664
|
return {
|
|
132850
133665
|
name: pkgData.name,
|
|
@@ -133149,6 +133964,40 @@ var require_dist3 = __commonJS({
|
|
|
133149
133964
|
logger: this.logger
|
|
133150
133965
|
});
|
|
133151
133966
|
}
|
|
133967
|
+
/**
|
|
133968
|
+
* Stop copying what the server closure already provides: replace each
|
|
133969
|
+
* top-level `node_modules` entry the closure serves at a COMPATIBLE version
|
|
133970
|
+
* with a symlink into the closure (see `deps/closure-dedupe.ts` for the
|
|
133971
|
+
* plan rules and the live measurements). Runs LAST in tree
|
|
133972
|
+
* materialisation — after the runtime-dep npm install AND the native-dep
|
|
133973
|
+
* steps — so no later npm invocation can fight the links.
|
|
133974
|
+
*
|
|
133975
|
+
* Non-fatal by design: dedupe is an optimisation, and its failure mode is
|
|
133976
|
+
* "the addon keeps its own copies", which is exactly today's behaviour. A
|
|
133977
|
+
* refusal on version grounds is logged loudly — that line is the answer to
|
|
133978
|
+
* "why does this addon still carry its own zod".
|
|
133979
|
+
*/
|
|
133980
|
+
dedupeTreeAgainstClosure(pkgDir, packageName) {
|
|
133981
|
+
const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
|
|
133982
|
+
if (closureNodeModules === null) return;
|
|
133983
|
+
try {
|
|
133984
|
+
const state = collectAddonTreeState(pkgDir);
|
|
133985
|
+
if (state.candidates.length === 0) return;
|
|
133986
|
+
const pkgView = readPackageJson(node_path.join(pkgDir, "package.json"));
|
|
133987
|
+
const plan = planClosureDedupe({
|
|
133988
|
+
candidates: state.candidates,
|
|
133989
|
+
closureVersions: readClosureVersions(closureNodeModules, state.candidates.map((c) => c.name)),
|
|
133990
|
+
requirements: state.requirements,
|
|
133991
|
+
neverLink: readNativeDependencyNames(pkgView?.raw)
|
|
133992
|
+
});
|
|
133993
|
+
const linked = executeClosureDedupe(pkgDir, closureNodeModules, plan, this.logger);
|
|
133994
|
+
if (linked.length > 0) this.logger.info(`${packageName} \u2014 ${linked.length} node_modules entr${linked.length === 1 ? "y" : "ies"} now resolve from the server closure`, { meta: { linked } });
|
|
133995
|
+
const refused = plan.keeps.filter((k2) => k2.reason === "version-incompatible");
|
|
133996
|
+
if (refused.length > 0) this.logger.info(`${packageName} \u2014 kept own copy where the closure version is incompatible`, { meta: { kept: refused.map((k2) => k2.name) } });
|
|
133997
|
+
} catch (err) {
|
|
133998
|
+
this.logger.warn(`${packageName} \u2014 closure dedupe failed (copies kept)`, { meta: { error: require_dist10.errMsg(err) } });
|
|
133999
|
+
}
|
|
134000
|
+
}
|
|
133152
134001
|
/** Install addon from a tgz file (uploaded or downloaded) */
|
|
133153
134002
|
async installFromTgz(tgzPath) {
|
|
133154
134003
|
const stagingRoot = node_path.join(this.addonsDir, ".staging");
|
|
@@ -133174,6 +134023,7 @@ var require_dist3 = __commonJS({
|
|
|
133174
134023
|
const targetDir = node_path.join(this.addonsDir, pkgView.name);
|
|
133175
134024
|
await this.installRuntimeDependencies(pkgDir, pkgView.name);
|
|
133176
134025
|
await this.installNativeDependencies(pkgDir, pkgView.name);
|
|
134026
|
+
this.dedupeTreeAgainstClosure(pkgDir, pkgView.name);
|
|
133177
134027
|
await this.evictInstallDir(targetDir);
|
|
133178
134028
|
ensureDir(node_path.dirname(targetDir));
|
|
133179
134029
|
await node_fs.promises.rename(pkgDir, targetDir);
|
|
@@ -133306,6 +134156,7 @@ var require_dist3 = __commonJS({
|
|
|
133306
134156
|
if (!node_fs.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
|
|
133307
134157
|
await this.installRuntimeDependencies(stagedPath, packageName);
|
|
133308
134158
|
await this.installNativeDependencies(stagedPath, packageName);
|
|
134159
|
+
this.dedupeTreeAgainstClosure(stagedPath, packageName);
|
|
133309
134160
|
const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
133310
134161
|
const backupRoot = node_path.join(this.addonsDir, ".backups", packageName);
|
|
133311
134162
|
ensureDir(backupRoot);
|
|
@@ -163521,7 +164372,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
|
|
|
163521
164372
|
}));
|
|
163522
164373
|
var require_symlink_type2 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports2, module2) => {
|
|
163523
164374
|
var fs2 = require_graceful_fs2();
|
|
163524
|
-
function
|
|
164375
|
+
function symlinkType2(srcpath, type2, callback) {
|
|
163525
164376
|
callback = typeof type2 === "function" ? type2 : callback;
|
|
163526
164377
|
type2 = typeof type2 === "function" ? false : type2;
|
|
163527
164378
|
if (type2) return callback(null, type2);
|
|
@@ -163542,7 +164393,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
|
|
|
163542
164393
|
return stats && stats.isDirectory() ? "dir" : "file";
|
|
163543
164394
|
}
|
|
163544
164395
|
module2.exports = {
|
|
163545
|
-
symlinkType,
|
|
164396
|
+
symlinkType: symlinkType2,
|
|
163546
164397
|
symlinkTypeSync
|
|
163547
164398
|
};
|
|
163548
164399
|
}));
|
|
@@ -163557,7 +164408,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
|
|
|
163557
164408
|
var symlinkPaths = _symlinkPaths.symlinkPaths;
|
|
163558
164409
|
var symlinkPathsSync = _symlinkPaths.symlinkPathsSync;
|
|
163559
164410
|
var _symlinkType = require_symlink_type2();
|
|
163560
|
-
var
|
|
164411
|
+
var symlinkType2 = _symlinkType.symlinkType;
|
|
163561
164412
|
var symlinkTypeSync = _symlinkType.symlinkTypeSync;
|
|
163562
164413
|
var pathExists = require_path_exists2().pathExists;
|
|
163563
164414
|
function createSymlink(srcpath, dstpath, type2, callback) {
|
|
@@ -163569,7 +164420,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
|
|
|
163569
164420
|
symlinkPaths(srcpath, dstpath, (err2, relative) => {
|
|
163570
164421
|
if (err2) return callback(err2);
|
|
163571
164422
|
srcpath = relative.toDst;
|
|
163572
|
-
|
|
164423
|
+
symlinkType2(relative.toCwd, type2, (err3, type3) => {
|
|
163573
164424
|
if (err3) return callback(err3);
|
|
163574
164425
|
const dir = path$23.dirname(dstpath);
|
|
163575
164426
|
pathExists(dir, (err4, dirExists) => {
|
|
@@ -210088,7 +210939,7 @@ var require_dist4 = __commonJS({
|
|
|
210088
210939
|
"use strict";
|
|
210089
210940
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
210090
210941
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
210091
|
-
var require_sleep =
|
|
210942
|
+
var require_sleep = require_sleep_jdpPltQH();
|
|
210092
210943
|
var require_canonical_hash = require_canonical_hash_DNV8S5ET();
|
|
210093
210944
|
var require_enums2 = require_enums();
|
|
210094
210945
|
var require_err_msg = require_err_msg_COpsHMw2();
|
|
@@ -214698,6 +215549,68 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
214698
215549
|
fileCount: zod.z.number(),
|
|
214699
215550
|
present: zod.z.boolean()
|
|
214700
215551
|
});
|
|
215552
|
+
var BackupRunStateSchema = zod.z.enum([
|
|
215553
|
+
"queued",
|
|
215554
|
+
"running",
|
|
215555
|
+
"succeeded",
|
|
215556
|
+
"failed",
|
|
215557
|
+
"cancelled"
|
|
215558
|
+
]);
|
|
215559
|
+
var BackupRunPhaseSchema = zod.z.enum([
|
|
215560
|
+
"queued",
|
|
215561
|
+
"building",
|
|
215562
|
+
"uploading",
|
|
215563
|
+
"done"
|
|
215564
|
+
]);
|
|
215565
|
+
var BackupRunSchema = zod.z.object({
|
|
215566
|
+
/** Stable run id — the handle `backup.cancel` takes. */
|
|
215567
|
+
id: zod.z.string(),
|
|
215568
|
+
state: BackupRunStateSchema,
|
|
215569
|
+
phase: BackupRunPhaseSchema,
|
|
215570
|
+
/**
|
|
215571
|
+
* Resolved destination location ids. Empty while queued (targets are
|
|
215572
|
+
* resolved when the run starts, against the then-current policies).
|
|
215573
|
+
*/
|
|
215574
|
+
destinationIds: zod.z.array(zod.z.string()).readonly(),
|
|
215575
|
+
label: zod.z.string().optional(),
|
|
215576
|
+
/** ms-epoch when the run was submitted (trigger call / schedule fire). */
|
|
215577
|
+
requestedAt: zod.z.number(),
|
|
215578
|
+
/** ms-epoch when the run left the queue and started building. */
|
|
215579
|
+
startedAt: zod.z.number().optional(),
|
|
215580
|
+
/** ms-epoch when the run reached a terminal state. */
|
|
215581
|
+
finishedAt: zod.z.number().optional(),
|
|
215582
|
+
/** Compressed bytes of the staging archive written so far. */
|
|
215583
|
+
stagedBytes: zod.z.number(),
|
|
215584
|
+
/** Final staged archive size, once the build phase completes. */
|
|
215585
|
+
archiveSizeBytes: zod.z.number().optional(),
|
|
215586
|
+
/** Bytes pushed to the destination currently uploading. */
|
|
215587
|
+
uploadedBytes: zod.z.number(),
|
|
215588
|
+
/** Destinations where the archive fully landed (uploaded + indexed). */
|
|
215589
|
+
completedDestinationIds: zod.z.array(zod.z.string()).readonly(),
|
|
215590
|
+
/** Destinations that failed during the fan-out. */
|
|
215591
|
+
failedDestinationIds: zod.z.array(zod.z.string()).readonly(),
|
|
215592
|
+
/** Failure message when `state === 'failed'`. */
|
|
215593
|
+
error: zod.z.string().optional(),
|
|
215594
|
+
/**
|
|
215595
|
+
* 1-based place in the execution queue — 1 = runs next. Present only
|
|
215596
|
+
* while `state === 'queued'`. Stamped by the orchestrator from the
|
|
215597
|
+
* queue's OWN pending order, never derived from timestamps, so the
|
|
215598
|
+
* UI cannot show an order the executor will not honour.
|
|
215599
|
+
*/
|
|
215600
|
+
queuePosition: zod.z.number().int().min(1).optional()
|
|
215601
|
+
});
|
|
215602
|
+
var BackupTriggerResultSchema = zod.z.object({
|
|
215603
|
+
/** The run this trigger mapped to — poll it via `listRuns`, stop it via `cancel`. */
|
|
215604
|
+
runId: zod.z.string(),
|
|
215605
|
+
/** True when the run waited behind an in-flight run instead of starting immediately. */
|
|
215606
|
+
queued: zod.z.boolean(),
|
|
215607
|
+
/** True when this trigger was coalesced onto an identical already-queued run. */
|
|
215608
|
+
joined: zod.z.boolean(),
|
|
215609
|
+
/** True when the run was cancelled before completing every destination. */
|
|
215610
|
+
cancelled: zod.z.boolean(),
|
|
215611
|
+
/** One entry per destination the archive landed at (partial on cancel). */
|
|
215612
|
+
entries: zod.z.array(BackupEntrySchema).readonly()
|
|
215613
|
+
});
|
|
214701
215614
|
var BackupScheduleSchema = zod.z.object({
|
|
214702
215615
|
/** Stable id. Generated by the orchestrator on first upsert if absent. */
|
|
214703
215616
|
id: zod.z.string(),
|
|
@@ -214734,6 +215647,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
214734
215647
|
* Trigger a backup. Without `destinations` the orchestrator fans
|
|
214735
215648
|
* out to every destination flagged as enabled in the routing
|
|
214736
215649
|
* config; with it, only the listed addons receive the archive.
|
|
215650
|
+
*
|
|
215651
|
+
* At most ONE backup run executes at a time — the source tree and
|
|
215652
|
+
* the staging disk are shared by every run, so a second trigger
|
|
215653
|
+
* while one is in flight is enqueued (or joined onto an identical
|
|
215654
|
+
* queued run) and the result says so. See D342.
|
|
214737
215655
|
*/
|
|
214738
215656
|
trigger: require_sleep.method(zod.z.object({
|
|
214739
215657
|
/** Subset of registered `backup-destination` addon ids to write to. */
|
|
@@ -214747,7 +215665,30 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
214747
215665
|
* retention (manual runs).
|
|
214748
215666
|
*/
|
|
214749
215667
|
retentionCount: zod.z.number().int().min(1).max(1e3).optional()
|
|
214750
|
-
}).optional(),
|
|
215668
|
+
}).optional(), BackupTriggerResultSchema, {
|
|
215669
|
+
kind: "mutation",
|
|
215670
|
+
auth: "admin"
|
|
215671
|
+
}),
|
|
215672
|
+
/**
|
|
215673
|
+
* Every run the orchestrator knows about, in EXECUTION order: the
|
|
215674
|
+
* running run first, then queued runs in the exact order they will
|
|
215675
|
+
* execute (each with `queuePosition`, 1 = next), then the bounded
|
|
215676
|
+
* finished history newest-first. Only one run executes at a time
|
|
215677
|
+
* (D342) — the queued section IS the line. Each run carries live
|
|
215678
|
+
* phase + byte counters so a runaway build is visible in seconds,
|
|
215679
|
+
* not via `du`.
|
|
215680
|
+
*/
|
|
215681
|
+
listRuns: require_sleep.method(zod.z.void(), zod.z.array(BackupRunSchema).readonly(), { auth: "admin" }),
|
|
215682
|
+
/**
|
|
215683
|
+
* Stop a backup run. Mirrors `storage-migration.cancel` semantics:
|
|
215684
|
+
* id in, `{ cancelled }` out — `false` when the run is unknown or
|
|
215685
|
+
* already terminal. A QUEUED run is removed before it ever starts;
|
|
215686
|
+
* the RUNNING run has its tar/upload stream actually aborted, the
|
|
215687
|
+
* half-written staging archive is deleted, and the in-flight
|
|
215688
|
+
* destination upload is aborted server-side (partial discarded).
|
|
215689
|
+
* Destinations that already completed keep their archive.
|
|
215690
|
+
*/
|
|
215691
|
+
cancel: require_sleep.method(zod.z.object({ runId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
|
|
214751
215692
|
kind: "mutation",
|
|
214752
215693
|
auth: "admin"
|
|
214753
215694
|
}),
|
|
@@ -215487,6 +216428,38 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
215487
216428
|
auth: "admin"
|
|
215488
216429
|
}),
|
|
215489
216430
|
/**
|
|
216431
|
+
* The HARDWARE behind a device number was replaced
|
|
216432
|
+
* (`deviceManager.migrateDevice`). Forget every piece of broker state that
|
|
216433
|
+
* described the old box, so the next catalog pull derives everything from
|
|
216434
|
+
* the camera that is actually there:
|
|
216435
|
+
*
|
|
216436
|
+
* - every `derived:*` stream definition — a derived is authored against a
|
|
216437
|
+
* specific profile layout, and against the wrong hardware its feeder
|
|
216438
|
+
* respawns forever (observed at attempt 2732 on the live hub,
|
|
216439
|
+
* 2026-09-01);
|
|
216440
|
+
* - the profile-slot assignment entry, PURGED (not unassigned — unassign
|
|
216441
|
+
* marks the slot manual, which would pin the stale choice instead of
|
|
216442
|
+
* letting `computeInitialAssignment` re-derive it);
|
|
216443
|
+
* - the probe snapshots (`<deviceId>/…` — probed codec/resolution of the
|
|
216444
|
+
* old hardware);
|
|
216445
|
+
* - the persisted RTSP token rows for the device's brokers (keyed
|
|
216446
|
+
* `<deviceId>/<camStreamId>`; the stream ids change with the hardware,
|
|
216447
|
+
* so the rows are dead URLs).
|
|
216448
|
+
*
|
|
216449
|
+
* An RPC, deliberately — an event is telemetry and may be dropped (D8),
|
|
216450
|
+
* and a dropped forget leaves a feeder respawning against a stream that
|
|
216451
|
+
* does not exist.
|
|
216452
|
+
*/
|
|
216453
|
+
forgetDeviceHardware: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.object({
|
|
216454
|
+
derivedStreamsDeleted: zod.z.array(zod.z.string()).readonly(),
|
|
216455
|
+
assignmentsPurged: zod.z.boolean(),
|
|
216456
|
+
probeSnapshotsDropped: zod.z.number().int().nonnegative(),
|
|
216457
|
+
rtspTokenRowsDeleted: zod.z.number().int().nonnegative()
|
|
216458
|
+
}), {
|
|
216459
|
+
kind: "mutation",
|
|
216460
|
+
auth: "admin"
|
|
216461
|
+
}),
|
|
216462
|
+
/**
|
|
215490
216463
|
* Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
|
|
215491
216464
|
*
|
|
215492
216465
|
* The pre-buffer is the only source that already holds the seconds BEFORE
|
|
@@ -217311,6 +218284,35 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
217311
218284
|
name: zod.z.string(),
|
|
217312
218285
|
type: zod.z.string()
|
|
217313
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
|
+
}),
|
|
217314
218316
|
supportsDiscovery: require_sleep.method(zod.z.object({}), zod.z.boolean()),
|
|
217315
218317
|
/**
|
|
217316
218318
|
* Run a network scan. `params` carries optional provider-specific scan
|
|
@@ -217632,13 +218634,21 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
217632
218634
|
* `sourceStillLive` names what stayed live; empty is the only value that
|
|
217633
218635
|
* means the old hardware is quiet. The migration proceeds either way —
|
|
217634
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.
|
|
217635
218644
|
*/
|
|
217636
218645
|
migrateDevice: require_sleep.method(zod.z.object({
|
|
217637
218646
|
sourceId: zod.z.number(),
|
|
217638
218647
|
targetId: zod.z.number()
|
|
217639
218648
|
}), MigrateDeviceResultSchema, {
|
|
217640
218649
|
kind: "mutation",
|
|
217641
|
-
auth: "admin"
|
|
218650
|
+
auth: "admin",
|
|
218651
|
+
timeoutMs: 12 * 6e4
|
|
217642
218652
|
}),
|
|
217643
218653
|
/** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
|
|
217644
218654
|
registerDevice: require_sleep.method(DeviceRegisterPayloadSchema, zod.z.void(), { kind: "mutation" }),
|
|
@@ -238323,6 +239333,138 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
238323
239333
|
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
238324
239334
|
}
|
|
238325
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
|
+
};
|
|
238326
239468
|
function toDeviceSummary(device2, addonId) {
|
|
238327
239469
|
const config = {};
|
|
238328
239470
|
for (const entry of device2.config.entries()) config[entry.key] = entry.value;
|
|
@@ -238348,6 +239490,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
238348
239490
|
}];
|
|
238349
239491
|
}
|
|
238350
239492
|
async onShutdown() {
|
|
239493
|
+
this.cancelRestoreRetries();
|
|
238351
239494
|
const devices = await this.ctx.kernel.devices?.getAll() ?? [];
|
|
238352
239495
|
for (const device2 of devices) try {
|
|
238353
239496
|
await this.ctx.kernel.devices?.decommission(device2.id);
|
|
@@ -238367,9 +239510,16 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
238367
239510
|
async stop() {
|
|
238368
239511
|
}
|
|
238369
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
|
+
};
|
|
238370
239519
|
return {
|
|
238371
239520
|
connected: true,
|
|
238372
|
-
deviceCount:
|
|
239521
|
+
deviceCount: all.length,
|
|
239522
|
+
error: summary
|
|
238373
239523
|
};
|
|
238374
239524
|
}
|
|
238375
239525
|
async getDevices() {
|
|
@@ -238459,8 +239609,137 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
238459
239609
|
};
|
|
238460
239610
|
}
|
|
238461
239611
|
async restoreDevices(savedDevices) {
|
|
238462
|
-
await this.onRestoreDevices(savedDevices);
|
|
238463
|
-
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 };
|
|
238464
239743
|
}
|
|
238465
239744
|
/**
|
|
238466
239745
|
* Restore devices from persisted state. Two-pass:
|
|
@@ -238486,55 +239765,108 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
238486
239765
|
* accessory-spawn flow handles via the parent's
|
|
238487
239766
|
* `getAccessoryChildren()`. Override only when the default doesn't
|
|
238488
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`.
|
|
238489
239774
|
*/
|
|
238490
239775
|
async onRestoreDevices(savedDevices) {
|
|
238491
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
|
+
};
|
|
238492
239786
|
const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
|
|
238493
239787
|
const restoreOne = async (saved) => {
|
|
238494
|
-
|
|
238495
|
-
if (!Class) {
|
|
239788
|
+
if (!this.deviceClasses[saved.type]) {
|
|
238496
239789
|
this.ctx.logger.warn("No device class registered for restored type \u2014 skipping", {
|
|
238497
|
-
tags: {
|
|
239790
|
+
tags: {
|
|
239791
|
+
deviceId: saved.id,
|
|
239792
|
+
stableId: saved.stableId
|
|
239793
|
+
},
|
|
238498
239794
|
meta: { type: saved.type }
|
|
238499
239795
|
});
|
|
238500
239796
|
return;
|
|
238501
239797
|
}
|
|
238502
239798
|
try {
|
|
238503
|
-
await
|
|
238504
|
-
restored.add(saved.id);
|
|
239799
|
+
await attemptRestore(saved);
|
|
238505
239800
|
} catch (err) {
|
|
238506
|
-
|
|
238507
|
-
|
|
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
|
+
},
|
|
238508
239807
|
meta: {
|
|
238509
239808
|
type: saved.type,
|
|
238510
|
-
|
|
239809
|
+
attempt: 1,
|
|
239810
|
+
error
|
|
238511
239811
|
}
|
|
238512
239812
|
});
|
|
239813
|
+
failures.push({
|
|
239814
|
+
saved,
|
|
239815
|
+
error
|
|
239816
|
+
});
|
|
238513
239817
|
}
|
|
238514
239818
|
};
|
|
238515
239819
|
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
239820
|
+
const failedTopLevelIds = new Set(failures.map((failure) => failure.saved.id));
|
|
238516
239821
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
238517
239822
|
for (const saved of childRows) {
|
|
238518
|
-
|
|
238519
|
-
if (!Class) continue;
|
|
239823
|
+
if (!this.deviceClasses[saved.type]) continue;
|
|
238520
239824
|
if (saved.parentDeviceId === null) continue;
|
|
238521
|
-
if (
|
|
238522
|
-
|
|
238523
|
-
|
|
238524
|
-
|
|
238525
|
-
|
|
238526
|
-
|
|
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", {
|
|
238527
239851
|
tags: {
|
|
239852
|
+
deviceId: saved.id,
|
|
238528
239853
|
stableId: saved.stableId,
|
|
238529
239854
|
parentDeviceId: saved.parentDeviceId
|
|
238530
239855
|
},
|
|
238531
|
-
meta: {
|
|
238532
|
-
type: saved.type,
|
|
238533
|
-
error: err instanceof Error ? err.message : String(err)
|
|
238534
|
-
}
|
|
239856
|
+
meta: { type: saved.type }
|
|
238535
239857
|
});
|
|
239858
|
+
failures.push({
|
|
239859
|
+
saved,
|
|
239860
|
+
error: `parent device ${saved.parentDeviceId} not restored`
|
|
239861
|
+
});
|
|
239862
|
+
continue;
|
|
238536
239863
|
}
|
|
238537
239864
|
}
|
|
239865
|
+
if (failures.length > 0) this.scheduleRestoreRetries(failures, attemptRestore);
|
|
239866
|
+
return {
|
|
239867
|
+
restoredCount: restored.size,
|
|
239868
|
+
failedCount: failures.length
|
|
239869
|
+
};
|
|
238538
239870
|
}
|
|
238539
239871
|
/** Convert an IDevice to the flat DeviceSummary for the cap router. */
|
|
238540
239872
|
toSummary(device2) {
|
|
@@ -241376,6 +242708,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
241376
242708
|
addonId: null,
|
|
241377
242709
|
access: "create"
|
|
241378
242710
|
},
|
|
242711
|
+
"backup.cancel": {
|
|
242712
|
+
capName: "backup",
|
|
242713
|
+
capScope: "system",
|
|
242714
|
+
addonId: null,
|
|
242715
|
+
access: "create"
|
|
242716
|
+
},
|
|
241379
242717
|
"backup.delete": {
|
|
241380
242718
|
capName: "backup",
|
|
241381
242719
|
capScope: "system",
|
|
@@ -241418,6 +242756,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
241418
242756
|
addonId: null,
|
|
241419
242757
|
access: "view"
|
|
241420
242758
|
},
|
|
242759
|
+
"backup.listRuns": {
|
|
242760
|
+
capName: "backup",
|
|
242761
|
+
capScope: "system",
|
|
242762
|
+
addonId: null,
|
|
242763
|
+
access: "view"
|
|
242764
|
+
},
|
|
241421
242765
|
"backup.listSchedules": {
|
|
241422
242766
|
capName: "backup",
|
|
241423
242767
|
capScope: "system",
|
|
@@ -242618,6 +243962,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
242618
243962
|
addonId: null,
|
|
242619
243963
|
access: "view"
|
|
242620
243964
|
},
|
|
243965
|
+
"deviceProvider.reloadDevice": {
|
|
243966
|
+
capName: "device-provider",
|
|
243967
|
+
capScope: "system",
|
|
243968
|
+
addonId: null,
|
|
243969
|
+
access: "create"
|
|
243970
|
+
},
|
|
242621
243971
|
"deviceProvider.start": {
|
|
242622
243972
|
capName: "device-provider",
|
|
242623
243973
|
capScope: "system",
|
|
@@ -245984,6 +247334,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
245984
247334
|
addonId: null,
|
|
245985
247335
|
access: "create"
|
|
245986
247336
|
},
|
|
247337
|
+
"streamBroker.forgetDeviceHardware": {
|
|
247338
|
+
capName: "stream-broker",
|
|
247339
|
+
capScope: "system",
|
|
247340
|
+
addonId: null,
|
|
247341
|
+
access: "delete"
|
|
247342
|
+
},
|
|
245987
247343
|
"streamBroker.getAllRtspEntries": {
|
|
245988
247344
|
capName: "stream-broker",
|
|
245989
247345
|
capScope: "system",
|
|
@@ -248700,6 +250056,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
248700
250056
|
form: "single",
|
|
248701
250057
|
optional: false
|
|
248702
250058
|
}],
|
|
250059
|
+
"streamBroker.forgetDeviceHardware": [{
|
|
250060
|
+
name: "deviceId",
|
|
250061
|
+
form: "single",
|
|
250062
|
+
optional: false
|
|
250063
|
+
}],
|
|
248703
250064
|
"streamBroker.getDeviceAudioMute": [{
|
|
248704
250065
|
name: "deviceId",
|
|
248705
250066
|
form: "single",
|
|
@@ -249071,6 +250432,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
249071
250432
|
"recordingExport.listExports",
|
|
249072
250433
|
"streamBroker.acquireEgressTranscode",
|
|
249073
250434
|
"streamBroker.assignProfile",
|
|
250435
|
+
"streamBroker.forgetDeviceHardware",
|
|
249074
250436
|
"streamBroker.getDeviceAudioMute",
|
|
249075
250437
|
"streamBroker.getStreamWithCodec",
|
|
249076
250438
|
"streamBroker.produceEventMedia",
|
|
@@ -249569,6 +250931,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
249569
250931
|
backup: {
|
|
249570
250932
|
listDestinations: (input) => dispatch("backup", "listDestinations", "query", input),
|
|
249571
250933
|
trigger: (input) => dispatch("backup", "trigger", "mutation", input),
|
|
250934
|
+
listRuns: (input) => dispatch("backup", "listRuns", "query", input),
|
|
250935
|
+
cancel: (input) => dispatch("backup", "cancel", "mutation", input),
|
|
249572
250936
|
list: (input) => dispatch("backup", "list", "query", input),
|
|
249573
250937
|
listLocations: (input) => dispatch("backup", "listLocations", "query", input),
|
|
249574
250938
|
getEntries: (input) => dispatch("backup", "getEntries", "query", input),
|
|
@@ -252848,6 +254212,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
252848
254212
|
exports.BacklightModeSchema = BacklightModeSchema;
|
|
252849
254213
|
exports.BackupDestinationInfoSchema = BackupDestinationInfoSchema;
|
|
252850
254214
|
exports.BackupEntrySchema = BackupEntrySchema;
|
|
254215
|
+
exports.BackupRunPhaseSchema = BackupRunPhaseSchema;
|
|
254216
|
+
exports.BackupRunSchema = BackupRunSchema;
|
|
254217
|
+
exports.BackupRunStateSchema = BackupRunStateSchema;
|
|
254218
|
+
exports.BackupTriggerResultSchema = BackupTriggerResultSchema;
|
|
252851
254219
|
exports.BaseAddon = require_sleep.BaseAddon;
|
|
252852
254220
|
exports.BaseDevice = BaseDevice;
|
|
252853
254221
|
exports.BaseDeviceProvider = BaseDeviceProvider;
|
|
@@ -253004,6 +254372,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
253004
254372
|
exports.DEVICE_CAP_NAMES = DEVICE_CAP_NAMES;
|
|
253005
254373
|
exports.DEVICE_CHILDREN_BATCH_MAX = DEVICE_CHILDREN_BATCH_MAX;
|
|
253006
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;
|
|
253007
254377
|
exports.DEVICE_SCOPED_CAPS = require_sleep.DEVICE_SCOPED_CAPS;
|
|
253008
254378
|
exports.DEVICE_SETTINGS_CONTRIBUTION_METHODS = require_sleep.DEVICE_SETTINGS_CONTRIBUTION_METHODS;
|
|
253009
254379
|
exports.DEVICE_STATE_READERS = DEVICE_STATE_READERS;
|
|
@@ -253033,6 +254403,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
253033
254403
|
exports.DeviceFeature = require_sleep.DeviceFeature;
|
|
253034
254404
|
exports.DeviceInfoSchema = DeviceInfoSchema;
|
|
253035
254405
|
exports.DeviceNetworkStatsSchema = DeviceNetworkStatsSchema;
|
|
254406
|
+
exports.DeviceRestoreRetryScheduler = DeviceRestoreRetryScheduler;
|
|
253036
254407
|
exports.DeviceRole = require_sleep.DeviceRole;
|
|
253037
254408
|
exports.DeviceRuntimeState = DeviceRuntimeState;
|
|
253038
254409
|
exports.DeviceSelectorSchema = DeviceSelectorSchema;
|
|
@@ -387819,6 +389190,16 @@ var require_main2 = __commonJS({
|
|
|
387819
389190
|
installSource: resolvedSource
|
|
387820
389191
|
});
|
|
387821
389192
|
await installer.ensureRequiredPackages(system_1.AddonInstaller.AGENT_PACKAGES);
|
|
389193
|
+
if (typeof installer.ensureAddonRuntimeDeps === "function") {
|
|
389194
|
+
try {
|
|
389195
|
+
const depsResult = await installer.ensureAddonRuntimeDeps();
|
|
389196
|
+
if (depsResult.linked.length > 0 || depsResult.reinstalled.length > 0 || depsResult.failed.length > 0) {
|
|
389197
|
+
console.log(`[Agent] Addon runtime deps ensured \u2014 linked: [${depsResult.linked.join(", ")}], reinstalled: [${depsResult.reinstalled.join(", ")}], failed: [${depsResult.failed.join(", ")}] (${depsResult.checked} checked)`);
|
|
389198
|
+
}
|
|
389199
|
+
} catch (err) {
|
|
389200
|
+
console.warn(`[Agent] ensureAddonRuntimeDeps failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
389201
|
+
}
|
|
389202
|
+
}
|
|
387822
389203
|
console.log(`[Agent] Addon packages ready in ${config.addonsDir}`);
|
|
387823
389204
|
let broker = (0, system_1.createBroker)({
|
|
387824
389205
|
nodeID: config.nodeId,
|
|
@@ -427674,6 +429055,18 @@ var require_launcher = __commonJS({
|
|
|
427674
429055
|
console.warn(`[launcher] first-boot addon plan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
427675
429056
|
}
|
|
427676
429057
|
await installer.ensureRequiredPackages(bootstrapRequired);
|
|
429058
|
+
if (typeof installer.ensureAddonRuntimeDeps === "function") {
|
|
429059
|
+
try {
|
|
429060
|
+
const depsResult = await installer.ensureAddonRuntimeDeps();
|
|
429061
|
+
if (depsResult.linked.length > 0 || depsResult.reinstalled.length > 0 || depsResult.failed.length > 0) {
|
|
429062
|
+
console.log(`[launcher] Addon runtime deps ensured \u2014 linked: [${depsResult.linked.join(", ")}], reinstalled: [${depsResult.reinstalled.join(", ")}], failed: [${depsResult.failed.join(", ")}] (${depsResult.checked} checked)`);
|
|
429063
|
+
}
|
|
429064
|
+
} catch (err) {
|
|
429065
|
+
console.warn(`[launcher] ensureAddonRuntimeDeps failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
429066
|
+
}
|
|
429067
|
+
} else {
|
|
429068
|
+
console.warn("[launcher] ensureAddonRuntimeDeps unavailable \u2014 skipping addon dep repair");
|
|
429069
|
+
}
|
|
427677
429070
|
if (typeof installer.reconcileManifest === "function") {
|
|
427678
429071
|
const reconciled = installer.reconcileManifest();
|
|
427679
429072
|
if (reconciled > 0) {
|