@camstack/system 1.1.32 → 1.1.34
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/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/snapshot/index.js +130 -5
- package/dist/builtins/snapshot/index.mjs +131 -6
- package/dist/builtins/snapshot/snapshot.addon.d.ts +91 -1
- package/dist/index.js +34 -55
- package/dist/index.mjs +30 -51
- package/dist/kernel/cap-router-builder.d.ts +1 -1
- package/dist/kernel/transport/parent-unowned-call.d.ts +24 -0
- package/dist/{manifest-python-deps-jWKVwa7t.js → manifest-python-deps-CAnO1wPx.js} +27 -0
- package/dist/{manifest-python-deps-DXMKBZW1.mjs → manifest-python-deps-u-AQBQY6.mjs} +29 -2
- package/package.json +1 -1
package/dist/addon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
2
|
+
const require_manifest_python_deps = require("./manifest-python-deps-CAnO1wPx.js");
|
|
3
3
|
const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
|
|
4
4
|
let node_fs = require("node:fs");
|
|
5
5
|
node_fs = require_chunk.__toESM(node_fs);
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as createUdsLoggerWithControl, I as LocalChildClient, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps, yt as resolveAddonClass } from "./manifest-python-deps-
|
|
1
|
+
import { F as createUdsLoggerWithControl, I as LocalChildClient, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps, yt as resolveAddonClass } from "./manifest-python-deps-u-AQBQY6.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import * as fs from "node:fs";
|
|
@@ -28,8 +28,18 @@ var BATTERY_DEFAULT_MAX_AGE_S = 3600;
|
|
|
28
28
|
* ffmpeg fallback, caching) is this addon's concern; stream-broker only
|
|
29
29
|
* publishes stream endpoints.
|
|
30
30
|
*/
|
|
31
|
-
var SnapshotAddon = class extends _camstack_types.BaseAddon {
|
|
31
|
+
var SnapshotAddon = class SnapshotAddon extends _camstack_types.BaseAddon {
|
|
32
32
|
cache = /* @__PURE__ */ new Map();
|
|
33
|
+
/**
|
|
34
|
+
* Cached resolution of `pipelineOrchestrator.getIngestOwner` — SnapshotAddon
|
|
35
|
+
* has no per-request attach hook to resolve this fresh, so it's cached with
|
|
36
|
+
* a short TTL and invalidated on `onConfigChanged` (covers the common case
|
|
37
|
+
* of an operator re-saving cluster/agent settings). A stale owner for up to
|
|
38
|
+
* `OWNER_CACHE_TTL_MS` just means a snapshot fallback briefly targets the
|
|
39
|
+
* previous ingest owner — self-heals on the next resolve.
|
|
40
|
+
*/
|
|
41
|
+
ownerCache = null;
|
|
42
|
+
static OWNER_CACHE_TTL_MS = 3e4;
|
|
33
43
|
constructor() {
|
|
34
44
|
super({ staleTtlMs: 6e4 });
|
|
35
45
|
}
|
|
@@ -49,6 +59,18 @@ var SnapshotAddon = class extends _camstack_types.BaseAddon {
|
|
|
49
59
|
}
|
|
50
60
|
async onShutdown() {
|
|
51
61
|
this.cache.clear();
|
|
62
|
+
this.ownerCache = null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Drop the cached ingest owner whenever addon-level settings change —
|
|
66
|
+
* cheap, correct-by-construction refresh point (BaseAddon calls this
|
|
67
|
+
* after every `updateGlobalSettings`/`updateAddonSettings`). Combined
|
|
68
|
+
* with the TTL in `resolveIngestOwner`, a cluster topology change is
|
|
69
|
+
* picked up either on the next settings save or within
|
|
70
|
+
* `OWNER_CACHE_TTL_MS`, whichever comes first.
|
|
71
|
+
*/
|
|
72
|
+
async onConfigChanged() {
|
|
73
|
+
this.ownerCache = null;
|
|
52
74
|
}
|
|
53
75
|
globalSettingsSchema() {
|
|
54
76
|
return this.schema({ sections: [{
|
|
@@ -191,11 +213,24 @@ var SnapshotAddon = class extends _camstack_types.BaseAddon {
|
|
|
191
213
|
* first to go idle, racing every snapshot with a broker resume.
|
|
192
214
|
* Now we ask the orchestrator of streams which one is warm and grab
|
|
193
215
|
* from there.
|
|
216
|
+
*
|
|
217
|
+
* Source acquisition is pinned to the cluster's ingest owner (the node
|
|
218
|
+
* the stream-broker actually runs on) instead of the device-scoped
|
|
219
|
+
* `dev.cameraStreams` facade — `dev.cameraStreams` resolves against
|
|
220
|
+
* whichever node the cap-router picks by default, which double-pulls
|
|
221
|
+
* the camera (or dials a hub-local restream that isn't serving) when
|
|
222
|
+
* `ingestNode != hub`. `fetchSnapshotBrokerEntries` does the pinned
|
|
223
|
+
* fetch + owner-relative URL host rewrite; hub-owner + no reachable
|
|
224
|
+
* host is byte-identical to the previous unpinned call.
|
|
194
225
|
*/
|
|
195
226
|
async grabFrameFromBroker(deviceId, preferredStreamId) {
|
|
196
|
-
const dev = await this.ctx.fetchDevice(deviceId);
|
|
197
227
|
const prefix = `${deviceId}/`;
|
|
198
|
-
const
|
|
228
|
+
const owner = await this.resolveIngestOwner();
|
|
229
|
+
const { entries: deviceEntries, slots: profileSlots } = await fetchSnapshotBrokerEntries(this.ctx.api, {
|
|
230
|
+
deviceId,
|
|
231
|
+
ownerNodeId: owner.ownerNodeId,
|
|
232
|
+
reachableHost: owner.reachableHost
|
|
233
|
+
});
|
|
199
234
|
const usable = deviceEntries.filter((e) => e.enabled && !!e.url);
|
|
200
235
|
if (usable.length === 0) return null;
|
|
201
236
|
if (preferredStreamId && preferredStreamId !== "auto") {
|
|
@@ -225,6 +260,35 @@ var SnapshotAddon = class extends _camstack_types.BaseAddon {
|
|
|
225
260
|
return null;
|
|
226
261
|
}
|
|
227
262
|
/**
|
|
263
|
+
* Cached, TTL-bounded resolution of the cluster's camera-source owner.
|
|
264
|
+
* See `ownerCache` doc for the invalidation story.
|
|
265
|
+
*/
|
|
266
|
+
async resolveIngestOwner() {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
if (this.ownerCache && now - this.ownerCache.ts < SnapshotAddon.OWNER_CACHE_TTL_MS) return this.ownerCache.owner;
|
|
269
|
+
const owner = await this.fetchIngestOwner();
|
|
270
|
+
this.ownerCache = {
|
|
271
|
+
owner,
|
|
272
|
+
ts: now
|
|
273
|
+
};
|
|
274
|
+
return owner;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Same graceful degrade as the recorder's `resolveIngestOwner`
|
|
278
|
+
* (`packages/addon-pipeline/src/recorder/addon/index.ts`) — the cap may
|
|
279
|
+
* not be mounted yet (pre-image-release) or the pipeline-orchestrator
|
|
280
|
+
* addon may be absent entirely; either way we fall back to `'hub'`,
|
|
281
|
+
* which is byte-identical to the previous hardcoded hub-only behaviour.
|
|
282
|
+
*/
|
|
283
|
+
async fetchIngestOwner() {
|
|
284
|
+
try {
|
|
285
|
+
return await this.ctx.api.pipelineOrchestrator.getIngestOwner.query();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
this.ctx.logger.warn("snapshot: getIngestOwner unavailable — defaulting to hub (cap not mounted yet?)", { meta: { error: (0, _camstack_types.errMsg)(err) } });
|
|
288
|
+
return { ownerNodeId: "hub" };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
228
292
|
* Ffmpeg grab with one retry on the broker-cold-start error
|
|
229
293
|
* signature. Covers the window between "client connected" and
|
|
230
294
|
* "first keyframe" when a suspended broker resumes.
|
|
@@ -264,10 +328,24 @@ var SnapshotAddon = class extends _camstack_types.BaseAddon {
|
|
|
264
328
|
* grabbing a frame is free (a consumer is already keeping the
|
|
265
329
|
* stream warm). When everything is suspended, the fallback would
|
|
266
330
|
* dial the camera and wake it — defeats the sleeping cache.
|
|
331
|
+
*
|
|
332
|
+
* Pinned to the ingest owner via `fetchSnapshotBrokerEntries` — same
|
|
333
|
+
* rationale as `grabFrameFromBroker`: the device-scoped
|
|
334
|
+
* `dev.cameraStreams` facade resolves against whichever node the
|
|
335
|
+
* cap-router picks by default, which is wrong once `ingestNode !=
|
|
336
|
+
* hub` (queries the hub for slots that only exist on the agent,
|
|
337
|
+
* always reporting no streaming broker). Owner === hub (no reachable
|
|
338
|
+
* host) is byte-identical to the previous unpinned call.
|
|
267
339
|
*/
|
|
268
340
|
async hasStreamingBrokerForDevice(deviceId) {
|
|
269
341
|
try {
|
|
270
|
-
|
|
342
|
+
const owner = await this.resolveIngestOwner();
|
|
343
|
+
const { slots } = await fetchSnapshotBrokerEntries(this.ctx.api, {
|
|
344
|
+
deviceId,
|
|
345
|
+
ownerNodeId: owner.ownerNodeId,
|
|
346
|
+
reachableHost: owner.reachableHost
|
|
347
|
+
});
|
|
348
|
+
return slots.some((s) => s.status === "streaming");
|
|
271
349
|
} catch {
|
|
272
350
|
return false;
|
|
273
351
|
}
|
|
@@ -379,13 +457,27 @@ var SnapshotAddon = class extends _camstack_types.BaseAddon {
|
|
|
379
457
|
async isDeviceBattery(deviceId) {
|
|
380
458
|
return (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false;
|
|
381
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Feeds the settings-UI stream picker. Pinned to the ingest owner via
|
|
462
|
+
* `fetchSnapshotBrokerEntries` — the device-scoped `dev.cameraStreams`
|
|
463
|
+
* facade this used to call resolves against whichever node the
|
|
464
|
+
* cap-router picks by default, which is empty once `ingestNode !=
|
|
465
|
+
* hub` (the picker would render nothing but "Auto"). Owner === hub
|
|
466
|
+
* (no reachable host) is byte-identical to the previous unpinned call.
|
|
467
|
+
*/
|
|
382
468
|
async getStreamOptions(deviceId) {
|
|
383
469
|
const prefix = `${deviceId}/`;
|
|
384
470
|
try {
|
|
471
|
+
const owner = await this.resolveIngestOwner();
|
|
472
|
+
const { entries } = await fetchSnapshotBrokerEntries(this.ctx.api, {
|
|
473
|
+
deviceId,
|
|
474
|
+
ownerNodeId: owner.ownerNodeId,
|
|
475
|
+
reachableHost: owner.reachableHost
|
|
476
|
+
});
|
|
385
477
|
return [{
|
|
386
478
|
value: "auto",
|
|
387
479
|
label: "Auto"
|
|
388
|
-
}, ...
|
|
480
|
+
}, ...entries.filter((e) => e.enabled).map((e) => e.brokerId.slice(prefix.length)).map((id) => ({
|
|
389
481
|
value: id,
|
|
390
482
|
label: (0, _camstack_types.streamQualityLabel)(id)
|
|
391
483
|
}))];
|
|
@@ -422,6 +514,39 @@ function isAbsentNativeError(msg) {
|
|
|
422
514
|
return msg.includes("no provider for") || msg.includes("no native provider for capability");
|
|
423
515
|
}
|
|
424
516
|
/**
|
|
517
|
+
* Pinned, testable fetch of this device's RTSP restream entries + profile
|
|
518
|
+
* slots from the system `streamBroker` cap. Module-level (not a class
|
|
519
|
+
* method) so `grabFrameFromBroker`'s owner-pin + host-rewrite contract is
|
|
520
|
+
* directly unit-testable without instantiating the addon — mirrors
|
|
521
|
+
* `acquireSessionDecodeRestreamFrom` in `pipeline-runner/index.ts`.
|
|
522
|
+
*
|
|
523
|
+
* Replaces the former `dev.cameraStreams.getRtspEntries({})` /
|
|
524
|
+
* `getBrokerStreams({})` device-scoped facade calls, which resolve against
|
|
525
|
+
* whichever node the cap-router picks by default — wrong once
|
|
526
|
+
* `ingestNode != hub` (double-pulls the camera, or dials a hub-local
|
|
527
|
+
* restream nobody is serving). `getAllRtspEntries`/`listAllProfileSlots`
|
|
528
|
+
* are cluster-wide system-cap methods; pinning the call with `nodePin` +
|
|
529
|
+
* filtering by the `${deviceId}/` brokerId prefix reproduces the exact
|
|
530
|
+
* device-scoped result set the facade used to return.
|
|
531
|
+
*
|
|
532
|
+
* `reachableHost`, when present, rides as the cap's own `hostname` input —
|
|
533
|
+
* the broker rewrites the returned URLs' host itself (same mechanism
|
|
534
|
+
* `getStreamWithCodec` callers use for cross-node pulls), so no separate
|
|
535
|
+
* client-side host-substitution is needed here. Omitting `hostname`
|
|
536
|
+
* (owner === hub, no reachable host registered) leaves the broker's
|
|
537
|
+
* default `127.0.0.1` URLs untouched — byte-identical to today.
|
|
538
|
+
*/
|
|
539
|
+
async function fetchSnapshotBrokerEntries(api, params) {
|
|
540
|
+
const { deviceId, ownerNodeId, reachableHost } = params;
|
|
541
|
+
const prefix = `${deviceId}/`;
|
|
542
|
+
const pin = (0, _camstack_types.nodePin)(ownerNodeId);
|
|
543
|
+
const [allEntries, allSlots] = await Promise.all([api.streamBroker.getAllRtspEntries.query(reachableHost !== void 0 ? { hostname: reachableHost } : {}, pin), api.streamBroker.listAllProfileSlots.query(void 0, pin)]);
|
|
544
|
+
return {
|
|
545
|
+
entries: allEntries.filter((e) => e.brokerId.startsWith(prefix)),
|
|
546
|
+
slots: allSlots.filter((s) => s.deviceId === deviceId)
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
425
550
|
* Quality ordering for broker picker: `high` > `mid` > `low` > other.
|
|
426
551
|
* The streamId is the suffix after `${deviceId}/` in the brokerId.
|
|
427
552
|
* Unknown labels fall through to 0 so they land at the bottom of the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseAddon, DeviceFeature, DeviceType, errMsg, snapshotCapability, streamQualityLabel } from "@camstack/types";
|
|
1
|
+
import { BaseAddon, DeviceFeature, DeviceType, errMsg, nodePin, snapshotCapability, streamQualityLabel } from "@camstack/types";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
//#region src/builtins/snapshot/snapshot.addon.ts
|
|
4
4
|
/** Default cache window for non-battery cams (seconds). 10s feels live. */
|
|
@@ -23,8 +23,18 @@ var BATTERY_DEFAULT_MAX_AGE_S = 3600;
|
|
|
23
23
|
* ffmpeg fallback, caching) is this addon's concern; stream-broker only
|
|
24
24
|
* publishes stream endpoints.
|
|
25
25
|
*/
|
|
26
|
-
var SnapshotAddon = class extends BaseAddon {
|
|
26
|
+
var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
27
27
|
cache = /* @__PURE__ */ new Map();
|
|
28
|
+
/**
|
|
29
|
+
* Cached resolution of `pipelineOrchestrator.getIngestOwner` — SnapshotAddon
|
|
30
|
+
* has no per-request attach hook to resolve this fresh, so it's cached with
|
|
31
|
+
* a short TTL and invalidated on `onConfigChanged` (covers the common case
|
|
32
|
+
* of an operator re-saving cluster/agent settings). A stale owner for up to
|
|
33
|
+
* `OWNER_CACHE_TTL_MS` just means a snapshot fallback briefly targets the
|
|
34
|
+
* previous ingest owner — self-heals on the next resolve.
|
|
35
|
+
*/
|
|
36
|
+
ownerCache = null;
|
|
37
|
+
static OWNER_CACHE_TTL_MS = 3e4;
|
|
28
38
|
constructor() {
|
|
29
39
|
super({ staleTtlMs: 6e4 });
|
|
30
40
|
}
|
|
@@ -44,6 +54,18 @@ var SnapshotAddon = class extends BaseAddon {
|
|
|
44
54
|
}
|
|
45
55
|
async onShutdown() {
|
|
46
56
|
this.cache.clear();
|
|
57
|
+
this.ownerCache = null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Drop the cached ingest owner whenever addon-level settings change —
|
|
61
|
+
* cheap, correct-by-construction refresh point (BaseAddon calls this
|
|
62
|
+
* after every `updateGlobalSettings`/`updateAddonSettings`). Combined
|
|
63
|
+
* with the TTL in `resolveIngestOwner`, a cluster topology change is
|
|
64
|
+
* picked up either on the next settings save or within
|
|
65
|
+
* `OWNER_CACHE_TTL_MS`, whichever comes first.
|
|
66
|
+
*/
|
|
67
|
+
async onConfigChanged() {
|
|
68
|
+
this.ownerCache = null;
|
|
47
69
|
}
|
|
48
70
|
globalSettingsSchema() {
|
|
49
71
|
return this.schema({ sections: [{
|
|
@@ -186,11 +208,24 @@ var SnapshotAddon = class extends BaseAddon {
|
|
|
186
208
|
* first to go idle, racing every snapshot with a broker resume.
|
|
187
209
|
* Now we ask the orchestrator of streams which one is warm and grab
|
|
188
210
|
* from there.
|
|
211
|
+
*
|
|
212
|
+
* Source acquisition is pinned to the cluster's ingest owner (the node
|
|
213
|
+
* the stream-broker actually runs on) instead of the device-scoped
|
|
214
|
+
* `dev.cameraStreams` facade — `dev.cameraStreams` resolves against
|
|
215
|
+
* whichever node the cap-router picks by default, which double-pulls
|
|
216
|
+
* the camera (or dials a hub-local restream that isn't serving) when
|
|
217
|
+
* `ingestNode != hub`. `fetchSnapshotBrokerEntries` does the pinned
|
|
218
|
+
* fetch + owner-relative URL host rewrite; hub-owner + no reachable
|
|
219
|
+
* host is byte-identical to the previous unpinned call.
|
|
189
220
|
*/
|
|
190
221
|
async grabFrameFromBroker(deviceId, preferredStreamId) {
|
|
191
|
-
const dev = await this.ctx.fetchDevice(deviceId);
|
|
192
222
|
const prefix = `${deviceId}/`;
|
|
193
|
-
const
|
|
223
|
+
const owner = await this.resolveIngestOwner();
|
|
224
|
+
const { entries: deviceEntries, slots: profileSlots } = await fetchSnapshotBrokerEntries(this.ctx.api, {
|
|
225
|
+
deviceId,
|
|
226
|
+
ownerNodeId: owner.ownerNodeId,
|
|
227
|
+
reachableHost: owner.reachableHost
|
|
228
|
+
});
|
|
194
229
|
const usable = deviceEntries.filter((e) => e.enabled && !!e.url);
|
|
195
230
|
if (usable.length === 0) return null;
|
|
196
231
|
if (preferredStreamId && preferredStreamId !== "auto") {
|
|
@@ -220,6 +255,35 @@ var SnapshotAddon = class extends BaseAddon {
|
|
|
220
255
|
return null;
|
|
221
256
|
}
|
|
222
257
|
/**
|
|
258
|
+
* Cached, TTL-bounded resolution of the cluster's camera-source owner.
|
|
259
|
+
* See `ownerCache` doc for the invalidation story.
|
|
260
|
+
*/
|
|
261
|
+
async resolveIngestOwner() {
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
if (this.ownerCache && now - this.ownerCache.ts < SnapshotAddon.OWNER_CACHE_TTL_MS) return this.ownerCache.owner;
|
|
264
|
+
const owner = await this.fetchIngestOwner();
|
|
265
|
+
this.ownerCache = {
|
|
266
|
+
owner,
|
|
267
|
+
ts: now
|
|
268
|
+
};
|
|
269
|
+
return owner;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Same graceful degrade as the recorder's `resolveIngestOwner`
|
|
273
|
+
* (`packages/addon-pipeline/src/recorder/addon/index.ts`) — the cap may
|
|
274
|
+
* not be mounted yet (pre-image-release) or the pipeline-orchestrator
|
|
275
|
+
* addon may be absent entirely; either way we fall back to `'hub'`,
|
|
276
|
+
* which is byte-identical to the previous hardcoded hub-only behaviour.
|
|
277
|
+
*/
|
|
278
|
+
async fetchIngestOwner() {
|
|
279
|
+
try {
|
|
280
|
+
return await this.ctx.api.pipelineOrchestrator.getIngestOwner.query();
|
|
281
|
+
} catch (err) {
|
|
282
|
+
this.ctx.logger.warn("snapshot: getIngestOwner unavailable — defaulting to hub (cap not mounted yet?)", { meta: { error: errMsg(err) } });
|
|
283
|
+
return { ownerNodeId: "hub" };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
223
287
|
* Ffmpeg grab with one retry on the broker-cold-start error
|
|
224
288
|
* signature. Covers the window between "client connected" and
|
|
225
289
|
* "first keyframe" when a suspended broker resumes.
|
|
@@ -259,10 +323,24 @@ var SnapshotAddon = class extends BaseAddon {
|
|
|
259
323
|
* grabbing a frame is free (a consumer is already keeping the
|
|
260
324
|
* stream warm). When everything is suspended, the fallback would
|
|
261
325
|
* dial the camera and wake it — defeats the sleeping cache.
|
|
326
|
+
*
|
|
327
|
+
* Pinned to the ingest owner via `fetchSnapshotBrokerEntries` — same
|
|
328
|
+
* rationale as `grabFrameFromBroker`: the device-scoped
|
|
329
|
+
* `dev.cameraStreams` facade resolves against whichever node the
|
|
330
|
+
* cap-router picks by default, which is wrong once `ingestNode !=
|
|
331
|
+
* hub` (queries the hub for slots that only exist on the agent,
|
|
332
|
+
* always reporting no streaming broker). Owner === hub (no reachable
|
|
333
|
+
* host) is byte-identical to the previous unpinned call.
|
|
262
334
|
*/
|
|
263
335
|
async hasStreamingBrokerForDevice(deviceId) {
|
|
264
336
|
try {
|
|
265
|
-
|
|
337
|
+
const owner = await this.resolveIngestOwner();
|
|
338
|
+
const { slots } = await fetchSnapshotBrokerEntries(this.ctx.api, {
|
|
339
|
+
deviceId,
|
|
340
|
+
ownerNodeId: owner.ownerNodeId,
|
|
341
|
+
reachableHost: owner.reachableHost
|
|
342
|
+
});
|
|
343
|
+
return slots.some((s) => s.status === "streaming");
|
|
266
344
|
} catch {
|
|
267
345
|
return false;
|
|
268
346
|
}
|
|
@@ -374,13 +452,27 @@ var SnapshotAddon = class extends BaseAddon {
|
|
|
374
452
|
async isDeviceBattery(deviceId) {
|
|
375
453
|
return (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false;
|
|
376
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Feeds the settings-UI stream picker. Pinned to the ingest owner via
|
|
457
|
+
* `fetchSnapshotBrokerEntries` — the device-scoped `dev.cameraStreams`
|
|
458
|
+
* facade this used to call resolves against whichever node the
|
|
459
|
+
* cap-router picks by default, which is empty once `ingestNode !=
|
|
460
|
+
* hub` (the picker would render nothing but "Auto"). Owner === hub
|
|
461
|
+
* (no reachable host) is byte-identical to the previous unpinned call.
|
|
462
|
+
*/
|
|
377
463
|
async getStreamOptions(deviceId) {
|
|
378
464
|
const prefix = `${deviceId}/`;
|
|
379
465
|
try {
|
|
466
|
+
const owner = await this.resolveIngestOwner();
|
|
467
|
+
const { entries } = await fetchSnapshotBrokerEntries(this.ctx.api, {
|
|
468
|
+
deviceId,
|
|
469
|
+
ownerNodeId: owner.ownerNodeId,
|
|
470
|
+
reachableHost: owner.reachableHost
|
|
471
|
+
});
|
|
380
472
|
return [{
|
|
381
473
|
value: "auto",
|
|
382
474
|
label: "Auto"
|
|
383
|
-
}, ...
|
|
475
|
+
}, ...entries.filter((e) => e.enabled).map((e) => e.brokerId.slice(prefix.length)).map((id) => ({
|
|
384
476
|
value: id,
|
|
385
477
|
label: streamQualityLabel(id)
|
|
386
478
|
}))];
|
|
@@ -417,6 +509,39 @@ function isAbsentNativeError(msg) {
|
|
|
417
509
|
return msg.includes("no provider for") || msg.includes("no native provider for capability");
|
|
418
510
|
}
|
|
419
511
|
/**
|
|
512
|
+
* Pinned, testable fetch of this device's RTSP restream entries + profile
|
|
513
|
+
* slots from the system `streamBroker` cap. Module-level (not a class
|
|
514
|
+
* method) so `grabFrameFromBroker`'s owner-pin + host-rewrite contract is
|
|
515
|
+
* directly unit-testable without instantiating the addon — mirrors
|
|
516
|
+
* `acquireSessionDecodeRestreamFrom` in `pipeline-runner/index.ts`.
|
|
517
|
+
*
|
|
518
|
+
* Replaces the former `dev.cameraStreams.getRtspEntries({})` /
|
|
519
|
+
* `getBrokerStreams({})` device-scoped facade calls, which resolve against
|
|
520
|
+
* whichever node the cap-router picks by default — wrong once
|
|
521
|
+
* `ingestNode != hub` (double-pulls the camera, or dials a hub-local
|
|
522
|
+
* restream nobody is serving). `getAllRtspEntries`/`listAllProfileSlots`
|
|
523
|
+
* are cluster-wide system-cap methods; pinning the call with `nodePin` +
|
|
524
|
+
* filtering by the `${deviceId}/` brokerId prefix reproduces the exact
|
|
525
|
+
* device-scoped result set the facade used to return.
|
|
526
|
+
*
|
|
527
|
+
* `reachableHost`, when present, rides as the cap's own `hostname` input —
|
|
528
|
+
* the broker rewrites the returned URLs' host itself (same mechanism
|
|
529
|
+
* `getStreamWithCodec` callers use for cross-node pulls), so no separate
|
|
530
|
+
* client-side host-substitution is needed here. Omitting `hostname`
|
|
531
|
+
* (owner === hub, no reachable host registered) leaves the broker's
|
|
532
|
+
* default `127.0.0.1` URLs untouched — byte-identical to today.
|
|
533
|
+
*/
|
|
534
|
+
async function fetchSnapshotBrokerEntries(api, params) {
|
|
535
|
+
const { deviceId, ownerNodeId, reachableHost } = params;
|
|
536
|
+
const prefix = `${deviceId}/`;
|
|
537
|
+
const pin = nodePin(ownerNodeId);
|
|
538
|
+
const [allEntries, allSlots] = await Promise.all([api.streamBroker.getAllRtspEntries.query(reachableHost !== void 0 ? { hostname: reachableHost } : {}, pin), api.streamBroker.listAllProfileSlots.query(void 0, pin)]);
|
|
539
|
+
return {
|
|
540
|
+
entries: allEntries.filter((e) => e.brokerId.startsWith(prefix)),
|
|
541
|
+
slots: allSlots.filter((s) => s.deviceId === deviceId)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
420
545
|
* Quality ordering for broker picker: `high` > `mid` > `low` > other.
|
|
421
546
|
* The streamId is the suffix after `${deviceId}/` in the brokerId.
|
|
422
547
|
* Unknown labels fall through to 0 so they land at the bottom of the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProviderRegistration, BaseAddon } from '@camstack/types';
|
|
1
|
+
import { AddonApi, ProfileSlot, ProviderRegistration, RtspRestreamEntry, BaseAddon } from '@camstack/types';
|
|
2
2
|
interface SnapshotAddonConfig {
|
|
3
3
|
/**
|
|
4
4
|
* Last-resort cache age (ms): if every live capture path fails
|
|
@@ -30,9 +30,28 @@ interface SnapshotAddonConfig {
|
|
|
30
30
|
*/
|
|
31
31
|
export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
32
32
|
private readonly cache;
|
|
33
|
+
/**
|
|
34
|
+
* Cached resolution of `pipelineOrchestrator.getIngestOwner` — SnapshotAddon
|
|
35
|
+
* has no per-request attach hook to resolve this fresh, so it's cached with
|
|
36
|
+
* a short TTL and invalidated on `onConfigChanged` (covers the common case
|
|
37
|
+
* of an operator re-saving cluster/agent settings). A stale owner for up to
|
|
38
|
+
* `OWNER_CACHE_TTL_MS` just means a snapshot fallback briefly targets the
|
|
39
|
+
* previous ingest owner — self-heals on the next resolve.
|
|
40
|
+
*/
|
|
41
|
+
private ownerCache;
|
|
42
|
+
private static readonly OWNER_CACHE_TTL_MS;
|
|
33
43
|
constructor();
|
|
34
44
|
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
35
45
|
protected onShutdown(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Drop the cached ingest owner whenever addon-level settings change —
|
|
48
|
+
* cheap, correct-by-construction refresh point (BaseAddon calls this
|
|
49
|
+
* after every `updateGlobalSettings`/`updateAddonSettings`). Combined
|
|
50
|
+
* with the TTL in `resolveIngestOwner`, a cluster topology change is
|
|
51
|
+
* picked up either on the next settings save or within
|
|
52
|
+
* `OWNER_CACHE_TTL_MS`, whichever comes first.
|
|
53
|
+
*/
|
|
54
|
+
protected onConfigChanged(): Promise<void>;
|
|
36
55
|
protected globalSettingsSchema(): import('@camstack/types').ConfigUISchema;
|
|
37
56
|
private getSnapshot;
|
|
38
57
|
/**
|
|
@@ -63,8 +82,30 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
63
82
|
* first to go idle, racing every snapshot with a broker resume.
|
|
64
83
|
* Now we ask the orchestrator of streams which one is warm and grab
|
|
65
84
|
* from there.
|
|
85
|
+
*
|
|
86
|
+
* Source acquisition is pinned to the cluster's ingest owner (the node
|
|
87
|
+
* the stream-broker actually runs on) instead of the device-scoped
|
|
88
|
+
* `dev.cameraStreams` facade — `dev.cameraStreams` resolves against
|
|
89
|
+
* whichever node the cap-router picks by default, which double-pulls
|
|
90
|
+
* the camera (or dials a hub-local restream that isn't serving) when
|
|
91
|
+
* `ingestNode != hub`. `fetchSnapshotBrokerEntries` does the pinned
|
|
92
|
+
* fetch + owner-relative URL host rewrite; hub-owner + no reachable
|
|
93
|
+
* host is byte-identical to the previous unpinned call.
|
|
66
94
|
*/
|
|
67
95
|
private grabFrameFromBroker;
|
|
96
|
+
/**
|
|
97
|
+
* Cached, TTL-bounded resolution of the cluster's camera-source owner.
|
|
98
|
+
* See `ownerCache` doc for the invalidation story.
|
|
99
|
+
*/
|
|
100
|
+
private resolveIngestOwner;
|
|
101
|
+
/**
|
|
102
|
+
* Same graceful degrade as the recorder's `resolveIngestOwner`
|
|
103
|
+
* (`packages/addon-pipeline/src/recorder/addon/index.ts`) — the cap may
|
|
104
|
+
* not be mounted yet (pre-image-release) or the pipeline-orchestrator
|
|
105
|
+
* addon may be absent entirely; either way we fall back to `'hub'`,
|
|
106
|
+
* which is byte-identical to the previous hardcoded hub-only behaviour.
|
|
107
|
+
*/
|
|
108
|
+
private fetchIngestOwner;
|
|
68
109
|
/**
|
|
69
110
|
* Ffmpeg grab with one retry on the broker-cold-start error
|
|
70
111
|
* signature. Covers the window between "client connected" and
|
|
@@ -87,6 +128,14 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
87
128
|
* grabbing a frame is free (a consumer is already keeping the
|
|
88
129
|
* stream warm). When everything is suspended, the fallback would
|
|
89
130
|
* dial the camera and wake it — defeats the sleeping cache.
|
|
131
|
+
*
|
|
132
|
+
* Pinned to the ingest owner via `fetchSnapshotBrokerEntries` — same
|
|
133
|
+
* rationale as `grabFrameFromBroker`: the device-scoped
|
|
134
|
+
* `dev.cameraStreams` facade resolves against whichever node the
|
|
135
|
+
* cap-router picks by default, which is wrong once `ingestNode !=
|
|
136
|
+
* hub` (queries the hub for slots that only exist on the agent,
|
|
137
|
+
* always reporting no streaming broker). Owner === hub (no reachable
|
|
138
|
+
* host) is byte-identical to the previous unpinned call.
|
|
90
139
|
*/
|
|
91
140
|
private hasStreamingBrokerForDevice;
|
|
92
141
|
/**
|
|
@@ -114,7 +163,48 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
114
163
|
private lookupDeviceMeta;
|
|
115
164
|
/** Settings-UI helper — battery flag drives the default max-age in the field description. */
|
|
116
165
|
private isDeviceBattery;
|
|
166
|
+
/**
|
|
167
|
+
* Feeds the settings-UI stream picker. Pinned to the ingest owner via
|
|
168
|
+
* `fetchSnapshotBrokerEntries` — the device-scoped `dev.cameraStreams`
|
|
169
|
+
* facade this used to call resolves against whichever node the
|
|
170
|
+
* cap-router picks by default, which is empty once `ingestNode !=
|
|
171
|
+
* hub` (the picker would render nothing but "Auto"). Owner === hub
|
|
172
|
+
* (no reachable host) is byte-identical to the previous unpinned call.
|
|
173
|
+
*/
|
|
117
174
|
private getStreamOptions;
|
|
118
175
|
private saveDeviceSettingsPatch;
|
|
119
176
|
}
|
|
177
|
+
/** Result of {@link fetchSnapshotBrokerEntries} — this device's RTSP entries + profile slots. */
|
|
178
|
+
interface SnapshotBrokerEntries {
|
|
179
|
+
readonly entries: readonly RtspRestreamEntry[];
|
|
180
|
+
readonly slots: readonly ProfileSlot[];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Pinned, testable fetch of this device's RTSP restream entries + profile
|
|
184
|
+
* slots from the system `streamBroker` cap. Module-level (not a class
|
|
185
|
+
* method) so `grabFrameFromBroker`'s owner-pin + host-rewrite contract is
|
|
186
|
+
* directly unit-testable without instantiating the addon — mirrors
|
|
187
|
+
* `acquireSessionDecodeRestreamFrom` in `pipeline-runner/index.ts`.
|
|
188
|
+
*
|
|
189
|
+
* Replaces the former `dev.cameraStreams.getRtspEntries({})` /
|
|
190
|
+
* `getBrokerStreams({})` device-scoped facade calls, which resolve against
|
|
191
|
+
* whichever node the cap-router picks by default — wrong once
|
|
192
|
+
* `ingestNode != hub` (double-pulls the camera, or dials a hub-local
|
|
193
|
+
* restream nobody is serving). `getAllRtspEntries`/`listAllProfileSlots`
|
|
194
|
+
* are cluster-wide system-cap methods; pinning the call with `nodePin` +
|
|
195
|
+
* filtering by the `${deviceId}/` brokerId prefix reproduces the exact
|
|
196
|
+
* device-scoped result set the facade used to return.
|
|
197
|
+
*
|
|
198
|
+
* `reachableHost`, when present, rides as the cap's own `hostname` input —
|
|
199
|
+
* the broker rewrites the returned URLs' host itself (same mechanism
|
|
200
|
+
* `getStreamWithCodec` callers use for cross-node pulls), so no separate
|
|
201
|
+
* client-side host-substitution is needed here. Omitting `hostname`
|
|
202
|
+
* (owner === hub, no reachable host registered) leaves the broker's
|
|
203
|
+
* default `127.0.0.1` URLs untouched — byte-identical to today.
|
|
204
|
+
*/
|
|
205
|
+
export declare function fetchSnapshotBrokerEntries(api: AddonApi, params: {
|
|
206
|
+
readonly deviceId: number;
|
|
207
|
+
readonly ownerNodeId: string;
|
|
208
|
+
readonly reachableHost?: string;
|
|
209
|
+
}): Promise<SnapshotBrokerEntries>;
|
|
120
210
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ const require_builtins_local_auth_local_auth_addon = require("./builtins/local-a
|
|
|
20
20
|
require("./builtins/local-auth/index.js");
|
|
21
21
|
const require_builtins_device_manager_device_manager_addon = require("./builtins/device-manager/device-manager.addon.js");
|
|
22
22
|
require("./builtins/device-manager/index.js");
|
|
23
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
23
|
+
const require_manifest_python_deps = require("./manifest-python-deps-CAnO1wPx.js");
|
|
24
24
|
const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
|
|
25
25
|
let _camstack_types_node = require("@camstack/types/node");
|
|
26
26
|
let node_http = require("node:http");
|
|
@@ -4917,13 +4917,24 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
4917
4917
|
* this builder's output to that static type at the single documented
|
|
4918
4918
|
* boundary, so the SDK keeps full procedure inference.
|
|
4919
4919
|
*
|
|
4920
|
-
*
|
|
4921
|
-
*
|
|
4922
|
-
*
|
|
4923
|
-
*
|
|
4924
|
-
*
|
|
4925
|
-
*
|
|
4926
|
-
*
|
|
4920
|
+
* The per-method procedure shape is mechanically derived from each
|
|
4921
|
+
* `cap.methods.*` (input zod schema + kind) + the auto-injected
|
|
4922
|
+
* `getStatus` / 3 settings-contribution methods (via `expandCapMethods`).
|
|
4923
|
+
* Provider resolution is derived from the declarative `mount` hint
|
|
4924
|
+
* (`resolveCapMount`) — the same decision the codegen
|
|
4925
|
+
* `generate-cap-mounts.ts` baked in.
|
|
4926
|
+
*
|
|
4927
|
+
* OUTPUT PASS-THROUGH (2026-07-11): unlike the historical codegen, the
|
|
4928
|
+
* builder deliberately does NOT apply `.output(schema)`. Re-validating an
|
|
4929
|
+
* output the hub does not own against the hub's OWN (framework-versioned,
|
|
4930
|
+
* therefore potentially stale) copy of the cap def silently STRIPS fields
|
|
4931
|
+
* a fresher addon added (a self-contained addon bundles its own newer
|
|
4932
|
+
* `@camstack/types`; the hub-closure copy lags). That is exactly how
|
|
4933
|
+
* `models[].group.resolution` vanished from `pipelineExecutor.getSchema`.
|
|
4934
|
+
* Cap dispatch is structural (CLAUDE.md invariant): the owning provider is
|
|
4935
|
+
* typed to return a complete `z.infer<output>` and is the SOLE authority
|
|
4936
|
+
* for the payload shape, so the hub forwards it verbatim. Input validation
|
|
4937
|
+
* is retained (`.input(...)`) — the hub still guards the client boundary.
|
|
4927
4938
|
*
|
|
4928
4939
|
* This module owns NO tRPC primitives of its own: `@camstack/system`
|
|
4929
4940
|
* cannot import the server's `TrpcContext`-bound procedures. The server
|
|
@@ -4931,32 +4942,6 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
4931
4942
|
* `buildCapRouters(t, services)` contract), keeping the kernel free of any
|
|
4932
4943
|
* backend coupling (self-sufficiency invariant).
|
|
4933
4944
|
*/
|
|
4934
|
-
/** Auth level → procedure key. Mirrors codegen's AUTH_PROCEDURE_MAP. */
|
|
4935
|
-
function procedureKeyFor(auth) {
|
|
4936
|
-
if (auth === "public") return "public";
|
|
4937
|
-
if (auth === "admin" || auth === "superAdmin") return "admin";
|
|
4938
|
-
return "protected";
|
|
4939
|
-
}
|
|
4940
|
-
/** Zod schema is `z.void()` — input carries no data. Mirrors codegen's isVoidInput. */
|
|
4941
|
-
function isVoidInput(schema) {
|
|
4942
|
-
const def = schema._def;
|
|
4943
|
-
return schema.constructor?.name === "ZodVoid" || def?.type === "void" || def?.typeName === "ZodVoid";
|
|
4944
|
-
}
|
|
4945
|
-
/** Zod schema is `z.object()` — supports `.loose()` for nodeId passthrough. Mirrors codegen's isObjectInput. */
|
|
4946
|
-
function isObjectInput(schema) {
|
|
4947
|
-
const def = schema._def;
|
|
4948
|
-
return schema.constructor?.name === "ZodObject" || def?.type === "object" || def?.typeName === "ZodObject";
|
|
4949
|
-
}
|
|
4950
|
-
/**
|
|
4951
|
-
* Apply `.loose()` to an object input schema when available (Zod 4) so the
|
|
4952
|
-
* router accepts the out-of-band `nodeId` / `addonId` selector keys
|
|
4953
|
-
* without stripping them — matching the codegen's `.input(schema.loose())`.
|
|
4954
|
-
* Falls back to the schema unchanged when `.loose()` is absent.
|
|
4955
|
-
*/
|
|
4956
|
-
function looseSchema(schema) {
|
|
4957
|
-
const loose = schema.loose;
|
|
4958
|
-
return typeof loose === "function" ? loose.call(schema) : schema;
|
|
4959
|
-
}
|
|
4960
4945
|
function methodsFor(def) {
|
|
4961
4946
|
const effective = (0, _camstack_types.expandCapMethods)(def);
|
|
4962
4947
|
return Object.entries(effective).map(([name, schema]) => {
|
|
@@ -4966,8 +4951,8 @@ function methodsFor(def) {
|
|
|
4966
4951
|
kind: m.kind ?? "query",
|
|
4967
4952
|
auth: m.auth ?? "protected",
|
|
4968
4953
|
schema: m,
|
|
4969
|
-
isVoid: isVoidInput(m.input),
|
|
4970
|
-
isObject: isObjectInput(m.input)
|
|
4954
|
+
isVoid: (0, _camstack_types.isVoidInput)(m.input),
|
|
4955
|
+
isObject: (0, _camstack_types.isObjectInput)(m.input)
|
|
4971
4956
|
};
|
|
4972
4957
|
});
|
|
4973
4958
|
}
|
|
@@ -5020,9 +5005,8 @@ function buildOneCapRouter(def, primitives, services) {
|
|
|
5020
5005
|
const procedures = {};
|
|
5021
5006
|
const getLocal = (ctx, addonId) => services.getLocalProvider(capName, mount.kind, ctx, addonId);
|
|
5022
5007
|
for (const method of methodsFor(def)) {
|
|
5023
|
-
const base = primitives.procedures[
|
|
5008
|
+
const base = primitives.procedures[(0, _camstack_types.procedureAuthKey)(method.auth)];
|
|
5024
5009
|
const inputSchema = method.schema.input;
|
|
5025
|
-
const outputSchema = method.schema.output;
|
|
5026
5010
|
if (method.kind === "subscription") {
|
|
5027
5011
|
procedures[method.name] = base.input(inputSchema).subscription(({ input, ctx }) => {
|
|
5028
5012
|
const provider = requireLocal(capName, () => getLocal(ctx), services);
|
|
@@ -5040,15 +5024,15 @@ function buildOneCapRouter(def, primitives, services) {
|
|
|
5040
5024
|
nodeId: zod.z.string().optional(),
|
|
5041
5025
|
addonId: zod.z.string().optional()
|
|
5042
5026
|
}).optional() : zod.z.object({ nodeId: zod.z.string().optional() }).optional();
|
|
5043
|
-
procedures[method.name] = base.input(voidSelector)
|
|
5027
|
+
procedures[method.name] = base.input(voidSelector)[procKind](({ input, ctx }) => {
|
|
5044
5028
|
const sel = input ?? {};
|
|
5045
5029
|
return (nodeIdData ? requireLocal(capName, () => getLocal(ctx, isCollection ? sel.addonId : void 0), services) : resolveProviderForCall(capName, sel.nodeId, () => getLocal(ctx, isCollection ? sel.addonId : void 0), services))[method.name]?.();
|
|
5046
5030
|
});
|
|
5047
5031
|
continue;
|
|
5048
5032
|
}
|
|
5049
5033
|
if (method.isObject) {
|
|
5050
|
-
const schema = nodeIdData ? inputSchema : looseSchema(inputSchema);
|
|
5051
|
-
procedures[method.name] = base.input(schema)
|
|
5034
|
+
const schema = nodeIdData ? inputSchema : (0, _camstack_types.looseSchema)(inputSchema);
|
|
5035
|
+
procedures[method.name] = base.input(schema)[procKind](({ input, ctx }) => {
|
|
5052
5036
|
if (nodeIdData) return requireLocal(capName, () => getLocal(ctx), services)[method.name]?.(input);
|
|
5053
5037
|
if (isCollection) {
|
|
5054
5038
|
const { rest, picked } = stripKeys(input, ["nodeId", "addonId"]);
|
|
@@ -5061,26 +5045,21 @@ function buildOneCapRouter(def, primitives, services) {
|
|
|
5061
5045
|
});
|
|
5062
5046
|
continue;
|
|
5063
5047
|
}
|
|
5064
|
-
procedures[method.name] = base.input(inputSchema)
|
|
5048
|
+
procedures[method.name] = base.input(inputSchema)[procKind](({ input, ctx }) => {
|
|
5065
5049
|
return requireLocal(capName, () => getLocal(ctx), services)[method.name]?.(input);
|
|
5066
5050
|
});
|
|
5067
5051
|
}
|
|
5068
5052
|
return primitives.router(procedures);
|
|
5069
5053
|
}
|
|
5070
|
-
/** kebab-case → camelCase, matching the codegen router-map key naming. */
|
|
5071
|
-
function kebabToCamel(s) {
|
|
5072
|
-
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
5073
|
-
}
|
|
5074
5054
|
/**
|
|
5075
|
-
* Mount kinds the runtime builder constructs. `
|
|
5076
|
-
* (the server supplies the provider via
|
|
5077
|
-
* `
|
|
5078
|
-
*
|
|
5079
|
-
*
|
|
5080
|
-
* that cap.
|
|
5055
|
+
* Mount kinds the runtime builder constructs. Everything EXCEPT `skip` is
|
|
5056
|
+
* built. `server-provided` IS built (the server supplies the provider via
|
|
5057
|
+
* `getLocalProvider`'s server-factory map); `skip` is never mounted (legacy
|
|
5058
|
+
* provider shapes). `hub-only` is built but its remote leg is `null` — handled
|
|
5059
|
+
* by the server's `remoteProxy` returning null for that cap.
|
|
5081
5060
|
*/
|
|
5082
5061
|
function isBuilderMounted(kind) {
|
|
5083
|
-
return kind !== "
|
|
5062
|
+
return kind !== "skip";
|
|
5084
5063
|
}
|
|
5085
5064
|
/**
|
|
5086
5065
|
* Build the runtime cap-router map: `{ <camelCapName>: <router> }` for
|
|
@@ -5095,7 +5074,7 @@ function buildCapRouters(primitives, services) {
|
|
|
5095
5074
|
if (!isBuilderMounted((0, _camstack_types.resolveCapMount)(def).kind)) continue;
|
|
5096
5075
|
const effective = (0, _camstack_types.expandCapMethods)(def);
|
|
5097
5076
|
if (Object.keys(effective).length === 0) continue;
|
|
5098
|
-
out[kebabToCamel(def.name)] = buildOneCapRouter(def, primitives, services);
|
|
5077
|
+
out[(0, _camstack_types.kebabToCamel)(def.name)] = buildOneCapRouter(def, primitives, services);
|
|
5099
5078
|
}
|
|
5100
5079
|
return out;
|
|
5101
5080
|
}
|
|
@@ -5105,7 +5084,7 @@ function builderMountedCapNames() {
|
|
|
5105
5084
|
for (const def of _camstack_types.ALL_CAPABILITY_DEFINITIONS) {
|
|
5106
5085
|
if (!isBuilderMounted((0, _camstack_types.resolveCapMount)(def).kind)) continue;
|
|
5107
5086
|
if (Object.keys((0, _camstack_types.expandCapMethods)(def)).length === 0) continue;
|
|
5108
|
-
names.push(kebabToCamel(def.name));
|
|
5087
|
+
names.push((0, _camstack_types.kebabToCamel)(def.name));
|
|
5109
5088
|
}
|
|
5110
5089
|
return names;
|
|
5111
5090
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { LocalAuthAddon, a as require_ms, c as __esmMin, d as __toCommonJS, f as
|
|
|
18
18
|
import "./builtins/local-auth/index.mjs";
|
|
19
19
|
import { DeviceManagerAddon } from "./builtins/device-manager/device-manager.addon.mjs";
|
|
20
20
|
import "./builtins/device-manager/index.mjs";
|
|
21
|
-
import { $ as buildUdsNativeCapProxy, A as createParentUnownedCallHandler, B as AGENT_CAP_FWD_SERVICE, C as buildLinkChain, D as HUB_CAP_FWD_ACTION, E as localProviderLink, F as createUdsLoggerWithControl, G as createLocalTransport, H as CapRouteError, I as LocalChildClient, J as SocketChannel, K as UdsLocalTransportClient, L as LocalChildRegistry, M as createUdsEventBus, N as udsChildLogToWorkerEntry, O as HUB_CAP_FWD_SERVICE, P as createUdsLogger, Q as buildNativeCapProxy, R as UDS_NO_ROUTE_PREFIX, S as brokerTransportLink, T as ipcParentLink, U as classifyCapRoute, V as CapRouteResolver, W as callWithServiceDiscovery, X as FrameDecoder, Y as localEndpointPath, Z as encodeFrame, _ as resolveHwAccel, _t as CapabilityUnavailableError, a as getWorkerDeviceRegistry, at as createAddonService, b as getCapUsageRegistry, c as setHubConnected, ct as capActionName, d as getMoleculerEventStats, dt as capServiceName, et as createBrokerDeviceManagerApi, f as registerEventBusService, ft as parseCapAction, g as createKernelHwAccel, gt as CapabilityHandle, h as AddonDepsManager, ht as DeviceRegistry, i as createUdsAddonContext, j as createUdsEventBridge, k as createHubCapForwardService, l as EVENT_TOPIC_PREFIX, lt as capActionSuffix, m as subscribePassthrough, mt as serializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as validateProviderRegistrations, p as setNodeEventInterest, pt as deserializeTypedArrays, q as UdsLocalTransportServer, r as createAddonContext, rt as mountNativeCapService, s as getOrInitReadinessRegistryForClient, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestPythonDeps, u as getBrokerEventBus, ut as capBareAction, v as CapUsageRegistry, vt as installManifestNativeDeps, w as ipcChildLink, x as brokerCallForCap, y as __resetCapUsageRegistryForTests, yt as resolveAddonClass, z as AGENT_CAP_FWD_ACTION } from "./manifest-python-deps-
|
|
21
|
+
import { $ as buildUdsNativeCapProxy, A as createParentUnownedCallHandler, B as AGENT_CAP_FWD_SERVICE, C as buildLinkChain, D as HUB_CAP_FWD_ACTION, E as localProviderLink, F as createUdsLoggerWithControl, G as createLocalTransport, H as CapRouteError, I as LocalChildClient, J as SocketChannel, K as UdsLocalTransportClient, L as LocalChildRegistry, M as createUdsEventBus, N as udsChildLogToWorkerEntry, O as HUB_CAP_FWD_SERVICE, P as createUdsLogger, Q as buildNativeCapProxy, R as UDS_NO_ROUTE_PREFIX, S as brokerTransportLink, T as ipcParentLink, U as classifyCapRoute, V as CapRouteResolver, W as callWithServiceDiscovery, X as FrameDecoder, Y as localEndpointPath, Z as encodeFrame, _ as resolveHwAccel, _t as CapabilityUnavailableError, a as getWorkerDeviceRegistry, at as createAddonService, b as getCapUsageRegistry, c as setHubConnected, ct as capActionName, d as getMoleculerEventStats, dt as capServiceName, et as createBrokerDeviceManagerApi, f as registerEventBusService, ft as parseCapAction, g as createKernelHwAccel, gt as CapabilityHandle, h as AddonDepsManager, ht as DeviceRegistry, i as createUdsAddonContext, j as createUdsEventBridge, k as createHubCapForwardService, l as EVENT_TOPIC_PREFIX, lt as capActionSuffix, m as subscribePassthrough, mt as serializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as validateProviderRegistrations, p as setNodeEventInterest, pt as deserializeTypedArrays, q as UdsLocalTransportServer, r as createAddonContext, rt as mountNativeCapService, s as getOrInitReadinessRegistryForClient, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestPythonDeps, u as getBrokerEventBus, ut as capBareAction, v as CapUsageRegistry, vt as installManifestNativeDeps, w as ipcChildLink, x as brokerCallForCap, y as __resetCapUsageRegistryForTests, yt as resolveAddonClass, z as AGENT_CAP_FWD_ACTION } from "./manifest-python-deps-u-AQBQY6.mjs";
|
|
22
22
|
import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
|
|
23
23
|
import { PYTHON_VERSION, buildBinaryPath, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
|
|
24
24
|
import { request } from "node:http";
|
|
@@ -26,7 +26,7 @@ import * as fs$17 from "node:fs";
|
|
|
26
26
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
27
27
|
import * as path$39 from "node:path";
|
|
28
28
|
import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
29
|
-
import { ALL_CAPABILITY_DEFINITIONS, DATAPLANE_SECRET_HEADER, EventCategory, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, asJsonObject, asNumber, asString, createEvent, emitDownForOwnedCaps, errMsg, expandCapMethods, lifecycleJobSchema, parseJsonObject, parseJsonUnknown, readinessKey, resolveCapMount, scopeKey } from "@camstack/types";
|
|
29
|
+
import { ALL_CAPABILITY_DEFINITIONS, DATAPLANE_SECRET_HEADER, EventCategory, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, asJsonObject, asNumber, asString, createEvent, emitDownForOwnedCaps, errMsg, expandCapMethods, isObjectInput, isVoidInput, kebabToCamel, lifecycleJobSchema, looseSchema, parseJsonObject, parseJsonUnknown, procedureAuthKey, readinessKey, resolveCapMount, scopeKey } from "@camstack/types";
|
|
30
30
|
import { X509Certificate, createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
31
31
|
import { execFile, spawn } from "node:child_process";
|
|
32
32
|
import * as util$10 from "node:util";
|
|
@@ -4909,13 +4909,24 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
4909
4909
|
* this builder's output to that static type at the single documented
|
|
4910
4910
|
* boundary, so the SDK keeps full procedure inference.
|
|
4911
4911
|
*
|
|
4912
|
-
*
|
|
4913
|
-
*
|
|
4914
|
-
*
|
|
4915
|
-
*
|
|
4916
|
-
*
|
|
4917
|
-
*
|
|
4918
|
-
*
|
|
4912
|
+
* The per-method procedure shape is mechanically derived from each
|
|
4913
|
+
* `cap.methods.*` (input zod schema + kind) + the auto-injected
|
|
4914
|
+
* `getStatus` / 3 settings-contribution methods (via `expandCapMethods`).
|
|
4915
|
+
* Provider resolution is derived from the declarative `mount` hint
|
|
4916
|
+
* (`resolveCapMount`) — the same decision the codegen
|
|
4917
|
+
* `generate-cap-mounts.ts` baked in.
|
|
4918
|
+
*
|
|
4919
|
+
* OUTPUT PASS-THROUGH (2026-07-11): unlike the historical codegen, the
|
|
4920
|
+
* builder deliberately does NOT apply `.output(schema)`. Re-validating an
|
|
4921
|
+
* output the hub does not own against the hub's OWN (framework-versioned,
|
|
4922
|
+
* therefore potentially stale) copy of the cap def silently STRIPS fields
|
|
4923
|
+
* a fresher addon added (a self-contained addon bundles its own newer
|
|
4924
|
+
* `@camstack/types`; the hub-closure copy lags). That is exactly how
|
|
4925
|
+
* `models[].group.resolution` vanished from `pipelineExecutor.getSchema`.
|
|
4926
|
+
* Cap dispatch is structural (CLAUDE.md invariant): the owning provider is
|
|
4927
|
+
* typed to return a complete `z.infer<output>` and is the SOLE authority
|
|
4928
|
+
* for the payload shape, so the hub forwards it verbatim. Input validation
|
|
4929
|
+
* is retained (`.input(...)`) — the hub still guards the client boundary.
|
|
4919
4930
|
*
|
|
4920
4931
|
* This module owns NO tRPC primitives of its own: `@camstack/system`
|
|
4921
4932
|
* cannot import the server's `TrpcContext`-bound procedures. The server
|
|
@@ -4923,32 +4934,6 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
4923
4934
|
* `buildCapRouters(t, services)` contract), keeping the kernel free of any
|
|
4924
4935
|
* backend coupling (self-sufficiency invariant).
|
|
4925
4936
|
*/
|
|
4926
|
-
/** Auth level → procedure key. Mirrors codegen's AUTH_PROCEDURE_MAP. */
|
|
4927
|
-
function procedureKeyFor(auth) {
|
|
4928
|
-
if (auth === "public") return "public";
|
|
4929
|
-
if (auth === "admin" || auth === "superAdmin") return "admin";
|
|
4930
|
-
return "protected";
|
|
4931
|
-
}
|
|
4932
|
-
/** Zod schema is `z.void()` — input carries no data. Mirrors codegen's isVoidInput. */
|
|
4933
|
-
function isVoidInput(schema) {
|
|
4934
|
-
const def = schema._def;
|
|
4935
|
-
return schema.constructor?.name === "ZodVoid" || def?.type === "void" || def?.typeName === "ZodVoid";
|
|
4936
|
-
}
|
|
4937
|
-
/** Zod schema is `z.object()` — supports `.loose()` for nodeId passthrough. Mirrors codegen's isObjectInput. */
|
|
4938
|
-
function isObjectInput(schema) {
|
|
4939
|
-
const def = schema._def;
|
|
4940
|
-
return schema.constructor?.name === "ZodObject" || def?.type === "object" || def?.typeName === "ZodObject";
|
|
4941
|
-
}
|
|
4942
|
-
/**
|
|
4943
|
-
* Apply `.loose()` to an object input schema when available (Zod 4) so the
|
|
4944
|
-
* router accepts the out-of-band `nodeId` / `addonId` selector keys
|
|
4945
|
-
* without stripping them — matching the codegen's `.input(schema.loose())`.
|
|
4946
|
-
* Falls back to the schema unchanged when `.loose()` is absent.
|
|
4947
|
-
*/
|
|
4948
|
-
function looseSchema(schema) {
|
|
4949
|
-
const loose = schema.loose;
|
|
4950
|
-
return typeof loose === "function" ? loose.call(schema) : schema;
|
|
4951
|
-
}
|
|
4952
4937
|
function methodsFor(def) {
|
|
4953
4938
|
const effective = expandCapMethods(def);
|
|
4954
4939
|
return Object.entries(effective).map(([name, schema]) => {
|
|
@@ -5012,9 +4997,8 @@ function buildOneCapRouter(def, primitives, services) {
|
|
|
5012
4997
|
const procedures = {};
|
|
5013
4998
|
const getLocal = (ctx, addonId) => services.getLocalProvider(capName, mount.kind, ctx, addonId);
|
|
5014
4999
|
for (const method of methodsFor(def)) {
|
|
5015
|
-
const base = primitives.procedures[
|
|
5000
|
+
const base = primitives.procedures[procedureAuthKey(method.auth)];
|
|
5016
5001
|
const inputSchema = method.schema.input;
|
|
5017
|
-
const outputSchema = method.schema.output;
|
|
5018
5002
|
if (method.kind === "subscription") {
|
|
5019
5003
|
procedures[method.name] = base.input(inputSchema).subscription(({ input, ctx }) => {
|
|
5020
5004
|
const provider = requireLocal(capName, () => getLocal(ctx), services);
|
|
@@ -5032,7 +5016,7 @@ function buildOneCapRouter(def, primitives, services) {
|
|
|
5032
5016
|
nodeId: z.string().optional(),
|
|
5033
5017
|
addonId: z.string().optional()
|
|
5034
5018
|
}).optional() : z.object({ nodeId: z.string().optional() }).optional();
|
|
5035
|
-
procedures[method.name] = base.input(voidSelector)
|
|
5019
|
+
procedures[method.name] = base.input(voidSelector)[procKind](({ input, ctx }) => {
|
|
5036
5020
|
const sel = input ?? {};
|
|
5037
5021
|
return (nodeIdData ? requireLocal(capName, () => getLocal(ctx, isCollection ? sel.addonId : void 0), services) : resolveProviderForCall(capName, sel.nodeId, () => getLocal(ctx, isCollection ? sel.addonId : void 0), services))[method.name]?.();
|
|
5038
5022
|
});
|
|
@@ -5040,7 +5024,7 @@ function buildOneCapRouter(def, primitives, services) {
|
|
|
5040
5024
|
}
|
|
5041
5025
|
if (method.isObject) {
|
|
5042
5026
|
const schema = nodeIdData ? inputSchema : looseSchema(inputSchema);
|
|
5043
|
-
procedures[method.name] = base.input(schema)
|
|
5027
|
+
procedures[method.name] = base.input(schema)[procKind](({ input, ctx }) => {
|
|
5044
5028
|
if (nodeIdData) return requireLocal(capName, () => getLocal(ctx), services)[method.name]?.(input);
|
|
5045
5029
|
if (isCollection) {
|
|
5046
5030
|
const { rest, picked } = stripKeys(input, ["nodeId", "addonId"]);
|
|
@@ -5053,26 +5037,21 @@ function buildOneCapRouter(def, primitives, services) {
|
|
|
5053
5037
|
});
|
|
5054
5038
|
continue;
|
|
5055
5039
|
}
|
|
5056
|
-
procedures[method.name] = base.input(inputSchema)
|
|
5040
|
+
procedures[method.name] = base.input(inputSchema)[procKind](({ input, ctx }) => {
|
|
5057
5041
|
return requireLocal(capName, () => getLocal(ctx), services)[method.name]?.(input);
|
|
5058
5042
|
});
|
|
5059
5043
|
}
|
|
5060
5044
|
return primitives.router(procedures);
|
|
5061
5045
|
}
|
|
5062
|
-
/** kebab-case → camelCase, matching the codegen router-map key naming. */
|
|
5063
|
-
function kebabToCamel(s) {
|
|
5064
|
-
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
5065
|
-
}
|
|
5066
5046
|
/**
|
|
5067
|
-
* Mount kinds the runtime builder constructs. `
|
|
5068
|
-
* (the server supplies the provider via
|
|
5069
|
-
* `
|
|
5070
|
-
*
|
|
5071
|
-
*
|
|
5072
|
-
* that cap.
|
|
5047
|
+
* Mount kinds the runtime builder constructs. Everything EXCEPT `skip` is
|
|
5048
|
+
* built. `server-provided` IS built (the server supplies the provider via
|
|
5049
|
+
* `getLocalProvider`'s server-factory map); `skip` is never mounted (legacy
|
|
5050
|
+
* provider shapes). `hub-only` is built but its remote leg is `null` — handled
|
|
5051
|
+
* by the server's `remoteProxy` returning null for that cap.
|
|
5073
5052
|
*/
|
|
5074
5053
|
function isBuilderMounted(kind) {
|
|
5075
|
-
return kind !== "
|
|
5054
|
+
return kind !== "skip";
|
|
5076
5055
|
}
|
|
5077
5056
|
/**
|
|
5078
5057
|
* Build the runtime cap-router map: `{ <camelCapName>: <router> }` for
|
|
@@ -51,7 +51,7 @@ export interface CapRouterServices {
|
|
|
51
51
|
/**
|
|
52
52
|
* Resolve the LOCAL provider for a cap given the effective mount kind.
|
|
53
53
|
* Returns null on miss (the procedure surfaces PRECONDITION_FAILED).
|
|
54
|
-
* For `
|
|
54
|
+
* For `server-provided` caps this calls the server-supplied service
|
|
55
55
|
* provider factory; for `singleton` / `device-native` / `collection`
|
|
56
56
|
* it goes through the registry.
|
|
57
57
|
*/
|
|
@@ -110,6 +110,30 @@ export interface ParentUnownedCallDeps {
|
|
|
110
110
|
* `CapabilityDefinition`s and the registry) injects this.
|
|
111
111
|
*/
|
|
112
112
|
readonly resolveEmptyCollection?: (capName: string, method: string) => readonly unknown[] | null;
|
|
113
|
+
/**
|
|
114
|
+
* Optional predicate — returns `true` when `capName` is a HUB-CORE `$`-service
|
|
115
|
+
* cap (`system`, `addons`, `nodes`, `capabilities`, `stream-probe`, …): served
|
|
116
|
+
* by the hub's `$core-caps` / `$stream-probe` Moleculer services, NOT by any
|
|
117
|
+
* addon provider. These are the ONLY legitimate consumers of the unpinned
|
|
118
|
+
* `brokerCallForCap` fallback.
|
|
119
|
+
*
|
|
120
|
+
* It generalises the per-cap patches (`isDeviceNativeCap`,
|
|
121
|
+
* `resolveEmptyCollection`) into ONE rule: for any cap the resolver misses
|
|
122
|
+
* (`no-provider`) that is NOT a core `$`-service, the unpinned broker fallback
|
|
123
|
+
* is a STRUCTURALLY DEAD path — the cap's providers are broker-less forked
|
|
124
|
+
* children that expose no Moleculer service, so discovery 30s-deadlines into
|
|
125
|
+
* the misleading `${cap}.${cap}.${method}` error. Instead the handler retries
|
|
126
|
+
* the resolver briefly (the provider registers late in the post-restart boot
|
|
127
|
+
* window) and then throws a PRECISE "not yet registered" error. A NEW
|
|
128
|
+
* multi-node addon cap is covered automatically — no allowlist entry needed —
|
|
129
|
+
* because the discriminant is the already-maintained hub-core cap set, not a
|
|
130
|
+
* per-cap flag.
|
|
131
|
+
*
|
|
132
|
+
* The kernel layer has no cap registry, so the wiring side injects it.
|
|
133
|
+
* Omitted (or returning `false` for a core cap by mistake) ⇒ legacy behaviour
|
|
134
|
+
* exactly: the miss falls straight to the broker fallback with no retry.
|
|
135
|
+
*/
|
|
136
|
+
readonly isCoreServiceCap?: (capName: string) => boolean;
|
|
113
137
|
/** Optional logger for the broker-fallback diagnostic line. */
|
|
114
138
|
readonly logger?: ParentUnownedCallLogger;
|
|
115
139
|
}
|
|
@@ -4865,6 +4865,17 @@ function createUdsEventBridge(deps) {
|
|
|
4865
4865
|
*/
|
|
4866
4866
|
var DEVICE_NATIVE_RETRY_ATTEMPTS = 5;
|
|
4867
4867
|
var DEVICE_NATIVE_RETRY_DELAY_MS = 200;
|
|
4868
|
+
/**
|
|
4869
|
+
* Bounded-retry budget for the general provider-not-yet-registered recovery:
|
|
4870
|
+
* a forked hub-local child that PROVIDES a system/singleton cap (e.g.
|
|
4871
|
+
* `pipeline-orchestrator`) registers only once its (possibly heavy) boot
|
|
4872
|
+
* finishes, so a sibling calling it right after a hub restart briefly resolver-
|
|
4873
|
+
* misses. ~3s (12 × 250ms) covers a normal boot; past it the handler throws a
|
|
4874
|
+
* precise error (the caller — e.g. the ingest-owner gate — fail-opens) instead
|
|
4875
|
+
* of a 30s doubled-name broker deadline. Only engages for a NON-core cap miss.
|
|
4876
|
+
*/
|
|
4877
|
+
var ADDON_CAP_RETRY_ATTEMPTS = 12;
|
|
4878
|
+
var ADDON_CAP_RETRY_DELAY_MS = 250;
|
|
4868
4879
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4869
4880
|
/**
|
|
4870
4881
|
* Build the `onUnownedCall` handler the hub / agent passes into
|
|
@@ -4936,6 +4947,22 @@ function createParentUnownedCallHandler(deps) {
|
|
|
4936
4947
|
...deviceId !== void 0 ? { deviceId } : {},
|
|
4937
4948
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
4938
4949
|
});
|
|
4950
|
+
if (deps.isCoreServiceCap !== void 0 && !deps.isCoreServiceCap(input.capName)) {
|
|
4951
|
+
for (let attempt = 1; attempt <= ADDON_CAP_RETRY_ATTEMPTS; attempt++) {
|
|
4952
|
+
await delay(ADDON_CAP_RETRY_DELAY_MS);
|
|
4953
|
+
const retryResolver = deps.getResolver();
|
|
4954
|
+
if (retryResolver !== null) try {
|
|
4955
|
+
const route = retryResolver.resolveCapRoute(input.capName, {
|
|
4956
|
+
...nodeId !== void 0 ? { nodeId } : {},
|
|
4957
|
+
...deviceId !== void 0 ? { deviceId } : {}
|
|
4958
|
+
});
|
|
4959
|
+
return await retryResolver.dispatch(route, input.method, input.args);
|
|
4960
|
+
} catch (err) {
|
|
4961
|
+
if (!(err instanceof CapRouteError) || err.reason !== "no-provider") throw err;
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
throw new Error(`no provider registered for cap "${input.capName}" (resolver miss after boot-window retry; forked-addon provider not yet registered — ${input.method})`);
|
|
4965
|
+
}
|
|
4939
4966
|
deps.logger?.warn?.("routing child unowned cap call via broker fallback", {
|
|
4940
4967
|
capName: input.capName,
|
|
4941
4968
|
method: input.method
|
|
@@ -4863,6 +4863,17 @@ function createUdsEventBridge(deps) {
|
|
|
4863
4863
|
*/
|
|
4864
4864
|
var DEVICE_NATIVE_RETRY_ATTEMPTS = 5;
|
|
4865
4865
|
var DEVICE_NATIVE_RETRY_DELAY_MS = 200;
|
|
4866
|
+
/**
|
|
4867
|
+
* Bounded-retry budget for the general provider-not-yet-registered recovery:
|
|
4868
|
+
* a forked hub-local child that PROVIDES a system/singleton cap (e.g.
|
|
4869
|
+
* `pipeline-orchestrator`) registers only once its (possibly heavy) boot
|
|
4870
|
+
* finishes, so a sibling calling it right after a hub restart briefly resolver-
|
|
4871
|
+
* misses. ~3s (12 × 250ms) covers a normal boot; past it the handler throws a
|
|
4872
|
+
* precise error (the caller — e.g. the ingest-owner gate — fail-opens) instead
|
|
4873
|
+
* of a 30s doubled-name broker deadline. Only engages for a NON-core cap miss.
|
|
4874
|
+
*/
|
|
4875
|
+
var ADDON_CAP_RETRY_ATTEMPTS = 12;
|
|
4876
|
+
var ADDON_CAP_RETRY_DELAY_MS = 250;
|
|
4866
4877
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4867
4878
|
/**
|
|
4868
4879
|
* Build the `onUnownedCall` handler the hub / agent passes into
|
|
@@ -4934,6 +4945,22 @@ function createParentUnownedCallHandler(deps) {
|
|
|
4934
4945
|
...deviceId !== void 0 ? { deviceId } : {},
|
|
4935
4946
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
4936
4947
|
});
|
|
4948
|
+
if (deps.isCoreServiceCap !== void 0 && !deps.isCoreServiceCap(input.capName)) {
|
|
4949
|
+
for (let attempt = 1; attempt <= ADDON_CAP_RETRY_ATTEMPTS; attempt++) {
|
|
4950
|
+
await delay(ADDON_CAP_RETRY_DELAY_MS);
|
|
4951
|
+
const retryResolver = deps.getResolver();
|
|
4952
|
+
if (retryResolver !== null) try {
|
|
4953
|
+
const route = retryResolver.resolveCapRoute(input.capName, {
|
|
4954
|
+
...nodeId !== void 0 ? { nodeId } : {},
|
|
4955
|
+
...deviceId !== void 0 ? { deviceId } : {}
|
|
4956
|
+
});
|
|
4957
|
+
return await retryResolver.dispatch(route, input.method, input.args);
|
|
4958
|
+
} catch (err) {
|
|
4959
|
+
if (!(err instanceof CapRouteError) || err.reason !== "no-provider") throw err;
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
throw new Error(`no provider registered for cap "${input.capName}" (resolver miss after boot-window retry; forked-addon provider not yet registered — ${input.method})`);
|
|
4963
|
+
}
|
|
4937
4964
|
deps.logger?.warn?.("routing child unowned cap call via broker fallback", {
|
|
4938
4965
|
capName: input.capName,
|
|
4939
4966
|
method: input.method
|
|
@@ -6698,7 +6725,7 @@ function getWorkerDeviceRegistry() {
|
|
|
6698
6725
|
* the property-access boundary and validate structurally.
|
|
6699
6726
|
*/
|
|
6700
6727
|
function buildRemoteNativeProvider(api, cap, options) {
|
|
6701
|
-
const capNameCamel = kebabToCamel(cap.name);
|
|
6728
|
+
const capNameCamel = kebabToCamel$1(cap.name);
|
|
6702
6729
|
const provider = {};
|
|
6703
6730
|
for (const [methodName, methodSchema] of Object.entries(cap.methods)) {
|
|
6704
6731
|
const callKind = methodSchema.kind === "mutation" ? "mutate" : "query";
|
|
@@ -6719,7 +6746,7 @@ function buildRemoteNativeProvider(api, cap, options) {
|
|
|
6719
6746
|
}
|
|
6720
6747
|
return provider;
|
|
6721
6748
|
}
|
|
6722
|
-
function kebabToCamel(s) {
|
|
6749
|
+
function kebabToCamel$1(s) {
|
|
6723
6750
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
6724
6751
|
}
|
|
6725
6752
|
/**
|