@camstack/addon-pipeline 1.2.104 → 1.2.105
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-utils-DRbRzrDy.js → addon-utils-UzUMfVj1.js} +1 -1
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +367 -40
- package/dist/detection-pipeline/index.mjs +365 -38
- package/dist/{dist-D9sltFoR.mjs → dist-BSWr9Qg8.mjs} +66 -5
- package/dist/{dist-gLAVhbvO.js → dist-DVMPCqaj.js} +66 -5
- package/dist/{event-loop-stall-monitor-BUhYY39J.js → event-loop-stall-monitor-BazdZ0g0.js} +1 -1
- package/dist/{event-loop-stall-monitor-BS_lPX6H.mjs → event-loop-stall-monitor-BeQHivuY.mjs} +1 -1
- package/dist/{lazy-sharp-OiAUva0g.js → lazy-sharp-Du1-XaNP.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-DY4RHcTj.js → process-memory-CiNyC-wv.js} +1 -1
- package/dist/{process-memory-Dw7-9jCI.mjs → process-memory-DFL51gCx.mjs} +1 -1
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZaqZ17-x.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-pkzutnza.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DvZWLWk1.mjs +26 -0
- package/dist/stream-broker/{hostInit-aZY81RnZ.mjs → hostInit-B1i1rtBB.mjs} +3 -3
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-Bz7-sMZC.mjs → worker-protocol-Clcy8PvY.mjs} +1 -1
- package/dist/{worker-protocol-D_0q_4Le.js → worker-protocol-pUUt-yhh.js} +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +45 -1
- package/python/test_inference_pool_reconfigure.py +122 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BnhMnnKV.mjs +0 -26
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $ as pipelineExecutorCapability, B as detectionPipelineCapability, Ct as hydrateSchema, Dt as nodePin, E as YAMNET_TO_MACRO, F as defaultDeviceFor$1, Ht as union, Nt as array, Ot as parseJsonUnknown, St as createEvent, U as enumerateInferenceDevices, Ut as EventCategory, Vt as string, W as evaluateZoneRules, X as pickClusterStepModels, at as resolvePoolMemoryPolicy, b as PoolMemoryWatchdog, c as DEFAULT_CLUSTER_STEP_MODELS, f as DEVICE_BACKEND_TO_FORMAT, ft as errMsg, jt as sleep, rt as resolveClusterStepModelId, st as runtimeDevices$1, t as APPLE_SA_TO_MACRO, ut as supportedRuntimes$1, vt as BaseAddon, zt as object } from "../dist-
|
|
3
|
-
import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep, u as landmarkPrecisionVerdict } from "../event-loop-stall-monitor-
|
|
2
|
+
import { $ as pipelineExecutorCapability, B as detectionPipelineCapability, Ct as hydrateSchema, Dt as nodePin, E as YAMNET_TO_MACRO, F as defaultDeviceFor$1, Ht as union, Nt as array, Ot as parseJsonUnknown, St as createEvent, U as enumerateInferenceDevices, Ut as EventCategory, Vt as string, W as evaluateZoneRules, X as pickClusterStepModels, at as resolvePoolMemoryPolicy, b as PoolMemoryWatchdog, c as DEFAULT_CLUSTER_STEP_MODELS, f as DEVICE_BACKEND_TO_FORMAT, ft as errMsg, jt as sleep, rt as resolveClusterStepModelId, st as runtimeDevices$1, t as APPLE_SA_TO_MACRO, ut as supportedRuntimes$1, vt as BaseAddon, zt as object } from "../dist-BSWr9Qg8.mjs";
|
|
3
|
+
import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep, u as landmarkPrecisionVerdict } from "../event-loop-stall-monitor-BeQHivuY.mjs";
|
|
4
4
|
import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
|
|
5
|
-
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-
|
|
5
|
+
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-DFL51gCx.mjs";
|
|
6
6
|
import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../addon-utils-A2S9D7pu.mjs";
|
|
7
7
|
import sharp from "sharp";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
@@ -1069,6 +1069,25 @@ var SharedInferencePool = class {
|
|
|
1069
1069
|
})));
|
|
1070
1070
|
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to unload model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
1071
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Retune a LOADED slot's decode-scoped settings without touching its
|
|
1074
|
+
* inference session — the narrow counterpart of `replaceModel`.
|
|
1075
|
+
*
|
|
1076
|
+
* `replace` is `unload` + `load`: it re-instantiates the session (~1.4s on
|
|
1077
|
+
* coreml, and an OpenVINO recompile) and stalls every live camera routed to
|
|
1078
|
+
* this pool. An operator moving the NMS slider must not cost that, and must
|
|
1079
|
+
* not touch any other slot: the Python side applies the patch to `index`
|
|
1080
|
+
* only, and refuses any key outside its reconfigurable allow-list rather
|
|
1081
|
+
* than accepting a `path` change that would silently not reload.
|
|
1082
|
+
*/
|
|
1083
|
+
async reconfigureModel(index, settings) {
|
|
1084
|
+
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
1085
|
+
cmd: "reconfigure",
|
|
1086
|
+
index,
|
|
1087
|
+
config: { nmsIouThreshold: settings.nmsIouThreshold }
|
|
1088
|
+
})));
|
|
1089
|
+
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to reconfigure model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
1090
|
+
}
|
|
1072
1091
|
async replaceModel(index, config) {
|
|
1073
1092
|
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
1074
1093
|
cmd: "replace",
|
|
@@ -1296,10 +1315,188 @@ function pruneUnrunnableDetailSteps(steps, isDetailStep, isUnrunnable) {
|
|
|
1296
1315
|
};
|
|
1297
1316
|
}
|
|
1298
1317
|
//#endregion
|
|
1318
|
+
//#region src/detection-pipeline/registry/effective-postprocessor.ts
|
|
1319
|
+
function resolveEffectivePostprocessor(definition, modelId) {
|
|
1320
|
+
return resolveEffectivePostprocessorForEntry(definition, modelId ? definition.models.find((m) => m.id === modelId) : void 0);
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Entry-based variant for callers that already hold the `ModelCatalogEntry` —
|
|
1324
|
+
* required for CUSTOM (operator-registered) models, which are NOT in
|
|
1325
|
+
* `definition.models` and would otherwise silently fall back to the step
|
|
1326
|
+
* default decode (a Frigate `yolonas` model decoded as `yolo` produces no
|
|
1327
|
+
* error anywhere, just zero detections).
|
|
1328
|
+
*/
|
|
1329
|
+
function resolveEffectivePostprocessorForEntry(definition, entry) {
|
|
1330
|
+
return entry?.postprocessor ?? definition.postprocessor;
|
|
1331
|
+
}
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region src/detection-pipeline/registry/model-knob-applicability.ts
|
|
1334
|
+
/** Decodes that run NMS in the pipeline (and hence consult `nmsIouThreshold`).
|
|
1335
|
+
* Mirrors the Python postprocessors that read the knob — yolo.py,
|
|
1336
|
+
* yolo_seg.py, rfdetr.py, scrfd.py. */
|
|
1337
|
+
var NMS_CONSUMING_POSTPROCESSORS = new Set([
|
|
1338
|
+
"yolo",
|
|
1339
|
+
"yolo-seg",
|
|
1340
|
+
"rfdetr",
|
|
1341
|
+
"scrfd"
|
|
1342
|
+
]);
|
|
1343
|
+
/**
|
|
1344
|
+
* Does this decode consult `nmsIouThreshold`?
|
|
1345
|
+
*
|
|
1346
|
+
* ONE predicate for the whole system: the UI disables the slider through
|
|
1347
|
+
* {@link annotateConfigSchemaForModels}, and the runtime refuses to forward a
|
|
1348
|
+
* value through `resolvePoolDecodeSettings` — both off this set. Two copies of
|
|
1349
|
+
* the matrix would let the UI promise an effect the pool does not deliver,
|
|
1350
|
+
* which is the failure D214 exists to remove.
|
|
1351
|
+
*/
|
|
1352
|
+
function postprocessorConsumesNms(postprocessor) {
|
|
1353
|
+
return NMS_CONSUMING_POSTPROCESSORS.has(postprocessor);
|
|
1354
|
+
}
|
|
1355
|
+
var MIN_CONFIDENCE_MACRO_PREFIX = "minConfidence";
|
|
1356
|
+
/** `minConfidencePerson` → `person`; bare `minConfidence` → null (generic). */
|
|
1357
|
+
function macroOfMinConfidenceKey(key) {
|
|
1358
|
+
if (!key.startsWith(MIN_CONFIDENCE_MACRO_PREFIX)) return null;
|
|
1359
|
+
const rest = key.slice(13);
|
|
1360
|
+
if (rest.length === 0) return null;
|
|
1361
|
+
return rest.charAt(0).toLowerCase() + rest.slice(1);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* The macro classes a model's OWN labelmap can reach through the step's
|
|
1365
|
+
* classMap. `undefined` ⇒ the model declares no labels (catalog default
|
|
1366
|
+
* labelmap) — every macro of the step is reachable, nothing to annotate.
|
|
1367
|
+
*/
|
|
1368
|
+
function reachableMacros(def, entry) {
|
|
1369
|
+
if (entry.labels.length === 0) return void 0;
|
|
1370
|
+
const map = def.classMap;
|
|
1371
|
+
if (!map) return new Set(entry.labels.map((l) => l.id));
|
|
1372
|
+
if (map.preserveOriginal) return void 0;
|
|
1373
|
+
const macros = /* @__PURE__ */ new Set();
|
|
1374
|
+
for (const label of entry.labels) {
|
|
1375
|
+
const mapped = map.mapping[label.id];
|
|
1376
|
+
if (mapped !== void 0) macros.add(mapped);
|
|
1377
|
+
}
|
|
1378
|
+
return macros;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* The labels a custom model declares that the step's dropping classMap
|
|
1382
|
+
* (`preserveOriginal: false`) removes UPSTREAM of every operator setting.
|
|
1383
|
+
* `null` when the model declares no labels, the step has no classMap, the map
|
|
1384
|
+
* preserves originals, or nothing is dropped. The engine factory logs this at
|
|
1385
|
+
* pool-config time — a dropped class must never be silent (CLAUDE.md logging
|
|
1386
|
+
* rule; the discard trail alone answers "what", not "why zero forever").
|
|
1387
|
+
*/
|
|
1388
|
+
function collectClassMapDropWarning(def, entry) {
|
|
1389
|
+
if (entry.labels.length === 0) return null;
|
|
1390
|
+
const map = def.classMap;
|
|
1391
|
+
if (!map || map.preserveOriginal) return null;
|
|
1392
|
+
const unmapped = entry.labels.filter((l) => map.mapping[l.id] === void 0).map((l) => l.id);
|
|
1393
|
+
if (unmapped.length === 0) return null;
|
|
1394
|
+
return {
|
|
1395
|
+
unmappedLabels: unmapped,
|
|
1396
|
+
mappedCount: entry.labels.length - unmapped.length
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function toApplicability(acc) {
|
|
1400
|
+
const hasInapplicable = Object.keys(acc.inapplicable).length > 0;
|
|
1401
|
+
const hasOptions = Object.keys(acc.optionsByModel).length > 0;
|
|
1402
|
+
if (!hasInapplicable && !hasOptions) return void 0;
|
|
1403
|
+
return {
|
|
1404
|
+
...hasInapplicable ? { inapplicable: acc.inapplicable } : {},
|
|
1405
|
+
...hasOptions ? { optionsByModel: acc.optionsByModel } : {}
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function isAnnotatable(field) {
|
|
1409
|
+
return field.type === "slider" || field.type === "number" || field.type === "multiselect" || field.type === "select" || field.type === "boolean";
|
|
1410
|
+
}
|
|
1411
|
+
function annotateField(field, inputs) {
|
|
1412
|
+
if (!isAnnotatable(field)) return field;
|
|
1413
|
+
const { def, models, customModelIds } = inputs;
|
|
1414
|
+
const acc = {
|
|
1415
|
+
inapplicable: {},
|
|
1416
|
+
optionsByModel: {}
|
|
1417
|
+
};
|
|
1418
|
+
const macroKey = macroOfMinConfidenceKey(field.key);
|
|
1419
|
+
for (const entry of models) {
|
|
1420
|
+
const postprocessor = resolveEffectivePostprocessorForEntry(def, entry);
|
|
1421
|
+
if (field.key === "nmsIouThreshold" && !postprocessorConsumesNms(postprocessor)) {
|
|
1422
|
+
acc.inapplicable[entry.id] = `NMS is fused inside this model's "${postprocessor}" export — the pipeline IoU threshold is not consulted.`;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
if (field.key === "rectify" && postprocessor !== "ctc") {
|
|
1426
|
+
acc.inapplicable[entry.id] = `The "${postprocessor}" reader consumes the raw plate box — its crop policy never deskews, so this switch has no effect.`;
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
if (macroKey !== null) {
|
|
1430
|
+
const macros = reachableMacros(def, entry);
|
|
1431
|
+
if (macros !== void 0 && !macros.has(macroKey)) acc.inapplicable[entry.id] = `This model's labelmap cannot produce "${macroKey}" — the slider filters a class that never arrives.`;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (field.key === "enabledMacroClasses" && field.type === "multiselect") {
|
|
1435
|
+
const macros = reachableMacros(def, entry);
|
|
1436
|
+
if (macros === void 0) continue;
|
|
1437
|
+
const reachable = field.options.filter((o) => macros.has(o.value));
|
|
1438
|
+
if (reachable.length === 0) acc.inapplicable[entry.id] = "None of this model's labels map to a macro class the pipeline tracks — every detection it emits is dropped upstream of this filter.";
|
|
1439
|
+
else if (reachable.length < field.options.length) acc.optionsByModel[entry.id] = reachable.map((o) => ({
|
|
1440
|
+
value: o.value,
|
|
1441
|
+
label: o.label
|
|
1442
|
+
}));
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
if (field.key === "enabledClasses" && field.type === "multiselect" && customModelIds.has(entry.id) && entry.labels.length > 0) {
|
|
1446
|
+
acc.optionsByModel[entry.id] = entry.labels.map((l) => ({
|
|
1447
|
+
value: l.id,
|
|
1448
|
+
label: l.name
|
|
1449
|
+
}));
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
const applicability = toApplicability(acc);
|
|
1454
|
+
if (applicability === void 0) return field;
|
|
1455
|
+
return {
|
|
1456
|
+
...field,
|
|
1457
|
+
modelApplicability: applicability
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Decorate a step's config schema with per-model applicability, computed from
|
|
1462
|
+
* the same model union the picker offers. Fields with nothing to say are
|
|
1463
|
+
* returned untouched (identity), so a static-only step with fully-consuming
|
|
1464
|
+
* models produces a byte-identical schema.
|
|
1465
|
+
*/
|
|
1466
|
+
function annotateConfigSchemaForModels(inputs) {
|
|
1467
|
+
return inputs.configSchema.map((field) => annotateField(field, inputs));
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* A persisted settings blob is external data — it survives schema changes,
|
|
1471
|
+
* hand edits and migrations. Accept only a finite IoU in the open (0,1)
|
|
1472
|
+
* interval; anything else falls back rather than reaching the decode, where a
|
|
1473
|
+
* 0 would suppress every overlapping box and a 1 would suppress none.
|
|
1474
|
+
*/
|
|
1475
|
+
function readIouThreshold(value) {
|
|
1476
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
1477
|
+
if (value <= 0 || value >= 1) return void 0;
|
|
1478
|
+
return value;
|
|
1479
|
+
}
|
|
1480
|
+
/** The effective decode-scoped values for a `(step, model)` pair. */
|
|
1481
|
+
function resolvePoolDecodeSettings(inputs) {
|
|
1482
|
+
const fallback = readIouThreshold(inputs.fallbackNmsIouThreshold) ?? .45;
|
|
1483
|
+
if (!postprocessorConsumesNms(resolveEffectivePostprocessorForEntry(inputs.def, inputs.modelEntry))) return { nmsIouThreshold: fallback };
|
|
1484
|
+
return { nmsIouThreshold: readIouThreshold(inputs.settings?.["nmsIouThreshold"]) ?? fallback };
|
|
1485
|
+
}
|
|
1486
|
+
/** What a built pool config actually carries — the state the slot is IN after
|
|
1487
|
+
* a `load`, so the manager's diff is anchored to the command it sent, never
|
|
1488
|
+
* to a second derivation of the same inputs. */
|
|
1489
|
+
function poolDecodeSettingsOf(config) {
|
|
1490
|
+
return { nmsIouThreshold: config.nmsIouThreshold ?? .45 };
|
|
1491
|
+
}
|
|
1492
|
+
function poolDecodeSettingsEqual(a, b) {
|
|
1493
|
+
return a.nmsIouThreshold === b.nmsIouThreshold;
|
|
1494
|
+
}
|
|
1495
|
+
//#endregion
|
|
1299
1496
|
//#region src/detection-pipeline/engine/pipeline-model-manager.ts
|
|
1300
1497
|
var PipelineModelManager = class {
|
|
1301
1498
|
pool;
|
|
1302
|
-
|
|
1499
|
+
source;
|
|
1303
1500
|
/** stepId → modelId → loaded entry. */
|
|
1304
1501
|
loaded = /* @__PURE__ */ new Map();
|
|
1305
1502
|
/** stepId → currently-active modelId (the runtime choice). */
|
|
@@ -1308,9 +1505,9 @@ var PipelineModelManager = class {
|
|
|
1308
1505
|
lruClock = 0;
|
|
1309
1506
|
log;
|
|
1310
1507
|
maxModelsPerStep;
|
|
1311
|
-
constructor(pool,
|
|
1508
|
+
constructor(pool, source, logger, options) {
|
|
1312
1509
|
this.pool = pool;
|
|
1313
|
-
this.
|
|
1510
|
+
this.source = source;
|
|
1314
1511
|
this.log = logger;
|
|
1315
1512
|
this.maxModelsPerStep = options?.maxModelsPerStep ?? 4;
|
|
1316
1513
|
}
|
|
@@ -1334,7 +1531,7 @@ var PipelineModelManager = class {
|
|
|
1334
1531
|
} });
|
|
1335
1532
|
}
|
|
1336
1533
|
for (const added of diff.added) {
|
|
1337
|
-
await this.ensureLoaded(added.addonId, added.modelId);
|
|
1534
|
+
await this.ensureLoaded(added.addonId, added.modelId, added.settings);
|
|
1338
1535
|
this.activeByStep.set(added.addonId, added.modelId);
|
|
1339
1536
|
this.log.info("Step loaded", { meta: {
|
|
1340
1537
|
step: added.addonId,
|
|
@@ -1343,7 +1540,7 @@ var PipelineModelManager = class {
|
|
|
1343
1540
|
} });
|
|
1344
1541
|
}
|
|
1345
1542
|
for (const { step, oldModelId } of diff.modelChanged) {
|
|
1346
|
-
await this.ensureLoaded(step.addonId, step.modelId);
|
|
1543
|
+
await this.ensureLoaded(step.addonId, step.modelId, step.settings);
|
|
1347
1544
|
this.activeByStep.set(step.addonId, step.modelId);
|
|
1348
1545
|
this.log.info("Active model switched", { meta: {
|
|
1349
1546
|
step: step.addonId,
|
|
@@ -1352,6 +1549,7 @@ var PipelineModelManager = class {
|
|
|
1352
1549
|
} });
|
|
1353
1550
|
await this.enforceLruCap(step.addonId);
|
|
1354
1551
|
}
|
|
1552
|
+
for (const { entry, step } of diff.unchanged) await this.reconcileDecode(entry, step.settings);
|
|
1355
1553
|
}
|
|
1356
1554
|
/**
|
|
1357
1555
|
* Get an `IInferenceEngine` handle. Without `modelId`, returns the
|
|
@@ -1373,6 +1571,22 @@ var PipelineModelManager = class {
|
|
|
1373
1571
|
isLoadedWithModel(stepId, modelId) {
|
|
1374
1572
|
return this.loaded.get(stepId)?.has(modelId) ?? false;
|
|
1375
1573
|
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Does the pool NOT yet reflect this step? True when the (step, model) pair
|
|
1576
|
+
* is absent, and also when it is resident with different decode-scoped
|
|
1577
|
+
* settings — the case `isLoadedWithModel` cannot see.
|
|
1578
|
+
*
|
|
1579
|
+
* This is the per-dispatch skip gate: callers that short-circuit on
|
|
1580
|
+
* "already loaded" were the reason a saved threshold never reached a
|
|
1581
|
+
* running pool. Cheap by contract — `resolveDecode` never touches the fs.
|
|
1582
|
+
*/
|
|
1583
|
+
needsPoolUpdate(step) {
|
|
1584
|
+
const entry = this.loaded.get(step.addonId)?.get(step.modelId);
|
|
1585
|
+
if (!entry) return true;
|
|
1586
|
+
const desired = this.source.resolveDecode(step.addonId, step.modelId, step.settings);
|
|
1587
|
+
if (desired === null) return true;
|
|
1588
|
+
return !poolDecodeSettingsEqual(entry.decode, desired);
|
|
1589
|
+
}
|
|
1376
1590
|
/** ModelId currently designated as active for the step. */
|
|
1377
1591
|
getLoadedModelId(stepId) {
|
|
1378
1592
|
return this.activeByStep.get(stepId);
|
|
@@ -1424,8 +1638,7 @@ var PipelineModelManager = class {
|
|
|
1424
1638
|
*/
|
|
1425
1639
|
async loadAdditional(steps) {
|
|
1426
1640
|
for (const step of steps) {
|
|
1427
|
-
|
|
1428
|
-
await this.ensureLoaded(step.addonId, step.modelId);
|
|
1641
|
+
await this.ensureLoaded(step.addonId, step.modelId, step.settings);
|
|
1429
1642
|
if (!this.activeByStep.has(step.addonId)) this.activeByStep.set(step.addonId, step.modelId);
|
|
1430
1643
|
}
|
|
1431
1644
|
}
|
|
@@ -1434,7 +1647,7 @@ var PipelineModelManager = class {
|
|
|
1434
1647
|
* Honors `maxModelsPerStep` — evicts a non-active LRU variant first
|
|
1435
1648
|
* when the cap would otherwise be exceeded.
|
|
1436
1649
|
*/
|
|
1437
|
-
async ensureLoaded(stepId, modelId) {
|
|
1650
|
+
async ensureLoaded(stepId, modelId, settings) {
|
|
1438
1651
|
let perStep = this.loaded.get(stepId);
|
|
1439
1652
|
if (!perStep) {
|
|
1440
1653
|
perStep = /* @__PURE__ */ new Map();
|
|
@@ -1443,6 +1656,7 @@ var PipelineModelManager = class {
|
|
|
1443
1656
|
const existing = perStep.get(modelId);
|
|
1444
1657
|
if (existing) {
|
|
1445
1658
|
this.touch(existing);
|
|
1659
|
+
await this.reconcileDecode(existing, settings);
|
|
1446
1660
|
return existing;
|
|
1447
1661
|
}
|
|
1448
1662
|
while (perStep.size >= this.maxModelsPerStep) {
|
|
@@ -1457,11 +1671,13 @@ var PipelineModelManager = class {
|
|
|
1457
1671
|
} });
|
|
1458
1672
|
}
|
|
1459
1673
|
const index = this.pool.allocateIndex();
|
|
1460
|
-
const config = this.
|
|
1674
|
+
const config = this.source.buildConfig(stepId, modelId, settings);
|
|
1675
|
+
const decode = poolDecodeSettingsOf(config);
|
|
1461
1676
|
this.log.info("Loading step variant", { meta: {
|
|
1462
1677
|
step: stepId,
|
|
1463
1678
|
modelId,
|
|
1464
|
-
poolIndex: index
|
|
1679
|
+
poolIndex: index,
|
|
1680
|
+
...decode
|
|
1465
1681
|
} });
|
|
1466
1682
|
const { loadMs } = await this.pool.loadModel(index, config);
|
|
1467
1683
|
this.log.info("Step variant loaded", { meta: {
|
|
@@ -1474,11 +1690,71 @@ var PipelineModelManager = class {
|
|
|
1474
1690
|
stepId,
|
|
1475
1691
|
modelId,
|
|
1476
1692
|
poolIndex: index,
|
|
1477
|
-
lruTick: ++this.lruClock
|
|
1693
|
+
lruTick: ++this.lruClock,
|
|
1694
|
+
decode
|
|
1478
1695
|
};
|
|
1479
1696
|
perStep.set(modelId, entry);
|
|
1480
1697
|
return entry;
|
|
1481
1698
|
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Push a retuned decode setting into ONE already-loaded slot.
|
|
1701
|
+
*
|
|
1702
|
+
* Scope is the point: `reconfigure` names a single pool index, so a slider
|
|
1703
|
+
* moved on `object-detection/yolo26n` never disturbs the face detector, the
|
|
1704
|
+
* classifiers, or the same step's warm benchmark variants — and it never
|
|
1705
|
+
* costs a session reload (see `SharedInferencePool.reconfigureModel`).
|
|
1706
|
+
*
|
|
1707
|
+
* A no-op when nothing moved: this runs on the per-dispatch path, so the
|
|
1708
|
+
* common case must send zero commands.
|
|
1709
|
+
*/
|
|
1710
|
+
async reconcileDecode(entry, settings) {
|
|
1711
|
+
const desired = this.source.resolveDecode(entry.stepId, entry.modelId, settings);
|
|
1712
|
+
if (desired === null) return;
|
|
1713
|
+
if (poolDecodeSettingsEqual(entry.decode, desired)) return;
|
|
1714
|
+
const previous = entry.decode;
|
|
1715
|
+
await this.pool.reconfigureModel(entry.poolIndex, desired);
|
|
1716
|
+
entry.decode = desired;
|
|
1717
|
+
this.log.info("Decode settings retuned in place — no model reload", { meta: {
|
|
1718
|
+
step: entry.stepId,
|
|
1719
|
+
modelId: entry.modelId,
|
|
1720
|
+
poolIndex: entry.poolIndex,
|
|
1721
|
+
previousNmsIouThreshold: previous.nmsIouThreshold,
|
|
1722
|
+
...desired
|
|
1723
|
+
} });
|
|
1724
|
+
this.reportContention(entry, previous, desired);
|
|
1725
|
+
}
|
|
1726
|
+
/** Decode values each (step, model) pair has already run, so a value
|
|
1727
|
+
* RETURNING can be told apart from a value being changed. */
|
|
1728
|
+
decodeHistory = /* @__PURE__ */ new Map();
|
|
1729
|
+
contentionReported = /* @__PURE__ */ new Set();
|
|
1730
|
+
/**
|
|
1731
|
+
* A pool slot is NODE-level; the settings driving it arrive per dispatch, so
|
|
1732
|
+
* two cameras with different per-camera overrides for the same (step, model)
|
|
1733
|
+
* pull the same slot back and forth. That is not expressible — one of them is
|
|
1734
|
+
* always decoding with the other's threshold — and it is invisible from the
|
|
1735
|
+
* outside, so it is reported the first time a pair flip-flops BACK to a value
|
|
1736
|
+
* it already ran. One steady retune (the operator moving a slider) never
|
|
1737
|
+
* trips it; alternating writers do, immediately.
|
|
1738
|
+
*/
|
|
1739
|
+
reportContention(entry, previous, desired) {
|
|
1740
|
+
const key = `${entry.stepId}::${entry.modelId}`;
|
|
1741
|
+
let seen = this.decodeHistory.get(key);
|
|
1742
|
+
if (!seen) {
|
|
1743
|
+
seen = new Set([previous.nmsIouThreshold]);
|
|
1744
|
+
this.decodeHistory.set(key, seen);
|
|
1745
|
+
}
|
|
1746
|
+
const returning = seen.has(desired.nmsIouThreshold);
|
|
1747
|
+
seen.add(desired.nmsIouThreshold);
|
|
1748
|
+
if (!returning || this.contentionReported.has(key)) return;
|
|
1749
|
+
this.contentionReported.add(key);
|
|
1750
|
+
this.log.warn("Two dispatches disagree on this step’s decode settings — the pool slot is node-level, so one of them always decodes with the other’s value", { meta: {
|
|
1751
|
+
step: entry.stepId,
|
|
1752
|
+
modelId: entry.modelId,
|
|
1753
|
+
poolIndex: entry.poolIndex,
|
|
1754
|
+
valuesSeen: [...seen].sort((a, b) => a - b),
|
|
1755
|
+
hint: "a per-camera override of a decode-scoped knob cannot be honoured on a shared pool — keep this knob at the node level"
|
|
1756
|
+
} });
|
|
1757
|
+
}
|
|
1482
1758
|
async unloadEntry(entry) {
|
|
1483
1759
|
await this.pool.unloadModel(entry.poolIndex);
|
|
1484
1760
|
const perStep = this.loaded.get(entry.stepId);
|
|
@@ -1544,7 +1820,10 @@ var PipelineModelManager = class {
|
|
|
1544
1820
|
step: desired,
|
|
1545
1821
|
oldModelId: activeModelId
|
|
1546
1822
|
});
|
|
1547
|
-
else unchanged.push(
|
|
1823
|
+
else unchanged.push({
|
|
1824
|
+
entry: activeEntry,
|
|
1825
|
+
step: desired
|
|
1826
|
+
});
|
|
1548
1827
|
}
|
|
1549
1828
|
for (const [stepId, step] of desiredMap) if (!this.activeByStep.has(stepId)) added.push(step);
|
|
1550
1829
|
return {
|
|
@@ -1556,21 +1835,6 @@ var PipelineModelManager = class {
|
|
|
1556
1835
|
}
|
|
1557
1836
|
};
|
|
1558
1837
|
//#endregion
|
|
1559
|
-
//#region src/detection-pipeline/registry/effective-postprocessor.ts
|
|
1560
|
-
function resolveEffectivePostprocessor(definition, modelId) {
|
|
1561
|
-
return resolveEffectivePostprocessorForEntry(definition, modelId ? definition.models.find((m) => m.id === modelId) : void 0);
|
|
1562
|
-
}
|
|
1563
|
-
/**
|
|
1564
|
-
* Entry-based variant for callers that already hold the `ModelCatalogEntry` —
|
|
1565
|
-
* required for CUSTOM (operator-registered) models, which are NOT in
|
|
1566
|
-
* `definition.models` and would otherwise silently fall back to the step
|
|
1567
|
-
* default decode (a Frigate `yolonas` model decoded as `yolo` produces no
|
|
1568
|
-
* error anywhere, just zero detections).
|
|
1569
|
-
*/
|
|
1570
|
-
function resolveEffectivePostprocessorForEntry(definition, entry) {
|
|
1571
|
-
return entry?.postprocessor ?? definition.postprocessor;
|
|
1572
|
-
}
|
|
1573
|
-
//#endregion
|
|
1574
1838
|
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1575
1839
|
/**
|
|
1576
1840
|
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
@@ -1837,6 +2101,14 @@ var EngineFactory = class {
|
|
|
1837
2101
|
return this.poolManager?.isLoadedWithModel(stepId, modelId) ?? false;
|
|
1838
2102
|
}
|
|
1839
2103
|
/**
|
|
2104
|
+
* Does the pool still owe this step something — a load, or a retune of its
|
|
2105
|
+
* decode-scoped settings? The skip gate every dispatch path must use instead
|
|
2106
|
+
* of `isLoadedWithModel`, which cannot see a changed threshold (D214).
|
|
2107
|
+
*/
|
|
2108
|
+
needsPoolUpdate(step) {
|
|
2109
|
+
return this.poolManager?.needsPoolUpdate(step) ?? true;
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
1840
2112
|
* List every loaded (stepId, modelId) variant — the pool surfaces each
|
|
1841
2113
|
* warm slot independently (including bench overrides).
|
|
1842
2114
|
*/
|
|
@@ -1972,19 +2244,57 @@ var EngineFactory = class {
|
|
|
1972
2244
|
numWorkers,
|
|
1973
2245
|
...this.opts.engine.device ? { device: this.opts.engine.device } : {}
|
|
1974
2246
|
});
|
|
1975
|
-
this.poolManager = new PipelineModelManager(this.pool,
|
|
2247
|
+
this.poolManager = new PipelineModelManager(this.pool, {
|
|
2248
|
+
buildConfig: (stepId, modelId, settings) => this.buildPoolModelConfig(stepId, modelId, poolRuntime, settings),
|
|
2249
|
+
resolveDecode: (stepId, modelId, settings) => this.resolveDecode(stepId, modelId, settings)
|
|
2250
|
+
}, this.log.child("model-mgr"));
|
|
1976
2251
|
await this.pool.initialize([]);
|
|
1977
2252
|
await this.poolManager.applyConfig(steps);
|
|
1978
2253
|
}
|
|
1979
|
-
|
|
2254
|
+
/** (step, model) pairs whose classMap-drop warning already logged — the
|
|
2255
|
+
* pool reloads models on every applyConfig; the fact doesn't change. */
|
|
2256
|
+
classMapDropLogged = /* @__PURE__ */ new Set();
|
|
2257
|
+
/**
|
|
2258
|
+
* The fs-free half of {@link PoolModelSource} — the effective decode-scoped
|
|
2259
|
+
* settings for a `(step, model, settings)` triple. Runs on the per-dispatch
|
|
2260
|
+
* path, so it must resolve the catalog entry and nothing more; the label
|
|
2261
|
+
* file, the format build and the model path are load-time concerns.
|
|
2262
|
+
*
|
|
2263
|
+
* `null` when the pair resolves to no model at all — the caller routes that
|
|
2264
|
+
* to `buildConfig`, which throws the message naming the step and the model.
|
|
2265
|
+
*/
|
|
2266
|
+
resolveDecode(stepId, modelId, settings) {
|
|
2267
|
+
const def = getStepDefinition(stepId);
|
|
2268
|
+
const modelEntry = def.models.find((m) => m.id === modelId) ?? this.opts.resolveCustomModel?.(stepId, modelId);
|
|
2269
|
+
if (!modelEntry) return null;
|
|
2270
|
+
return resolvePoolDecodeSettings({
|
|
2271
|
+
def,
|
|
2272
|
+
modelEntry,
|
|
2273
|
+
settings,
|
|
2274
|
+
fallbackNmsIouThreshold: this.opts.nmsIouThreshold
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
buildPoolModelConfig(stepId, modelId, poolRuntime, stepSettings) {
|
|
1980
2278
|
return buildPoolModelConfigForStep({
|
|
1981
2279
|
stepId,
|
|
1982
2280
|
modelId,
|
|
1983
2281
|
poolRuntime,
|
|
1984
2282
|
modelsDir: this.opts.modelsDir,
|
|
1985
2283
|
nmsIouThreshold: this.opts.nmsIouThreshold,
|
|
2284
|
+
stepSettings,
|
|
1986
2285
|
device: this.opts.engine.device,
|
|
1987
|
-
resolveCustomModel: this.opts.resolveCustomModel
|
|
2286
|
+
resolveCustomModel: this.opts.resolveCustomModel,
|
|
2287
|
+
onClassMapDrop: ({ stepId: sid, modelId: mid, unmappedLabels }) => {
|
|
2288
|
+
const key = `${sid}::${mid}`;
|
|
2289
|
+
if (this.classMapDropLogged.has(key)) return;
|
|
2290
|
+
this.classMapDropLogged.add(key);
|
|
2291
|
+
this.log.warn("Model labels dropped by step classMap — unreachable by any setting", { meta: {
|
|
2292
|
+
step: sid,
|
|
2293
|
+
modelId: mid,
|
|
2294
|
+
unmappedLabels: [...unmappedLabels],
|
|
2295
|
+
hint: "these classes are removed before enabledMacroClasses / minConfidence* run; only labels in the step classMap are tracked"
|
|
2296
|
+
} });
|
|
2297
|
+
}
|
|
1988
2298
|
});
|
|
1989
2299
|
}
|
|
1990
2300
|
};
|
|
@@ -2003,6 +2313,12 @@ function buildPoolModelConfigForStep(inputs) {
|
|
|
2003
2313
|
if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${stepId}" catalog (static or custom registry)`);
|
|
2004
2314
|
const format = RUNTIME_TO_FORMAT[poolRuntime];
|
|
2005
2315
|
if (!format) throw new Error(`No format mapping for pool runtime "${poolRuntime}"`);
|
|
2316
|
+
const dropWarning = collectClassMapDropWarning(def, modelEntry);
|
|
2317
|
+
if (dropWarning !== null) inputs.onClassMapDrop?.({
|
|
2318
|
+
stepId,
|
|
2319
|
+
modelId,
|
|
2320
|
+
unmappedLabels: dropWarning.unmappedLabels
|
|
2321
|
+
});
|
|
2006
2322
|
const formatEntry = modelEntry.formats[format];
|
|
2007
2323
|
if (!formatEntry) throw new Error(`Model "${modelId}" has no ${format} format. Available: ${Object.keys(modelEntry.formats).join(", ")}`);
|
|
2008
2324
|
const urlParts = formatEntry.url.split("/");
|
|
@@ -2024,7 +2340,12 @@ function buildPoolModelConfigForStep(inputs) {
|
|
|
2024
2340
|
outputProbabilities: modelEntry.outputProbabilities,
|
|
2025
2341
|
postprocessor: resolveEffectivePostprocessorForEntry(def, modelEntry),
|
|
2026
2342
|
confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
|
|
2027
|
-
nmsIouThreshold:
|
|
2343
|
+
nmsIouThreshold: resolvePoolDecodeSettings({
|
|
2344
|
+
def,
|
|
2345
|
+
modelEntry,
|
|
2346
|
+
settings: inputs.stepSettings,
|
|
2347
|
+
fallbackNmsIouThreshold: inputs.nmsIouThreshold
|
|
2348
|
+
}).nmsIouThreshold,
|
|
2028
2349
|
labels,
|
|
2029
2350
|
charset: def.charset,
|
|
2030
2351
|
plateRegion: def.plateRegion,
|
|
@@ -6944,7 +7265,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6944
7265
|
this.executor = executor;
|
|
6945
7266
|
const dispatchEngine = dispatchDeviceKey ? resolveDeviceEngine(dispatchDeviceKey) : runEngine;
|
|
6946
7267
|
const dispatchFactory = dispatchDeviceKey ? await this.resolveDeviceFactory(dispatchDeviceKey) : this.engineFactory;
|
|
6947
|
-
const needed = enabledSteps.filter((s) =>
|
|
7268
|
+
const needed = enabledSteps.filter((s) => dispatchFactory.needsPoolUpdate(s));
|
|
6948
7269
|
if (needed.length > 0) {
|
|
6949
7270
|
this.log.info("Benchmark: models to load", { meta: {
|
|
6950
7271
|
count: needed.length,
|
|
@@ -7385,7 +7706,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
7385
7706
|
const needed = [];
|
|
7386
7707
|
for (const step of allEnabled) {
|
|
7387
7708
|
if (!step.enabled) continue;
|
|
7388
|
-
if (factory
|
|
7709
|
+
if (factory !== null && !factory.needsPoolUpdate(step)) continue;
|
|
7389
7710
|
needed.push(step);
|
|
7390
7711
|
}
|
|
7391
7712
|
if (needed.length === 0) return;
|
|
@@ -7544,7 +7865,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
7544
7865
|
try {
|
|
7545
7866
|
const factory = dispatchDeviceKey ? await this.resolveDeviceFactory(dispatchDeviceKey) : this.engineFactory;
|
|
7546
7867
|
if (!factory) throw new Error("runPipelineBatch: factory not initialised");
|
|
7547
|
-
if (enabledSteps.filter((s) =>
|
|
7868
|
+
if (enabledSteps.filter((s) => factory.needsPoolUpdate(s)).length > 0) await this.ensureModelsForSteps(benchmarkSteps, factory, resolveFormat);
|
|
7548
7869
|
const canFastPath = singleRoot && allRaw && uniformDims && factory.supportsBatch();
|
|
7549
7870
|
this.log.info("runPipelineBatch path decision", { meta: {
|
|
7550
7871
|
phase: "batch",
|
|
@@ -8253,7 +8574,13 @@ function buildSchemaSlots(formats, modelsDir, customByStep) {
|
|
|
8253
8574
|
if (availableModels.length === 0) continue;
|
|
8254
8575
|
const slot = step.slot;
|
|
8255
8576
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|
|
8256
|
-
const
|
|
8577
|
+
const customIds = new Set((customByStep?.get(step.id) ?? []).map((m) => m.id));
|
|
8578
|
+
const configSchema = annotateConfigSchemaForModels({
|
|
8579
|
+
def: step,
|
|
8580
|
+
configSchema: pipelineStep.getConfigSchema(),
|
|
8581
|
+
models: availableModels,
|
|
8582
|
+
customModelIds: customIds
|
|
8583
|
+
});
|
|
8257
8584
|
slotMap.get(slot).push({
|
|
8258
8585
|
id: step.id,
|
|
8259
8586
|
name: step.name,
|
|
@@ -20764,6 +20764,12 @@ var CameraStatusSchema = object({
|
|
|
20764
20764
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
20765
20765
|
fetchedAt: number()
|
|
20766
20766
|
});
|
|
20767
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
20768
|
+
"disabled",
|
|
20769
|
+
"unavailable",
|
|
20770
|
+
"cannot-host-camera-root",
|
|
20771
|
+
"accelerator-preferred"
|
|
20772
|
+
]);
|
|
20767
20773
|
var NodeInferenceDeviceSchema = object({
|
|
20768
20774
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
20769
20775
|
key: string(),
|
|
@@ -20794,7 +20800,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
20794
20800
|
* available per format; this is the stored selection that becomes the
|
|
20795
20801
|
* default for EVERY camera landing on this accelerator.
|
|
20796
20802
|
*/
|
|
20797
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
20803
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
20804
|
+
/**
|
|
20805
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
20806
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
20807
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
20808
|
+
* never disagree with the election — deriving it in the UI from
|
|
20809
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
20810
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
20811
|
+
* "an accelerator is serving" predicate).
|
|
20812
|
+
*/
|
|
20813
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
20798
20814
|
});
|
|
20799
20815
|
var NodeInferenceDevicesSchema = object({
|
|
20800
20816
|
nodeId: string(),
|
|
@@ -24714,7 +24730,12 @@ var ListResultSchema = object({
|
|
|
24714
24730
|
probedAt: number()
|
|
24715
24731
|
});
|
|
24716
24732
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
24717
|
-
|
|
24733
|
+
/**
|
|
24734
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
24735
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
24736
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
24737
|
+
*/
|
|
24738
|
+
var ConnectionEndpointSchema = object({
|
|
24718
24739
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
24719
24740
|
label: string(),
|
|
24720
24741
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -24757,7 +24778,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
24757
24778
|
* ordering between polls.
|
|
24758
24779
|
*/
|
|
24759
24780
|
priority: number()
|
|
24760
|
-
})
|
|
24781
|
+
});
|
|
24782
|
+
/**
|
|
24783
|
+
* Where the advertised local port came from. Ordered most → least
|
|
24784
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
24785
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
24786
|
+
*/
|
|
24787
|
+
var LocalPortSourceEnum = _enum([
|
|
24788
|
+
"server-config",
|
|
24789
|
+
"server-env",
|
|
24790
|
+
"caller-hint",
|
|
24791
|
+
"default"
|
|
24792
|
+
]);
|
|
24793
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
24794
|
+
var AdvertisedLocalPortSchema = object({
|
|
24795
|
+
port: number().int().min(1).max(65535),
|
|
24796
|
+
source: LocalPortSourceEnum
|
|
24797
|
+
});
|
|
24798
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
24799
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
24800
|
+
/**
|
|
24801
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
24802
|
+
* came from.
|
|
24803
|
+
*
|
|
24804
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
24805
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
24806
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
24807
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
24808
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
24809
|
+
* `caller-hint` is the hub's own socket.
|
|
24810
|
+
*
|
|
24811
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
24812
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
24813
|
+
* `caller-hint`.
|
|
24814
|
+
*/
|
|
24815
|
+
localPort: AdvertisedLocalPortSchema
|
|
24816
|
+
});
|
|
24761
24817
|
/**
|
|
24762
24818
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
24763
24819
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -24778,8 +24834,13 @@ var AllowedAddressesSchema = object({
|
|
|
24778
24834
|
*/
|
|
24779
24835
|
addresses: array(string()).readonly() });
|
|
24780
24836
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
24781
|
-
/**
|
|
24782
|
-
|
|
24837
|
+
/**
|
|
24838
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
24839
|
+
* written against the echoing contract keep working; the hub uses it
|
|
24840
|
+
* only when it cannot read its own port, and says so via
|
|
24841
|
+
* `localPort.source === 'caller-hint'`.
|
|
24842
|
+
*/
|
|
24843
|
+
port: number().int().min(1).max(65535).optional(),
|
|
24783
24844
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24784
24845
|
* candidate. Default `true`. */
|
|
24785
24846
|
includeLoopback: boolean().optional(),
|