@camstack/addon-pipeline-orchestrator 1.1.15 → 1.1.16

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.
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.1.17",
21
+ version: "1.1.18",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_orchestrator_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.1.17",
39
+ version: "1.1.18",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
package/dist/index.js CHANGED
@@ -23037,6 +23037,25 @@ function computeDispatchGap(desired, known) {
23037
23037
  return [...desired].filter((id) => !known.has(id));
23038
23038
  }
23039
23039
  //#endregion
23040
+ //#region src/frame-source-eligibility.ts
23041
+ /**
23042
+ * The set of nodes that can obtain a given camera's decoded frames.
23043
+ *
23044
+ * - No `sourceOwnerNodeId` → `enabledDecoderNodes` unchanged (today's
23045
+ * semantics; `remoteSourcingNodes` cannot restrict anything when no owner
23046
+ * restricts first — it only ever WIDENS an owner-restricted set).
23047
+ * - With an owner → `enabledDecoderNodes ∩ ({owner} ∪ remoteSourcingNodes)`.
23048
+ *
23049
+ * Pure: never mutates its inputs; result preserves `enabledDecoderNodes`
23050
+ * order.
23051
+ */
23052
+ function computeFrameSourceNodes(input) {
23053
+ const { enabledDecoderNodes, sourceOwnerNodeId, remoteSourcingNodes } = input;
23054
+ if (sourceOwnerNodeId === void 0) return enabledDecoderNodes;
23055
+ const remote = remoteSourcingNodes ?? [];
23056
+ return enabledDecoderNodes.filter((nodeId) => nodeId === sourceOwnerNodeId || remote.includes(nodeId));
23057
+ }
23058
+ //#endregion
23040
23059
  //#region src/load-balancer.ts
