@camstack/addon-pipeline-orchestrator 1.2.128 → 1.2.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8894,6 +8894,20 @@ var RelocateJobSchema = object({
8894
8894
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8895
8895
  */
8896
8896
  rowsReconciled: number().int().nonnegative().optional(),
8897
+ /**
8898
+ * Rows this run FORGOT because the file they name is not on disk.
8899
+ *
8900
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8901
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8902
+ * and the durable row is dropped through the same channel eviction uses. It
8903
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8904
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8905
+ * the same failure as one that quietly skips them (D295).
8906
+ *
8907
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8908
+ * ledger claimed 5.65 GB of footage that no longer existed.
8909
+ */
8910
+ rowsForgotten: number().int().nonnegative().optional(),
8897
8911
  startedAt: number(),
8898
8912
  finishedAt: number().nullable(),
8899
8913
  error: string().nullable()
@@ -9257,6 +9271,91 @@ var RelocateResidueSchema = object({
9257
9271
  segments: number().int().nonnegative(),
9258
9272
  bytes: number().int().nonnegative()
9259
9273
  }).nullable();
9274
+ /**
9275
+ * Ask one location whether its durable hour rows describe the disk — the walk
9276
+ * (D319).
9277
+ *
9278
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
9279
+ * missing tool is the question, and the dry run is how they sanity-check the
9280
+ * destructive run before authorising it.
9281
+ */
9282
+ var LedgerWalkInputSchema = object({
9283
+ locationId: string().min(1),
9284
+ /** Forget the confirmed-absent rows, rather than only counting them. */
9285
+ apply: boolean().optional(),
9286
+ /** Narrow to one camera. */
9287
+ deviceId: number().int().positive().optional(),
9288
+ /** Narrow to these recording profiles; empty/absent = every profile. */
9289
+ profiles: array(string().min(1)).optional()
9290
+ });
9291
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
9292
+ var LedgerWalkRefusalSchema = _enum([
9293
+ "location-unknown",
9294
+ "source-writable",
9295
+ "no-ledger",
9296
+ "archive-unreadable",
9297
+ "anchor-absent",
9298
+ "anchor-unreadable",
9299
+ "anchor-moved"
9300
+ ]);
9301
+ _enum([
9302
+ "live-tail",
9303
+ "listing-error",
9304
+ "path-mismatch",
9305
+ "durable-refused"
9306
+ ]);
9307
+ /** Every skip reason, always present, always a number — so a reason that never
9308
+ * fired reports as zero rather than absent and the report shape is constant
9309
+ * between passes. Spelled out rather than `z.record` for exactly that. */
9310
+ var LedgerWalkSkipCountsSchema = object({
9311
+ "live-tail": number().int().nonnegative(),
9312
+ "listing-error": number().int().nonnegative(),
9313
+ "path-mismatch": number().int().nonnegative(),
9314
+ "durable-refused": number().int().nonnegative()
9315
+ });
9316
+ /** One camera's share of a walk, so a report names cameras and not rows. */
9317
+ var LedgerWalkDeviceReportSchema = object({
9318
+ deviceId: number().int(),
9319
+ hoursWalked: number().int().nonnegative(),
9320
+ hoursMissing: number().int().nonnegative(),
9321
+ ghostSegments: number().int().nonnegative(),
9322
+ ghostBytes: number().int().nonnegative(),
9323
+ forgottenSegments: number().int().nonnegative(),
9324
+ orphanFiles: number().int().nonnegative()
9325
+ });
9326
+ /**
9327
+ * What one walk claimed, listed, found and (only when armed) forgot.
9328
+ *
9329
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
9330
+ * walk that saw a fraction of the location is visible in its own report rather
9331
+ * than in the absence of one.
9332
+ */
9333
+ var LedgerWalkReportSchema = object({
9334
+ locationId: string(),
9335
+ applied: boolean(),
9336
+ refused: LedgerWalkRefusalSchema.nullable(),
9337
+ archiveSegments: number().int().nonnegative().nullable(),
9338
+ archiveBytes: number().int().nonnegative().nullable(),
9339
+ hoursClaimed: number().int().nonnegative(),
9340
+ hoursWalked: number().int().nonnegative(),
9341
+ hoursMissing: number().int().nonnegative(),
9342
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
9343
+ listings: number().int().nonnegative(),
9344
+ segmentsClaimed: number().int().nonnegative(),
9345
+ ghostSegments: number().int().nonnegative(),
9346
+ ghostBytes: number().int().nonnegative(),
9347
+ ghostHoursWhole: number().int().nonnegative(),
9348
+ forgottenSegments: number().int().nonnegative(),
9349
+ forgottenBytes: number().int().nonnegative(),
9350
+ /** Files under a claimed hour that no durable row names. Never deleted. */
9351
+ orphanFiles: number().int().nonnegative(),
9352
+ orphanSample: array(string()).readonly(),
9353
+ hoursSkipped: number().int().nonnegative(),
9354
+ skippedByReason: LedgerWalkSkipCountsSchema,
9355
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
9356
+ bounded: boolean(),
9357
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
9358
+ });
9260
9359
  /** How many rows a media pass would still act on against a given target — the
9261
9360
  * media lane's denominator AND its residue, from ONE derivation so the two can
9262
9361
  * never disagree. `null` = the count could not be taken. */
@@ -19833,13 +19932,15 @@ var ListGroupsPageSchema = object({
19833
19932
  groups: array(AnalyticsGroupRecordSchema).readonly(),
19834
19933
  nextCursor: string().nullable()
19835
19934
  });
19935
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19936
+ var KEY_EVENTS_MAX_LIMIT = 200;
19836
19937
  var KeyEventQueryInput = object({
19837
19938
  deviceId: number(),
19838
19939
  /** Window lower bound (track firstSeen ≥ since). */
19839
19940
  since: number(),
19840
19941
  /** Window upper bound (track firstSeen ≤ until). */
19841
19942
  until: number(),
19842
- limit: number().int().min(1).max(200).default(50),
19943
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19843
19944
  /** Drop tracks scoring below this importance. */
19844
19945
  minImportance: number().min(0).max(1).optional(),
19845
19946
  /** Restrict to a single class (e.g. 'person'). */
@@ -19861,6 +19962,32 @@ var KeyEventSchema = object({
19861
19962
  ...TrackFlagFields,
19862
19963
  ...TrackRetrainFields
19863
19964
  });
19965
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19966
+ var KeyEventBatchQueryInput = object({
19967
+ deviceIds: array(number()).min(1).max(200),
19968
+ since: number(),
19969
+ until: number(),
19970
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19971
+ * across the set, which would let a busy camera starve a quiet one of its
19972
+ * rows and change what the merged feed contains. */
19973
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19974
+ minImportance: number().min(0).max(1).optional(),
19975
+ classFilter: string().optional()
19976
+ });
19977
+ /**
19978
+ * One camera's key events in a batch answer.
19979
+ *
19980
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19981
+ * error rather than throwing, so a camera whose store read failed and one with
19982
+ * no events in the window were ALREADY indistinguishable per camera — the
19983
+ * batch does not make that worse, and the row keeps the deviceId the single
19984
+ * method's output never carried (the caller used to stamp it from the fan-out
19985
+ * key, which only worked because there was one query per camera).
19986
+ */
19987
+ var KeyEventsForDeviceSchema = object({
19988
+ deviceId: number(),
19989
+ events: array(KeyEventSchema).readonly()
19990
+ });
19864
19991
  object({
19865
19992
  trackId: string(),
19866
19993
  className: string(),
@@ -20130,7 +20257,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20130
20257
  until: number().optional(),
20131
20258
  kinds: array(string()).optional(),
20132
20259
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
20133
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
20260
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
20134
20261
  deviceId: number(),
20135
20262
  since: number(),
20136
20263
  until: number(),
@@ -28263,6 +28390,9 @@ method(object({
28263
28390
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28264
28391
  kind: "query",
28265
28392
  auth: "admin"
28393
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
28394
+ kind: "mutation",
28395
+ auth: "admin"
28266
28396
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28267
28397
  kind: "mutation",
28268
28398
  auth: "admin"
@@ -28654,7 +28784,26 @@ var SceneMonitorStatusSchema = object({
28654
28784
  monitors: array(SceneMonitorSchema),
28655
28785
  lastFetchedAt: number()
28656
28786
  });
28657
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
28787
+ /**
28788
+ * One camera's row in a `listScenesBatch` answer.
28789
+ *
28790
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
28791
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
28792
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
28793
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
28794
+ * configured. Fanned out per camera the difference was visible — one query
28795
+ * errored while the others resolved — and a batch that returned only the rows
28796
+ * it managed would have destroyed it, silently, by making an unreachable camera
28797
+ * indistinguishable from one that answered `monitors: []`.
28798
+ *
28799
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
28800
+ * could not be read"; `status.monitors: []` means "read, and it has none".
28801
+ */
28802
+ var SceneMonitorStatusForDeviceSchema = object({
28803
+ deviceId: number(),
28804
+ status: SceneMonitorStatusSchema.nullable()
28805
+ });
28806
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
28658
28807
  deviceId: number(),
28659
28808
  label: string(),
28660
28809
  roi: MaskRectShapeSchema,
@@ -29978,6 +30127,27 @@ var CameraOccupancySnapshotSchema = object({
29978
30127
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
29979
30128
  });
29980
30129
  /**
30130
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
30131
+ *
30132
+ * THREE outcomes, and the single-camera method could only express two of them
30133
+ * because `snapshot: null` was already spoken for:
30134
+ *
30135
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
30136
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
30137
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
30138
+ * - `read: 'unreadable'` — the owner could not answer for this
30139
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
30140
+ *
30141
+ * Collapsing the last two is the failure this field exists to prevent: a
30142
+ * hydration that threw would otherwise render as an empty Stationary section,
30143
+ * which is a definite claim about a camera nobody could read.
30144
+ */
30145
+ var CameraOccupancySnapshotForDeviceSchema = object({
30146
+ deviceId: number(),
30147
+ read: _enum(["read", "unreadable"]),
30148
+ snapshot: CameraOccupancySnapshotSchema.nullable()
30149
+ });
30150
+ /**
29981
30151
  * Time-series resolution. The history methods return one bucket per
29982
30152
  * step over the requested range. Smaller resolutions cost more
29983
30153
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -30001,7 +30171,7 @@ var HistoryPointSchema = object({
30001
30171
  /** Object count averaged over the bucket (rounded to nearest integer). */
30002
30172
  count: number().int().nonnegative()
30003
30173
  });
30004
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
30174
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
30005
30175
  deviceId: number(),
30006
30176
  zoneId: string(),
30007
30177
  className: string().optional()
@@ -33390,6 +33560,12 @@ Object.freeze({
33390
33560
  addonId: null,
33391
33561
  access: "view"
33392
33562
  },
33563
+ "pipelineAnalytics.getKeyEventsBatch": {
33564
+ capName: "pipeline-analytics",
33565
+ capScope: "device",
33566
+ addonId: null,
33567
+ access: "view"
33568
+ },
33393
33569
  "pipelineAnalytics.getMotionEvents": {
33394
33570
  capName: "pipeline-analytics",
33395
33571
  capScope: "device",
@@ -34566,6 +34742,12 @@ Object.freeze({
34566
34742
  addonId: null,
34567
34743
  access: "view"
34568
34744
  },
34745
+ "recording.reconcileLedgerAgainstDisk": {
34746
+ capName: "recording",
34747
+ capScope: "system",
34748
+ addonId: null,
34749
+ access: "create"
34750
+ },
34569
34751
  "recording.refreshStorageLocationsForMigration": {
34570
34752
  capName: "recording",
34571
34753
  capScope: "system",
@@ -34692,6 +34874,12 @@ Object.freeze({
34692
34874
  addonId: null,
34693
34875
  access: "view"
34694
34876
  },
34877
+ "sceneMonitor.listScenesBatch": {
34878
+ capName: "scene-monitor",
34879
+ capScope: "device",
34880
+ addonId: null,
34881
+ access: "view"
34882
+ },
34695
34883
  "sceneMonitor.recheckNow": {
34696
34884
  capName: "scene-monitor",
34697
34885
  capScope: "device",
@@ -36048,6 +36236,12 @@ Object.freeze({
36048
36236
  addonId: null,
36049
36237
  access: "view"
36050
36238
  },
36239
+ "zoneAnalytics.getCurrentSnapshotBatch": {
36240
+ capName: "zone-analytics",
36241
+ capScope: "device",
36242
+ addonId: null,
36243
+ access: "view"
36244
+ },
36051
36245
  "zoneAnalytics.getUnzonedHistory": {
36052
36246
  capName: "zone-analytics",
36053
36247
  capScope: "device",
@@ -37052,6 +37246,11 @@ Object.freeze({
37052
37246
  form: "single",
37053
37247
  optional: false
37054
37248
  }],
37249
+ "pipelineAnalytics.getKeyEventsBatch": [{
37250
+ name: "deviceIds",
37251
+ form: "array",
37252
+ optional: false
37253
+ }],
37055
37254
  "pipelineAnalytics.getMotionEvents": [{
37056
37255
  name: "deviceId",
37057
37256
  form: "single",
@@ -37492,6 +37691,11 @@ Object.freeze({
37492
37691
  form: "single",
37493
37692
  optional: false
37494
37693
  }],
37694
+ "recording.reconcileLedgerAgainstDisk": [{
37695
+ name: "deviceId",
37696
+ form: "single",
37697
+ optional: true
37698
+ }],
37495
37699
  "recording.relocateFootage": [{
37496
37700
  name: "deviceId",
37497
37701
  form: "single",
@@ -37557,6 +37761,11 @@ Object.freeze({
37557
37761
  form: "single",
37558
37762
  optional: false
37559
37763
  }],
37764
+ "sceneMonitor.listScenesBatch": [{
37765
+ name: "deviceIds",
37766
+ form: "array",
37767
+ optional: false
37768
+ }],
37560
37769
  "sceneMonitor.recheckNow": [{
37561
37770
  name: "deviceId",
37562
37771
  form: "single",
@@ -37818,6 +38027,11 @@ Object.freeze({
37818
38027
  form: "single",
37819
38028
  optional: false
37820
38029
  }],
38030
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
38031
+ name: "deviceIds",
38032
+ form: "array",
38033
+ optional: false
38034
+ }],
37821
38035
  "zoneAnalytics.getUnzonedHistory": [{
37822
38036
  name: "deviceId",
37823
38037
  form: "single",
@@ -40927,6 +41141,124 @@ function composeCameraStatus(input) {
40927
41141
  };
40928
41142
  }
40929
41143
  //#endregion
41144
+ //#region src/camera-status/fleet-read-pass.ts
41145
+ /**
41146
+ * Keep a rejection from becoming an UNHANDLED one.
41147
+ *
41148
+ * A shared promise is created by the first camera to ask and consumed by the
41149
+ * others later — `listBindableCapsForDeviceType` is reached in stage 2 of
41150
+ * `readSwitchAuthorities`, after an await, so two cameras genuinely attach
41151
+ * their handlers in different ticks. Without this the first rejection would be
41152
+ * unhandled for the window in between.
41153
+ *
41154
+ * The handler is a no-op ON PURPOSE: it never becomes the answer. Every real
41155
+ * consumer attaches its own `.catch` and reports the failure per camera.
41156
+ */
41157
+ function keepAlive(p) {
41158
+ p.catch(() => void 0);
41159
+ return p;
41160
+ }
41161
+ var FleetReadPass = class {
41162
+ deps;
41163
+ roster = null;
41164
+ bindings = null;
41165
+ mutes = null;
41166
+ bindableByType = /* @__PURE__ */ new Map();
41167
+ slotsByNode = /* @__PURE__ */ new Map();
41168
+ rtspByNode = /* @__PURE__ */ new Map();
41169
+ provisioningByNode = /* @__PURE__ */ new Map();
41170
+ selectedEngineByNode = /* @__PURE__ */ new Map();
41171
+ constructor(deps) {
41172
+ this.deps = deps;
41173
+ }
41174
+ /** The cameras this pass covers — the set both batch reads are issued for. */
41175
+ get deviceIds() {
41176
+ return this.deps.deviceIds;
41177
+ }
41178
+ /**
41179
+ * The device rows for the pass's set.
41180
+ *
41181
+ * **The shape follows the SET, not the caller.** A pass of one asks
41182
+ * `getDevice` — the exact question, and the one the write path
41183
+ * (`CameraSwitchService.setCameraSwitch`) has always asked. A pass of many
41184
+ * asks `listAll({deviceIds})`, which exists precisely because "these N" was
41185
+ * otherwise either N round trips or the whole fleet. Answering a set of one
41186
+ * with the fleet-shaped method would be no cheaper and would move the write
41187
+ * path onto a read it does not need.
41188
+ *
41189
+ * `projection: 'slim'` because the only fields read off it are `type` and
41190
+ * `disabled`: the full projection reads each device's settings row, which is
41191
+ * the per-device round trip this whole change exists to remove.
41192
+ */
41193
+ deviceRoster(api) {
41194
+ const single = this.deps.deviceIds.length === 1 ? this.deps.deviceIds[0] : void 0;
41195
+ const source = single === void 0 ? "deviceManager.listAll" : "deviceManager.getDevice";
41196
+ if (this.roster === null) this.roster = keepAlive(single === void 0 ? api.deviceManager.listAll.query({
41197
+ deviceIds: [...this.deps.deviceIds],
41198
+ projection: "slim"
41199
+ }) : api.deviceManager.getDevice.query({ deviceId: single }).then((row) => row === null ? [] : [row]));
41200
+ return {
41201
+ source,
41202
+ result: this.roster
41203
+ };
41204
+ }
41205
+ /** The binding rows for the pass's set — `getBindings` for one, the batch for many. */
41206
+ bindingRows(api) {
41207
+ const single = this.deps.deviceIds.length === 1 ? this.deps.deviceIds[0] : void 0;
41208
+ const source = single === void 0 ? "deviceManager.getBindingsBatch" : "deviceManager.getBindings";
41209
+ if (this.bindings === null) this.bindings = keepAlive(single === void 0 ? api.deviceManager.getBindingsBatch.query({ deviceIds: [...this.deps.deviceIds] }) : api.deviceManager.getBindings.query({ deviceId: single }).then((row) => [row]));
41210
+ return {
41211
+ source,
41212
+ result: this.bindings
41213
+ };
41214
+ }
41215
+ /** The fleet's muted-camera list. One answer for every camera in the pass. */
41216
+ mutedDevices(api) {
41217
+ if (this.mutes === null) this.mutes = keepAlive(api.notificationRules.listDeviceMutes.query({}));
41218
+ return this.mutes;
41219
+ }
41220
+ /** The bindable caps for one device TYPE. Every camera shares one answer. */
41221
+ bindableCapsFor(api, deviceType) {
41222
+ const existing = this.bindableByType.get(deviceType);
41223
+ if (existing !== void 0) return existing;
41224
+ const created = keepAlive(api.deviceManager.listBindableCapsForDeviceType.query({ deviceType }));
41225
+ this.bindableByType.set(deviceType, created);
41226
+ return created;
41227
+ }
41228
+ /** One source node's whole profile-slot table. */
41229
+ profileSlots(api, sourceNodeId) {
41230
+ const existing = this.slotsByNode.get(sourceNodeId);
41231
+ if (existing !== void 0) return existing;
41232
+ const created = keepAlive(api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)));
41233
+ this.slotsByNode.set(sourceNodeId, created);
41234
+ return created;
41235
+ }
41236
+ /** One source node's whole RTSP-restream table. */
41237
+ rtspEntries(api, sourceNodeId) {
41238
+ const existing = this.rtspByNode.get(sourceNodeId);
41239
+ if (existing !== void 0) return existing;
41240
+ const created = keepAlive(api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)));
41241
+ this.rtspByNode.set(sourceNodeId, created);
41242
+ return created;
41243
+ }
41244
+ /** One detection node's runtime-provisioning snapshot. */
41245
+ engineProvisioning(api, detectionNodeId) {
41246
+ const existing = this.provisioningByNode.get(detectionNodeId);
41247
+ if (existing !== void 0) return existing;
41248
+ const created = keepAlive(api.pipelineExecutor.getEngineProvisioning.query({ nodeId: detectionNodeId }));
41249
+ this.provisioningByNode.set(detectionNodeId, created);
41250
+ return created;
41251
+ }
41252
+ /** The executor's bootstrap engine, as the detection stage reads it. */
41253
+ selectedEngine(api, detectionNodeId) {
41254
+ const existing = this.selectedEngineByNode.get(detectionNodeId);
41255
+ if (existing !== void 0) return existing;
41256
+ const created = keepAlive(api.pipelineExecutor.getSelectedEngine.query({ nodeId: detectionNodeId }));
41257
+ this.selectedEngineByNode.set(detectionNodeId, created);
41258
+ return created;
41259
+ }
41260
+ };
41261
+ //#endregion
40930
41262
  //#region src/camera-status-service.ts
40931
41263
  /** WebRTC consumer kinds counted toward `BrokerResult.webrtcSessions`. */
40932
41264
  var WEBRTC_KINDS = new Set([
@@ -41115,7 +41447,7 @@ var CameraStatusService = class {
41115
41447
  * broker's actual decode-session node into `liveDecoder` (T6) — the first
41116
41448
  * slot that reports one wins.
41117
41449
  */
41118
- buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, sink) {
41450
+ buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, sink) {
41119
41451
  if (!api || !allSlotsFetch) return Promise.resolve(null);
41120
41452
  return this.boundedStage(allSlotsFetch.then(async (slots) => {
41121
41453
  const deviceSlots = slots.filter((s) => s.deviceId === deviceId);
@@ -41125,12 +41457,13 @@ var CameraStatusService = class {
41125
41457
  rtspRestream: false
41126
41458
  };
41127
41459
  const [statsAndClients, rtspEntry] = await Promise.all([Promise.all(deviceSlots.map(async (slot) => {
41460
+ const [stats, clients] = await Promise.all([api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null), api.streamBroker.listClients.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null)]);
41128
41461
  return {
41129
41462
  slot,
41130
- stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null),
41131
- clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null)
41463
+ stats,
41464
+ clients
41132
41465
  };
41133
- })), api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)).catch(() => null)]);
41466
+ })), pass.rtspEntries(api, sourceNodeId).catch(() => null)]);
41134
41467
  const profileDetails = statsAndClients.map(({ slot, stats, clients }) => ({
41135
41468
  profile: slot.profile,
41136
41469
  status: slot.status,
@@ -41191,13 +41524,16 @@ var CameraStatusService = class {
41191
41524
  };
41192
41525
  }
41193
41526
  /** Detection stage (pipeline-executor + runner metrics). */
41194
- buildDetectionStage(api, detectionNodeId, deviceId, sink) {
41527
+ buildDetectionStage(api, detectionNodeId, deviceId, pass, sink) {
41195
41528
  if (!api || !detectionNodeId) return Promise.resolve(null);
41196
- return this.boundedStage(Promise.all([api.pipelineExecutor.getEngineProvisioning.query({ nodeId: detectionNodeId }).catch(() => null), api.pipelineExecutor.getSelectedEngine.query({ nodeId: detectionNodeId }).catch(() => null)]).then(async ([provisioning, engine]) => {
41197
- const metrics = await api.pipelineRunner.getCameraMetrics.query({
41529
+ return this.boundedStage(Promise.all([
41530
+ pass.engineProvisioning(api, detectionNodeId).catch(() => null),
41531
+ pass.selectedEngine(api, detectionNodeId).catch(() => null),
41532
+ api.pipelineRunner.getCameraMetrics.query({
41198
41533
  deviceId,
41199
41534
  nodeId: detectionNodeId
41200
- }).catch(() => null);
41535
+ }).catch(() => null)
41536
+ ]).then(async ([provisioning, engine, metrics]) => {
41201
41537
  const phase = (() => {
41202
41538
  const p = metrics?.phase;
41203
41539
  if (p === "active") return "active";
@@ -41309,9 +41645,9 @@ var CameraStatusService = class {
41309
41645
  * badge on a working camera); what matters is that the emptiness travels with
41310
41646
  * the reason it is empty.
41311
41647
  */
41312
- buildSwitchStage(deviceId, sink) {
41648
+ buildSwitchStage(deviceId, pass, sink) {
41313
41649
  const startedAt = Date.now();
41314
- return this.boundedStage(this.deps.switchAuthoritiesFor(deviceId), STAGE_TIMEOUT_MS, "switches", deviceId, sink).then((reads) => {
41650
+ return this.boundedStage(this.deps.switchAuthoritiesFor(deviceId, pass), STAGE_TIMEOUT_MS, "switches", deviceId, sink).then((reads) => {
41315
41651
  if (reads === null) return [];
41316
41652
  const { switchedOff, unreadable } = composeSwitchedOff(reads);
41317
41653
  if (unreadable.length > 0) this.recordDegraded(sink, deviceId, "switches", "partial", Date.now() - startedAt, { unreadableAuthorities: unreadable });
@@ -41329,18 +41665,28 @@ var CameraStatusService = class {
41329
41665
  * `null` of a camera that legitimately has no such stage.
41330
41666
  */
41331
41667
  async getCameraStatus(deviceId) {
41668
+ return this.composeOne(deviceId, new FleetReadPass({ deviceIds: [deviceId] }));
41669
+ }
41670
+ /**
41671
+ * One camera's status, composed WITHIN a pass.
41672
+ *
41673
+ * Everything the camera alone can answer is fetched here; everything the pass
41674
+ * already knows (a node's slot table, the fleet's mute list, a device type's
41675
+ * bindable caps) is asked of `pass`, which issues it once for the whole call.
41676
+ */
41677
+ async composeOne(deviceId, pass) {
41332
41678
  const api = this.deps.api();
41333
41679
  const degradations = { entries: [] };
41334
41680
  const { detectionNodeId, sourceNodeId, pinned, detectionReason, audioNodeId, audioPinned } = this.buildAssignmentContext(deviceId);
41335
41681
  const liveDecoder = { nodeId: null };
41336
- const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)) : null;
41682
+ const allSlotsFetch = api ? pass.profileSlots(api, sourceNodeId) : null;
41337
41683
  const sourceFetch = this.buildSourceStage(api, allSlotsFetch, deviceId, degradations);
41338
- const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, degradations);
41684
+ const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, degradations);
41339
41685
  const decoderFetch = this.buildDecoderStage(detectionNodeId);
41340
41686
  const motionResult = this.buildMotionStage(deviceId);
41341
- const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, degradations);
41687
+ const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, pass, degradations);
41342
41688
  const recordingFetch = this.buildRecordingStage(api, deviceId, degradations);
41343
- const switchesFetch = this.buildSwitchStage(deviceId, degradations);
41689
+ const switchesFetch = this.buildSwitchStage(deviceId, pass, degradations);
41344
41690
  const [sourceResult, brokerResult, decoderResult, detectionResult, recordingResult, switchedOff] = await Promise.all([
41345
41691
  sourceFetch,
41346
41692
  brokerFetch,
@@ -41383,15 +41729,30 @@ var CameraStatusService = class {
41383
41729
  * `deviceIds` defaults to all cameras currently tracked by the
41384
41730
  * orchestrator's assignment map when omitted.
41385
41731
  *
41386
- * v1: `Promise.all` over per-device composition (no concurrency cap).
41387
- * Note: for large fleets (hundreds of cameras) this may fan out many
41388
- * parallel calls. A concurrency limiter (p-limit / semaphore) should be
41389
- * added if latency measurements show it's necessary deliberately
41390
- * deferred per the YAGNI constraint in the spec.
41732
+ * The whole call runs inside ONE {@link FleetReadPass}, and that is what
41733
+ * makes a five-second poll affordable. Composing a camera reaches seven
41734
+ * stages, but six of the questions those stages ask are about a NODE, a
41735
+ * device TYPE or the whole FLEETthe slot table, the restream table, the
41736
+ * mute list, the engine pair, the bindable caps — and asking them per camera
41737
+ * meant N identical round trips for N identical answers. Two more (the device
41738
+ * row, the binding table) are per-camera questions the device manager already
41739
+ * answers for a set. The pass asks each exactly once and shares the promise;
41740
+ * it holds nothing between calls, so the next poll asks again from scratch.
41741
+ *
41742
+ * What is left is genuinely per camera and stays per camera: the broker slot
41743
+ * stats, the runner metrics, the recorder, the broker mute and the camera's
41744
+ * own privacy provider. Collapsing any of those would be a narrower answer,
41745
+ * not a cheaper one.
41746
+ *
41747
+ * `Promise.all` over per-device composition (no concurrency cap). The fan-out
41748
+ * is now ~8 round trips per camera rather than ~16, and the six fleet reads
41749
+ * no longer multiply — which is the part that made the count grow with the
41750
+ * fleet twice over.
41391
41751
  */
41392
41752
  async getCameraStatuses(deviceIds) {
41393
41753
  const ids = deviceIds !== void 0 && deviceIds.length > 0 ? deviceIds : this.deps.listAssignedDeviceIds();
41394
- return Promise.all(ids.map((deviceId) => this.getCameraStatus(deviceId)));
41754
+ const pass = new FleetReadPass({ deviceIds: ids });
41755
+ return Promise.all(ids.map((deviceId) => this.composeOne(deviceId, pass)));
41395
41756
  }
41396
41757
  };
41397
41758
  //#endregion
@@ -41640,10 +42001,15 @@ async function probeCamera(api, deviceId, deps) {
41640
42001
  * @param deviceId The camera.
41641
42002
  * @param deps Logger + source-owner resolver.
41642
42003
  */
41643
- async function readSwitchAuthorities(api, deviceId, deps) {
42004
+ async function readSwitchAuthorities(api, deviceId, deps, sharedPass) {
41644
42005
  if (!api) return unknownReads(deviceId);
41645
- const devicePromise = bounded(deps, deviceId, "deviceManager.getDevice", api.deviceManager.getDevice.query({ deviceId }).then((d) => isDeviceShape(d) ? d : null).catch((err) => {
41646
- warnUnreachable(deps, deviceId, "getDevice", err);
42006
+ const pass = sharedPass ?? new FleetReadPass({ deviceIds: [deviceId] });
42007
+ const roster = pass.deviceRoster(api);
42008
+ const devicePromise = bounded(deps, deviceId, roster.source, roster.result.then((rows) => {
42009
+ const row = rows.find((d) => d.id === deviceId);
42010
+ return isDeviceShape(row) ? row : null;
42011
+ }).catch((err) => {
42012
+ warnUnreachable(deps, deviceId, roster.source, err);
41647
42013
  return null;
41648
42014
  }), null);
41649
42015
  const unknownBindings = {
@@ -41651,11 +42017,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41651
42017
  providerAddonIdByCap: /* @__PURE__ */ new Map(),
41652
42018
  allCapNames: null
41653
42019
  };
41654
- const bindingsPromise = bounded(deps, deviceId, "deviceManager.getBindings", api.deviceManager.getBindings.query({ deviceId }).then((b) => {
42020
+ const bindingRead = pass.bindingRows(api);
42021
+ const bindingsPromise = bounded(deps, deviceId, bindingRead.source, bindingRead.result.then((rows) => {
42022
+ const row = rows.find((r) => r.deviceId === deviceId);
42023
+ if (row === void 0) return unknownBindings;
41655
42024
  const active = [];
41656
42025
  const all = [];
41657
42026
  const providers = /* @__PURE__ */ new Map();
41658
- for (const e of b.entries) {
42027
+ for (const e of row.entries) {
41659
42028
  all.push(e.capName);
41660
42029
  if (e.kind !== "wrapped") continue;
41661
42030
  active.push(e.capName);
@@ -41667,14 +42036,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41667
42036
  allCapNames: all
41668
42037
  };
41669
42038
  }).catch((err) => {
41670
- warnUnreachable(deps, deviceId, "getBindings", err);
42039
+ warnUnreachable(deps, deviceId, bindingRead.source, err);
41671
42040
  return unknownBindings;
41672
42041
  }), unknownBindings);
41673
42042
  const recordingPromise = bounded(deps, deviceId, "recording.getDeviceConfig", api.recording.getDeviceConfig.query({ deviceId }).then((c) => isRecordingConfig(c) ? c : null).catch((err) => {
41674
42043
  warnUnreachable(deps, deviceId, "recording.getDeviceConfig", err);
41675
42044
  return null;
41676
42045
  }), null);
41677
- const mutesPromise = bounded(deps, deviceId, "notificationRules.listDeviceMutes", api.notificationRules.listDeviceMutes.query({}).then((r) => r.mutedDeviceIds).catch((err) => {
42046
+ const mutesPromise = bounded(deps, deviceId, "notificationRules.listDeviceMutes", pass.mutedDevices(api).then((r) => r.mutedDeviceIds).catch((err) => {
41678
42047
  warnUnreachable(deps, deviceId, "notificationRules.listDeviceMutes", err);
41679
42048
  return null;
41680
42049
  }), null);
@@ -41689,7 +42058,7 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41689
42058
  mutesPromise,
41690
42059
  brokerAudioPromise
41691
42060
  ]);
41692
- const [bindable, privacy] = await Promise.all([device === null ? Promise.resolve(null) : bounded(deps, deviceId, "deviceManager.listBindableCapsForDeviceType", api.deviceManager.listBindableCapsForDeviceType.query({ deviceType: device.type }).catch((err) => {
42061
+ const [bindable, privacy] = await Promise.all([device === null ? Promise.resolve(null) : bounded(deps, deviceId, "deviceManager.listBindableCapsForDeviceType", pass.bindableCapsFor(api, device.type).catch((err) => {
41693
42062
  warnUnreachable(deps, deviceId, "listBindableCapsForDeviceType", err);
41694
42063
  return null;
41695
42064
  }), null), readPrivacyPlanes(api, deviceId, deps, bindings.allCapNames)]);
@@ -50396,11 +50765,11 @@ async function buildOrchestratorControllers(deps) {
50396
50765
  assignSource: (deviceId) => topology.assignSource(deviceId),
50397
50766
  listAssignedDeviceIds: () => [...new Set([...ledger.listAssignedDeviceIds(), ...detectionWiring.activeDeviceIds()])],
50398
50767
  isSessionCamera: (deviceId) => deps.isSessionCamera(deviceId),
50399
- switchAuthoritiesFor: async (deviceId) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50768
+ switchAuthoritiesFor: async (deviceId, pass) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50400
50769
  logger: deps.ctx().logger,
50401
50770
  assignSource: (id) => topology.assignSource(id),
50402
50771
  warnSampler: switchWarnSampler
50403
- })).derivation
50772
+ }, pass)).derivation
50404
50773
  });
50405
50774
  const reconcile = new ReconcileController({
50406
50775
  api: () => deps.ctx().api ?? null,