23041
23060
  /**
23042
23061
  * Compute the L2 capacity score for a runner node. Lower is better.
@@ -24526,7 +24545,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24526
24545
  nodes: await this.collectAgentLoad({ onlyEnabled: true }),
24527
24546
  preferredAgent,
24528
24547
  nodeCaps: await this.buildNodeCaps(),
24529
- eligibleNodes: this.detectionEligibleNodes()
24548
+ eligibleNodes: this.detectionEligibleNodes(runnerConfig.deviceId)
24530
24549
  });
24531
24550
  if (!decision) throw new Error(`dispatchCamera: no runner available for ${runnerConfig.deviceId}`);
24532
24551
  if (decision.kind === "pending") {
@@ -24591,7 +24610,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24591
24610
  }
24592
24611
  async assignPipeline(input) {
24593
24612
  if (!this.ctx) throw new Error("PipelineOrchestrator: assignPipeline called before initialize");
24594
- const eligible = this.detectionEligibleNodes();
24613
+ const eligible = this.detectionEligibleNodes(input.deviceId);
24595
24614
  if (!eligible.includes(input.agentNodeId)) throw new Error(`Cannot pin camera ${input.deviceId} detection to '${input.agentNodeId}': the node cannot obtain this camera's decoded frames (frame-source nodes: ${eligible.join(", ") || "none"}). Add it to Enabled Decoder Nodes first.`);
24596
24615
  await this.ctx.settings?.writeDeviceStore(input.deviceId, { [PREFERRED_AGENT_SETTING]: input.agentNodeId }).catch((err) => {
24597
24616
  const msg = errMsg(err);
@@ -24639,7 +24658,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24639
24658
  nodes: await this.collectAgentLoad({ onlyEnabled: true }),
24640
24659
  preferredAgent: null,
24641
24660
  nodeCaps: await this.buildNodeCaps(),
24642
- eligibleNodes: this.detectionEligibleNodes()
24661
+ eligibleNodes: this.detectionEligibleNodes(input.deviceId)
24643
24662
  });
24644
24663
  if (!decision) this.ctx.logger.warn("unassignPipeline: no runner available, leaving unassigned", { tags: { deviceId: input.deviceId } });
24645
24664
  else if (decision.kind === "pending") {
@@ -24695,7 +24714,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24695
24714
  nodes: loads,
24696
24715
  preferredAgent: await this.readPreferredAgent(deviceId),
24697
24716
  nodeCaps,
24698
- eligibleNodes: this.detectionEligibleNodes()
24717
+ eligibleNodes: this.detectionEligibleNodes(deviceId)
24699
24718
  });
24700
24719
  if (!decision) continue;
24701
24720
  if (decision.kind === "pending") {
@@ -24910,34 +24929,54 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24910
24929
  return this.enabledNodes.includes(nodeId);
24911
24930
  }
24912
24931
  /**
24913
- * The set of nodes that can OBTAIN a camera's decoded frames.
24932
+ * The node that owns `deviceId`'s source pull (dials the real RTSP, hosts
24933
+ * the broker). **P2a (today): intentionally `undefined`** — per-camera
24934
+ * source ownership is not modeled yet, and an absent owner makes
24935
+ * `computeFrameSourceNodes` collapse to the global `enabledDecoderNodes`
24936
+ * (bit-identical to pre-P2a behavior, the safe-rollout invariant).
24914
24937
  *
24915
- * **Phase 1 (today):** detection HARD-requires that the runner is itself a
24916
- * decoder node a node can only run detection where it can read the shm
24917
- * frame ring locally. So the frame-source set is exactly
24918
- * `enabledDecoderNodes`.
24938
+ * Deliberately NOT `'hub'` yet: with an owner set and no
24939
+ * `remoteSourcingNodes`, the predicate would restrict eligibility to the
24940
+ * hub even when agents are decoder-enabled a behavior change reserved for
24941
+ * P2d, which backs this seam with the `assignSource`/`getSourceAssignment`
24942
+ * cap surface (restream-owner design §2.3).
24943
+ */
24944
+ sourceOwner(_deviceId) {}
24945
+ /**
24946
+ * The set of nodes that can OBTAIN a camera's decoded frames — PER-CAMERA
24947
+ * since P2a (restream-owner design §2.3), delegating to the pure
24948
+ * `computeFrameSourceNodes` predicate: a node qualifies iff it can decode
24949
+ * locally (`enabledDecoderNodes`) AND (it owns the camera's source pull OR
24950
+ * it may dial the owner's restream — `remoteSourcingNodes`, a P2c/P2d
24951
+ * rollout knob not wired yet).
24919
24952
  *
24920
- * **Phase 2 (T10):** this becomes per-camera source/decoder ownership — a
24921
- * detection node will be able to source a camera's frames from a co-located
24922
- * OR a remote restream/decoder leg. Replace this body then; every predicate
24923
- * change edits this ONE method (and `detectionEligibleNodes`).
24924
- */
24925
- frameSourceNodes() {
24926
- return this.enabledDecoderNodes;
24953
+ * **Today's effective value:** `sourceOwner()` returns `undefined` for
24954
+ * every camera, so this is exactly `enabledDecoderNodes` identical to the
24955
+ * pre-P2a global predicate. Every predicate change edits the pure module
24956
+ * (and this ONE method); `deviceId` is threaded from all placement sites so
24957
+ * later phases turn per-camera without re-touching call sites.
24958
+ */
24959
+ frameSourceNodes(deviceId) {
24960
+ return computeFrameSourceNodes({
24961
+ enabledDecoderNodes: this.enabledDecoderNodes,
24962
+ sourceOwnerNodeId: this.sourceOwner(deviceId)
24963
+ });
24927
24964
  }
24928
24965
  /**
24929
24966
  * Nodes eligible to run a camera's detection pipeline: the whitelist of
24930
24967
  * detection-enabled nodes (`enabledNodes`) intersected with the nodes that
24931
- * can obtain the camera's frames (`frameSourceNodes()`). A node that is
24932
- * detection-enabled but cannot source frames (or vice-versa) is NEVER a
24933
- * valid placement — the balancer receives this as `eligibleNodes` and treats
24934
- * anything outside it as unassignable (`no-frame-source`).
24968
+ * can obtain the camera's frames (`frameSourceNodes(deviceId)`). A node
24969
+ * that is detection-enabled but cannot source frames (or vice-versa) is
24970
+ * NEVER a valid placement — the balancer receives this as `eligibleNodes`
24971
+ * and treats anything outside it as unassignable (`no-frame-source`).
24935
24972
  *
24936
24973
  * Single choke point for T2/T10: passed to every `balance()` call site and
24937
- * enforced by `assignPipeline` before persisting a pin.
24974
+ * enforced by `assignPipeline` before persisting a pin. `deviceId` is
24975
+ * optional only for node-scoped checks (reconnect-restore guard); camera
24976
+ * placement sites always pass it.
24938
24977
  */
24939
- detectionEligibleNodes() {
24940
- const frameSource = this.frameSourceNodes();
24978
+ detectionEligibleNodes(deviceId) {
24979
+ const frameSource = this.frameSourceNodes(deviceId);
24941
24980
  return this.enabledNodes.filter((nodeId) => frameSource.includes(nodeId));
24942
24981
  }
24943
24982
  /**
@@ -25192,7 +25231,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25192
25231
  nodes: loads,
25193
25232
  preferredAgent: null,
25194
25233
  nodeCaps: await this.buildNodeCaps(),
25195
- eligibleNodes: this.detectionEligibleNodes()
25234
+ eligibleNodes: this.detectionEligibleNodes(deviceId)
25196
25235
  });
25197
25236
  if (!decision) {
25198
25237
  this.ctx.logger.error("Failover: no online runner", { tags: { deviceId } });
package/dist/index.mjs CHANGED
@@ -23033,6 +23033,25 @@ function computeDispatchGap(desired, known) {
23033
23033
  return [...desired].filter((id) => !known.has(id));
23034
23034
  }
23035
23035
  //#endregion
23036
+ //#region src/frame-source-eligibility.ts
23037
+ /**
23038
+ * The set of nodes that can obtain a given camera's decoded frames.
23039
+ *
23040
+ * - No `sourceOwnerNodeId` → `enabledDecoderNodes` unchanged (today's
23041
+ * semantics; `remoteSourcingNodes` cannot restrict anything when no owner
23042
+ * restricts first — it only ever WIDENS an owner-restricted set).
23043
+ * - With an owner → `enabledDecoderNodes ∩ ({owner} ∪ remoteSourcingNodes)`.
23044
+ *
23045
+ * Pure: never mutates its inputs; result preserves `enabledDecoderNodes`
23046
+ * order.
23047
+ */
23048
+ function computeFrameSourceNodes(input) {
23049
+ const { enabledDecoderNodes, sourceOwnerNodeId, remoteSourcingNodes } = input;
23050
+ if (sourceOwnerNodeId === void 0) return enabledDecoderNodes;
23051
+ const remote = remoteSourcingNodes ?? [];
23052
+ return enabledDecoderNodes.filter((nodeId) => nodeId === sourceOwnerNodeId || remote.includes(nodeId));
23053
+ }
23054
+ //#endregion
23036
23055
  //#region src/load-balancer.ts
23037
23056
  /**
23038
23057
  * Compute the L2 capacity score for a runner node. Lower is better.
@@ -24522,7 +24541,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24522
24541
  nodes: await this.collectAgentLoad({ onlyEnabled: true }),
24523
24542
  preferredAgent,
24524
24543
  nodeCaps: await this.buildNodeCaps(),
24525
- eligibleNodes: this.detectionEligibleNodes()
24544
+ eligibleNodes: this.detectionEligibleNodes(runnerConfig.deviceId)
24526
24545
  });
24527
24546
  if (!decision) throw new Error(`dispatchCamera: no runner available for ${runnerConfig.deviceId}`);
24528
24547
  if (decision.kind === "pending") {
@@ -24587,7 +24606,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24587
24606
  }
24588
24607
  async assignPipeline(input) {
24589
24608
  if (!this.ctx) throw new Error("PipelineOrchestrator: assignPipeline called before initialize");
24590
- const eligible = this.detectionEligibleNodes();
24609
+ const eligible = this.detectionEligibleNodes(input.deviceId);
24591
24610
  if (!eligible.includes(input.agentNodeId)) throw new Error(`Cannot pin camera ${input.deviceId} detection to '${input.agentNodeId}': the node cannot obtain this camera's decoded frames (frame-source nodes: ${eligible.join(", ") || "none"}). Add it to Enabled Decoder Nodes first.`);
24592
24611
  await this.ctx.settings?.writeDeviceStore(input.deviceId, { [PREFERRED_AGENT_SETTING]: input.agentNodeId }).catch((err) => {
24593
24612
  const msg = errMsg(err);
@@ -24635,7 +24654,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24635
24654
  nodes: await this.collectAgentLoad({ onlyEnabled: true }),
24636
24655
  preferredAgent: null,
24637
24656
  nodeCaps: await this.buildNodeCaps(),
24638
- eligibleNodes: this.detectionEligibleNodes()
24657
+ eligibleNodes: this.detectionEligibleNodes(input.deviceId)
24639
24658
  });
24640
24659
  if (!decision) this.ctx.logger.warn("unassignPipeline: no runner available, leaving unassigned", { tags: { deviceId: input.deviceId } });
24641
24660
  else if (decision.kind === "pending") {
@@ -24691,7 +24710,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24691
24710
  nodes: loads,
24692
24711
  preferredAgent: await this.readPreferredAgent(deviceId),
24693
24712
  nodeCaps,
24694
- eligibleNodes: this.detectionEligibleNodes()
24713
+ eligibleNodes: this.detectionEligibleNodes(deviceId)
24695
24714
  });
24696
24715
  if (!decision) continue;
24697
24716
  if (decision.kind === "pending") {
@@ -24906,34 +24925,54 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24906
24925
  return this.enabledNodes.includes(nodeId);
24907
24926
  }
24908
24927
  /**
24909
- * The set of nodes that can OBTAIN a camera's decoded frames.
24928
+ * The node that owns `deviceId`'s source pull (dials the real RTSP, hosts
24929
+ * the broker). **P2a (today): intentionally `undefined`** — per-camera
24930
+ * source ownership is not modeled yet, and an absent owner makes
24931
+ * `computeFrameSourceNodes` collapse to the global `enabledDecoderNodes`
24932
+ * (bit-identical to pre-P2a behavior, the safe-rollout invariant).
24910
24933
  *
24911
- * **Phase 1 (today):** detection HARD-requires that the runner is itself a
24912
- * decoder node a node can only run detection where it can read the shm
24913
- * frame ring locally. So the frame-source set is exactly
24914
- * `enabledDecoderNodes`.
24934
+ * Deliberately NOT `'hub'` yet: with an owner set and no
24935
+ * `remoteSourcingNodes`, the predicate would restrict eligibility to the
24936
+ * hub even when agents are decoder-enabled a behavior change reserved for
24937
+ * P2d, which backs this seam with the `assignSource`/`getSourceAssignment`
24938
+ * cap surface (restream-owner design §2.3).
24939
+ */
24940
+ sourceOwner(_deviceId) {}
24941
+ /**
24942
+ * The set of nodes that can OBTAIN a camera's decoded frames — PER-CAMERA
24943
+ * since P2a (restream-owner design §2.3), delegating to the pure
24944
+ * `computeFrameSourceNodes` predicate: a node qualifies iff it can decode
24945
+ * locally (`enabledDecoderNodes`) AND (it owns the camera's source pull OR
24946
+ * it may dial the owner's restream — `remoteSourcingNodes`, a P2c/P2d
24947
+ * rollout knob not wired yet).
24915
24948
  *
24916
- * **Phase 2 (T10):** this becomes per-camera source/decoder ownership — a
24917
- * detection node will be able to source a camera's frames from a co-located
24918
- * OR a remote restream/decoder leg. Replace this body then; every predicate
24919
- * change edits this ONE method (and `detectionEligibleNodes`).
24920
- */
24921
- frameSourceNodes() {
24922
- return this.enabledDecoderNodes;
24949
+ * **Today's effective value:** `sourceOwner()` returns `undefined` for
24950
+ * every camera, so this is exactly `enabledDecoderNodes` identical to the
24951
+ * pre-P2a global predicate. Every predicate change edits the pure module
24952
+ * (and this ONE method); `deviceId` is threaded from all placement sites so
24953
+ * later phases turn per-camera without re-touching call sites.
24954
+ */
24955
+ frameSourceNodes(deviceId) {
24956
+ return computeFrameSourceNodes({
24957
+ enabledDecoderNodes: this.enabledDecoderNodes,
24958
+ sourceOwnerNodeId: this.sourceOwner(deviceId)
24959
+ });
24923
24960
  }
24924
24961
  /**
24925
24962
  * Nodes eligible to run a camera's detection pipeline: the whitelist of
24926
24963
  * detection-enabled nodes (`enabledNodes`) intersected with the nodes that
24927
- * can obtain the camera's frames (`frameSourceNodes()`). A node that is
24928
- * detection-enabled but cannot source frames (or vice-versa) is NEVER a
24929
- * valid placement — the balancer receives this as `eligibleNodes` and treats
24930
- * anything outside it as unassignable (`no-frame-source`).
24964
+ * can obtain the camera's frames (`frameSourceNodes(deviceId)`). A node
24965
+ * that is detection-enabled but cannot source frames (or vice-versa) is
24966
+ * NEVER a valid placement — the balancer receives this as `eligibleNodes`
24967
+ * and treats anything outside it as unassignable (`no-frame-source`).
24931
24968
  *
24932
24969
  * Single choke point for T2/T10: passed to every `balance()` call site and
24933
- * enforced by `assignPipeline` before persisting a pin.
24970
+ * enforced by `assignPipeline` before persisting a pin. `deviceId` is
24971
+ * optional only for node-scoped checks (reconnect-restore guard); camera
24972
+ * placement sites always pass it.
24934
24973
  */
24935
- detectionEligibleNodes() {
24936
- const frameSource = this.frameSourceNodes();
24974
+ detectionEligibleNodes(deviceId) {
24975
+ const frameSource = this.frameSourceNodes(deviceId);
24937
24976
  return this.enabledNodes.filter((nodeId) => frameSource.includes(nodeId));
24938
24977
  }
24939
24978
  /**
@@ -25188,7 +25227,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25188
25227
  nodes: loads,
25189
25228
  preferredAgent: null,
25190
25229
  nodeCaps: await this.buildNodeCaps(),
25191
- eligibleNodes: this.detectionEligibleNodes()
25230
+ eligibleNodes: this.detectionEligibleNodes(deviceId)
25192
25231
  });
25193
25232
  if (!decision) {
25194
25233
  this.ctx.logger.error("Failover: no online runner", { tags: { deviceId } });
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-9JducZh4.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-C2Ftp01d.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline-orchestrator",
3
- "version": "1.1.15",
3
+ "version": "1.1.16",
4
4
  "description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
5
5
  "keywords": [
6
6
  "camstack",