@camstack/core 0.1.37 → 0.1.39
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/auth/auth-manager.d.ts +12 -1
- package/dist/auth/auth-manager.d.ts.map +1 -1
- package/dist/auth/scope-matcher.d.ts +8 -0
- package/dist/auth/scope-matcher.d.ts.map +1 -0
- package/dist/auth/totp-manager.d.ts +0 -1
- package/dist/auth/totp-manager.d.ts.map +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +15 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts.map +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +27 -6
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +27 -6
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -1
- package/dist/builtins/device-manager/device-config-contribution.d.ts +33 -0
- package/dist/builtins/device-manager/device-config-contribution.d.ts.map +1 -0
- package/dist/builtins/device-manager/device-manager.addon.d.ts +52 -17
- package/dist/builtins/device-manager/device-manager.addon.d.ts.map +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +285 -161
- package/dist/builtins/device-manager/device-manager.addon.js.map +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +286 -162
- package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -1
- package/dist/builtins/local-auth/auth-schema.d.ts +1 -0
- package/dist/builtins/local-auth/auth-schema.d.ts.map +1 -1
- package/dist/builtins/local-auth/local-auth.addon.d.ts +1 -0
- package/dist/builtins/local-auth/local-auth.addon.d.ts.map +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +354 -3
- package/dist/builtins/local-auth/local-auth.addon.js.map +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +355 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -1
- package/dist/builtins/local-auth/oauth-grants.d.ts +46 -0
- package/dist/builtins/local-auth/oauth-grants.d.ts.map +1 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts +51 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts.map +1 -0
- package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts +97 -0
- package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts.map +1 -0
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +24 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts.map +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +136 -56
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js.map +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +137 -57
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs.map +1 -1
- package/dist/builtins/snapshot/index.js +1 -3
- package/dist/builtins/snapshot/index.js.map +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -3
- package/dist/builtins/snapshot/index.mjs.map +1 -1
- package/dist/builtins/snapshot/snapshot.addon.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +428 -234
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +428 -235
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -37
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts +0 -8
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts.map +0 -1
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js +0 -75
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js.map +0 -1
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs +0 -69
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs.map +0 -1
- package/dist/builtins/auth-orchestrator/index.d.ts +0 -2
- package/dist/builtins/auth-orchestrator/index.d.ts.map +0 -1
- package/dist/builtins/auth-orchestrator/index.js +0 -7
- package/dist/builtins/auth-orchestrator/index.mjs +0 -2
- package/dist/builtins/mesh-orchestrator/index.d.ts +0 -2
- package/dist/builtins/mesh-orchestrator/index.d.ts.map +0 -1
- package/dist/builtins/mesh-orchestrator/index.js +0 -7
- package/dist/builtins/mesh-orchestrator/index.mjs +0 -2
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts +0 -9
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts.map +0 -1
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js +0 -113
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js.map +0 -1
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs +0 -107
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs.map +0 -1
- package/dist/builtins/turn-orchestrator/index.d.ts +0 -2
- package/dist/builtins/turn-orchestrator/index.d.ts.map +0 -1
- package/dist/builtins/turn-orchestrator/index.js +0 -7
- package/dist/builtins/turn-orchestrator/index.mjs +0 -2
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts +0 -34
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts.map +0 -1
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js +0 -126
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js.map +0 -1
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs +0 -120
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { BaseAddon, CAP_NAMES_WITH_STATUS, DeviceFeature, DeviceType, EventCategory, WELL_KNOWN_TAB_MAP, deviceManagerCapability, deviceStateCapability, errMsg } from "@camstack/types";
|
|
2
|
+
import { BaseAddon, CAP_NAMES_WITH_STATUS, DeviceFeature, DeviceType, EventCategory, STREAM_PROFILE_META, WELL_KNOWN_TAB_MAP, buildStreamParamsConfigSchema, deviceManagerCapability, deviceStateCapability, errMsg, isDeviceConfigCap, parseStreamParamsFormPatch } from "@camstack/types";
|
|
3
3
|
//#region src/builtins/device-manager/device-event-propagator.ts
|
|
4
4
|
/**
|
|
5
5
|
* Walks the parent chain for every device-sourced event and re-emits a
|
|
@@ -107,6 +107,34 @@ var DeviceEventPropagator = class {
|
|
|
107
107
|
return chain;
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* Build the device-detail form section for a `derived-form` device-config
|
|
112
|
+
* cap. Returns null when the camera exposes no configurable property.
|
|
113
|
+
*/
|
|
114
|
+
function deriveFormContribution(builderId, options, status) {
|
|
115
|
+
if (builderId !== "stream-params") throw new Error(`device-config: unknown derived-form builderId "${builderId}"`);
|
|
116
|
+
const schema = buildStreamParamsConfigSchema(options, status ?? null);
|
|
117
|
+
if (!schema) return null;
|
|
118
|
+
return { sections: schema.sections.map((s) => ({
|
|
119
|
+
id: s.id,
|
|
120
|
+
title: s.title,
|
|
121
|
+
...s.tab !== void 0 ? { tab: s.tab } : {},
|
|
122
|
+
...s.order !== void 0 ? { order: s.order } : {},
|
|
123
|
+
...s.description !== void 0 ? { description: s.description } : {},
|
|
124
|
+
...s.columns !== void 0 ? { columns: s.columns } : {},
|
|
125
|
+
fields: [...s.fields]
|
|
126
|
+
})) };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Route a flat form patch back through the cap's per-profile setter.
|
|
130
|
+
*/
|
|
131
|
+
async function applyDerivedFormPatch(builderId, patch, setProfile) {
|
|
132
|
+
if (builderId !== "stream-params") throw new Error(`device-config: unknown derived-form builderId "${builderId}"`);
|
|
133
|
+
for (const meta of STREAM_PROFILE_META) {
|
|
134
|
+
const profilePatch = parseStreamParamsFormPatch(patch, meta.prefix);
|
|
135
|
+
if (profilePatch) await setProfile(meta.profile, profilePatch);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
110
138
|
//#endregion
|
|
111
139
|
//#region src/builtins/device-manager/device-manager.addon.ts
|
|
112
140
|
/**
|
|
@@ -133,6 +161,28 @@ var DeviceEventPropagator = class {
|
|
|
133
161
|
* - `device-persistence` capability (absorbed here)
|
|
134
162
|
* - live operations previously served by `device-management.router.ts`
|
|
135
163
|
*/
|
|
164
|
+
/**
|
|
165
|
+
* Return true when `err` is a transient Moleculer error that is worth
|
|
166
|
+
* retrying — specifically any `MoleculerRetryableError` subclass
|
|
167
|
+
* (ServiceNotAvailableError, ServiceNotFoundError, BrokerDisconnectedError,
|
|
168
|
+
* RequestTimeoutError, …). Moleculer sets `retryable: true` on all of them.
|
|
169
|
+
*
|
|
170
|
+
* Falls back to a message-substring check for serialised errors that arrive
|
|
171
|
+
* across the Moleculer transport as plain objects rather than real instances.
|
|
172
|
+
*/
|
|
173
|
+
function isTransientMoleculerError(err) {
|
|
174
|
+
if (err !== null && typeof err === "object") {
|
|
175
|
+
const e = err;
|
|
176
|
+
if (e["retryable"] === true) return true;
|
|
177
|
+
const code = typeof e["code"] === "string" ? e["code"] : "";
|
|
178
|
+
if (code === "SERVICE_NOT_FOUND" || code === "SERVICE_NOT_AVAILABLE" || code === "REQUEST_TIMEOUT" || code === "BAD_GATEWAY") return true;
|
|
179
|
+
}
|
|
180
|
+
if (err instanceof Error) {
|
|
181
|
+
const msg = err.message;
|
|
182
|
+
if (msg.includes("is not available") || msg.includes("is not found") || msg.includes("transporter has disconnected") || msg.includes("Request timed out")) return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
136
186
|
function shallowEqual(a, b) {
|
|
137
187
|
const ak = Object.keys(a);
|
|
138
188
|
const bk = Object.keys(b);
|
|
@@ -355,11 +405,21 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
355
405
|
static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
|
|
356
406
|
/**
|
|
357
407
|
* Cross-process native-provider cache: deviceId (numeric) → capName → { addonId, nodeId }.
|
|
358
|
-
* Kept in sync with `
|
|
359
|
-
* workers
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
408
|
+
* Kept in sync with `DeviceBindingsChanged` push events emitted by forked
|
|
409
|
+
* workers on `ctx.registerNativeCap` / device removal. Union'd into
|
|
410
|
+
* `getBindings` so hub-side consumers see every native cap regardless of
|
|
411
|
+
* which process owns the IDevice.
|
|
412
|
+
*
|
|
413
|
+
* No persistence — entries re-populate when the worker re-handshakes or
|
|
414
|
+
* re-emits its native-cap registrations after restart. Entries that were
|
|
415
|
+
* lost in the Moleculer transport handshake window are recovered lazily:
|
|
416
|
+
* `resolveNativeCapOwnerSync` and `getBindings` fall through to
|
|
417
|
+
* `ctx.kernel.listClusterNativeCaps()` (the handshake-fed
|
|
418
|
+
* `HubNodeRegistry`) when the push-based cache misses.
|
|
419
|
+
*
|
|
420
|
+
* The previous pull-based recovery (`syncWorkerNativeCaps`, driven by
|
|
421
|
+
* `$node.connected` + `addon.restarted`) has been removed in Task 13 —
|
|
422
|
+
* the D3 re-handshake after device restore is the reliable replacement.
|
|
363
423
|
*/
|
|
364
424
|
remoteNativeCaps = /* @__PURE__ */ new Map();
|
|
365
425
|
/** Wait for a device-provider by addonId, returning null on timeout. */
|
|
@@ -383,79 +443,34 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
383
443
|
return "hub";
|
|
384
444
|
}
|
|
385
445
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* `
|
|
446
|
+
* Resolve a remote native cap entry for a given `(capName, deviceId)` by
|
|
447
|
+
* consulting the handshake-fed `HubNodeRegistry` via
|
|
448
|
+
* `ctx.kernel.listClusterNativeCaps()`. Called when the push-based
|
|
449
|
+
* `remoteNativeCaps` cache misses — covers the Moleculer transport
|
|
450
|
+
* handshake window where `DeviceBindingsChanged` events were lost but the
|
|
451
|
+
* D3 re-handshake (post device restore) has already populated the registry.
|
|
389
452
|
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* connected node, calls each service's `$listDeviceIds` action to
|
|
393
|
-
* fetch the deviceIds the worker has registered that cap on, then
|
|
394
|
-
* writes entries into `remoteNativeCaps`.
|
|
395
|
-
*
|
|
396
|
-
* Best-effort: per-service failures are logged and swallowed so a
|
|
397
|
-
* single bad service doesn't abort the whole rebuild.
|
|
453
|
+
* Returns `null` when the entry is genuinely not present in the cluster
|
|
454
|
+
* view (cap not registered on any worker for that device).
|
|
398
455
|
*/
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
if (!
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const NATIVE_INFIX = ".native-provider.";
|
|
408
|
-
const matched = services.filter((s) => s.nodeID === connectedNodeId && s.name.includes(NATIVE_INFIX));
|
|
409
|
-
if (matched.length === 0) return;
|
|
410
|
-
for (const svc of matched) {
|
|
411
|
-
const idx = svc.name.indexOf(NATIVE_INFIX);
|
|
412
|
-
if (idx <= 0) continue;
|
|
413
|
-
const addonId = svc.name.slice(0, idx);
|
|
414
|
-
const capName = svc.name.slice(idx + 17);
|
|
415
|
-
if (!addonId || !capName) continue;
|
|
416
|
-
try {
|
|
417
|
-
const action = `${svc.name}.$listDeviceIds`;
|
|
418
|
-
const deviceIds = await broker.call?.(action, {}, { nodeID: connectedNodeId });
|
|
419
|
-
if (!deviceIds || deviceIds.length === 0) continue;
|
|
420
|
-
for (const deviceId of deviceIds) {
|
|
421
|
-
if (this.capabilityRegistry?.getNativeAddonId(capName, deviceId)) continue;
|
|
422
|
-
let perDevice = this.remoteNativeCaps.get(deviceId);
|
|
423
|
-
if (!perDevice) {
|
|
424
|
-
perDevice = /* @__PURE__ */ new Map();
|
|
425
|
-
this.remoteNativeCaps.set(deviceId, perDevice);
|
|
426
|
-
}
|
|
427
|
-
perDevice.set(capName, {
|
|
428
|
-
addonId,
|
|
429
|
-
nodeId: connectedNodeId
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
this.ctx.logger.debug("worker native-cap discovered", { meta: {
|
|
433
|
-
nodeId: connectedNodeId,
|
|
434
|
-
addonId,
|
|
435
|
-
capName,
|
|
436
|
-
deviceIds
|
|
437
|
-
} });
|
|
438
|
-
} catch (err) {
|
|
439
|
-
this.ctx.logger.debug("worker native-cap $listDeviceIds failed", { meta: {
|
|
440
|
-
service: svc.name,
|
|
441
|
-
nodeId: connectedNodeId,
|
|
442
|
-
error: errMsg(err)
|
|
443
|
-
} });
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
this.ctx.logger.info("worker native-cap discovery completed", { meta: {
|
|
447
|
-
nodeId: connectedNodeId,
|
|
448
|
-
services: matched.length
|
|
449
|
-
} });
|
|
456
|
+
resolveRemoteNativeCapFromRegistry(capName, deviceId) {
|
|
457
|
+
const clusterCaps = this.ctx.kernel.listClusterNativeCaps?.();
|
|
458
|
+
if (!clusterCaps) return null;
|
|
459
|
+
for (const entry of clusterCaps) if (entry.capName === capName && entry.deviceId === deviceId && entry.addonId) return {
|
|
460
|
+
addonId: entry.addonId,
|
|
461
|
+
nodeId: entry.nodeId
|
|
462
|
+
};
|
|
463
|
+
return null;
|
|
450
464
|
}
|
|
451
465
|
async getBindings(input) {
|
|
452
466
|
const storeKey = String(input.deviceId);
|
|
453
467
|
const perDevice = (await this.readBindingsStore()).deviceBindings[storeKey] ?? {};
|
|
454
468
|
const entries = [];
|
|
455
469
|
const seenCaps = /* @__PURE__ */ new Set();
|
|
470
|
+
const resolveRemote = (capName) => this.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? this.resolveRemoteNativeCapFromRegistry(capName, input.deviceId);
|
|
456
471
|
for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
|
|
457
472
|
const hubLocalNative = this.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
|
|
458
|
-
const remoteNative =
|
|
473
|
+
const remoteNative = resolveRemote(capName);
|
|
459
474
|
const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
|
|
460
475
|
const nativeNodeId = hubLocalNative ? this.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? this.ctx.kernel.localNodeId ?? "hub";
|
|
461
476
|
if (wrapperAddonId === null && !nativeAddonId) {
|
|
@@ -471,13 +486,12 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
471
486
|
});
|
|
472
487
|
seenCaps.add(capName);
|
|
473
488
|
}
|
|
474
|
-
const remote = this.remoteNativeCaps.get(input.deviceId);
|
|
475
489
|
if (this.capabilityRegistry) for (const capName of this.capabilityRegistry.getCapsWithDefaultWrapper()) {
|
|
476
490
|
if (seenCaps.has(capName)) continue;
|
|
477
491
|
const defaultWrapperAddonId = this.capabilityRegistry.getDefaultWrapperForCap(capName);
|
|
478
492
|
if (!defaultWrapperAddonId) continue;
|
|
479
493
|
const hubLocalNative = this.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
|
|
480
|
-
const remoteNative =
|
|
494
|
+
const remoteNative = resolveRemote(capName);
|
|
481
495
|
const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
|
|
482
496
|
entries.push({
|
|
483
497
|
capName,
|
|
@@ -500,7 +514,8 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
500
514
|
});
|
|
501
515
|
seenCaps.add(capName);
|
|
502
516
|
}
|
|
503
|
-
|
|
517
|
+
const pushFed = this.remoteNativeCaps.get(input.deviceId);
|
|
518
|
+
if (pushFed) for (const [capName, info] of pushFed) {
|
|
504
519
|
if (seenCaps.has(capName)) continue;
|
|
505
520
|
entries.push({
|
|
506
521
|
capName,
|
|
@@ -511,6 +526,22 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
511
526
|
});
|
|
512
527
|
seenCaps.add(capName);
|
|
513
528
|
}
|
|
529
|
+
const clusterCaps = this.ctx.kernel.listClusterNativeCaps?.();
|
|
530
|
+
if (clusterCaps) for (const entry of clusterCaps) {
|
|
531
|
+
if (entry.deviceId !== input.deviceId) continue;
|
|
532
|
+
if (seenCaps.has(entry.capName)) continue;
|
|
533
|
+
if (!entry.addonId) continue;
|
|
534
|
+
const localNodeId = this.ctx.kernel.localNodeId ?? "hub";
|
|
535
|
+
if (entry.nodeId === localNodeId) continue;
|
|
536
|
+
entries.push({
|
|
537
|
+
capName: entry.capName,
|
|
538
|
+
kind: "native",
|
|
539
|
+
providerAddonId: entry.addonId,
|
|
540
|
+
providerNodeId: entry.nodeId,
|
|
541
|
+
nativeAddonId: entry.addonId
|
|
542
|
+
});
|
|
543
|
+
seenCaps.add(entry.capName);
|
|
544
|
+
}
|
|
514
545
|
return {
|
|
515
546
|
deviceId: input.deviceId,
|
|
516
547
|
entries
|
|
@@ -556,42 +587,26 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
556
587
|
return null;
|
|
557
588
|
}
|
|
558
589
|
const method = kind === "settings" ? "getDeviceSettingsContribution" : "getDeviceLiveContribution";
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const baseId = addonId.includes("@") ? addonId.slice(0, addonId.indexOf("@")) : addonId;
|
|
567
|
-
if (seen.has(baseId)) continue;
|
|
568
|
-
seen.add(baseId);
|
|
569
|
-
contributors.push({
|
|
570
|
-
capName: info.name,
|
|
571
|
-
addonId
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
const results = await Promise.all(contributors.map(async ({ capName, addonId }) => {
|
|
576
|
-
const provider = registry.getProviderByAddon(capName, addonId);
|
|
577
|
-
if (!provider) throw new Error(`[device-manager] capability "${capName}" lists provider "${addonId}" but getProviderByAddon returned null — registry inconsistency`);
|
|
590
|
+
const { entries: bindingEntries } = await this.getBindings({ deviceId });
|
|
591
|
+
const results = await Promise.all(bindingEntries.map(async (entry) => {
|
|
592
|
+
const def = registry.getDefinition(entry.capName);
|
|
593
|
+
if (isDeviceConfigCap(def)) return this.deriveDeviceConfigContribution(registry, def, entry, deviceId, kind);
|
|
594
|
+
if (!def?.exposesDeviceSettings) return null;
|
|
595
|
+
const provider = registry.getProviderForDevice(entry.capName, deviceId);
|
|
596
|
+
if (!provider) return null;
|
|
578
597
|
try {
|
|
579
598
|
const contribution = await provider[method]({ deviceId });
|
|
580
599
|
if (!contribution) return null;
|
|
581
|
-
return
|
|
582
|
-
capName,
|
|
583
|
-
addonId,
|
|
584
|
-
contribution: tagContribution(toWireShape(contribution), capName, addonId, kind)
|
|
585
|
-
};
|
|
600
|
+
return tagContribution(toWireShape(contribution), entry.capName, entry.providerAddonId, kind);
|
|
586
601
|
} catch (err) {
|
|
587
602
|
const msg = err instanceof Error ? err.message : String(err);
|
|
588
603
|
this.ctx.logger.warn("contribution method failed", {
|
|
589
604
|
tags: {
|
|
590
605
|
deviceId,
|
|
591
|
-
addonId
|
|
606
|
+
addonId: entry.providerAddonId
|
|
592
607
|
},
|
|
593
608
|
meta: {
|
|
594
|
-
capName,
|
|
609
|
+
capName: entry.capName,
|
|
595
610
|
method,
|
|
596
611
|
error: msg
|
|
597
612
|
}
|
|
@@ -600,11 +615,75 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
600
615
|
}
|
|
601
616
|
}));
|
|
602
617
|
const base = kind === "settings" ? await this.buildBaseDeviceSection(deviceId) : null;
|
|
603
|
-
const parts = [...base ? [tagContribution(base, "device-manager", "device-manager", kind)] : [], ...results.filter((r) => r !== null)
|
|
618
|
+
const parts = [...base ? [tagContribution(base, "device-manager", "device-manager", kind)] : [], ...results.filter((r) => r !== null)];
|
|
604
619
|
if (parts.length === 0) return null;
|
|
605
620
|
return mergeAggregates(parts);
|
|
606
621
|
}
|
|
607
622
|
/**
|
|
623
|
+
* D14: framework-derived device-config contribution.
|
|
624
|
+
*
|
|
625
|
+
* `kind === 'live'` — device-config caps contribute nothing to the live
|
|
626
|
+
* aggregate (they hold editable config, not live observables).
|
|
627
|
+
*
|
|
628
|
+
* `ui.kind === 'widget'` — emits a single structural `type:'widget'`
|
|
629
|
+
* section; the widget self-persists via the cap's own mutations.
|
|
630
|
+
*
|
|
631
|
+
* `ui.kind === 'derived-form'` — calls `getOptions`/`getStatus` on the
|
|
632
|
+
* bound provider, runs the registered pure builder, and returns the
|
|
633
|
+
* derived form sections. Returns null when the camera exposes nothing
|
|
634
|
+
* configurable or the provider is not yet registered.
|
|
635
|
+
*/
|
|
636
|
+
async deriveDeviceConfigContribution(registry, def, entry, deviceId, kind) {
|
|
637
|
+
if (kind === "live") return null;
|
|
638
|
+
const ui = def.deviceConfig.ui;
|
|
639
|
+
if (ui.kind === "widget") return tagContribution({ sections: [{
|
|
640
|
+
id: `${entry.capName}-widget`,
|
|
641
|
+
title: ui.label,
|
|
642
|
+
tab: ui.tab,
|
|
643
|
+
order: ui.order ?? 0,
|
|
644
|
+
...ui.topTab ? { location: "top-tab" } : {},
|
|
645
|
+
fields: [{
|
|
646
|
+
type: "widget",
|
|
647
|
+
key: `${entry.capName}Widget`,
|
|
648
|
+
label: ui.label,
|
|
649
|
+
widgetId: ui.widgetId
|
|
650
|
+
}]
|
|
651
|
+
}] }, entry.capName, entry.providerAddonId, kind);
|
|
652
|
+
const provider = registry.getProviderForDevice(entry.capName, deviceId);
|
|
653
|
+
if (!provider || typeof provider.getOptions !== "function") return null;
|
|
654
|
+
try {
|
|
655
|
+
const options = await provider.getOptions({ deviceId });
|
|
656
|
+
const status = typeof provider.getStatus === "function" ? await provider.getStatus({ deviceId }) : null;
|
|
657
|
+
const derived = deriveFormContribution(ui.builderId, options, status ?? null);
|
|
658
|
+
if (!derived) return null;
|
|
659
|
+
return tagContribution({
|
|
660
|
+
...derived.tabs ? { tabs: derived.tabs.map((t) => ({ ...t })) } : {},
|
|
661
|
+
sections: derived.sections.map((s) => ({
|
|
662
|
+
id: s.id,
|
|
663
|
+
title: s.title,
|
|
664
|
+
...s.tab !== void 0 ? { tab: s.tab } : {},
|
|
665
|
+
...s.order !== void 0 ? { order: s.order } : {},
|
|
666
|
+
...s.description !== void 0 ? { description: s.description } : {},
|
|
667
|
+
...s.columns !== void 0 ? { columns: s.columns } : {},
|
|
668
|
+
fields: [...s.fields]
|
|
669
|
+
}))
|
|
670
|
+
}, entry.capName, entry.providerAddonId, kind);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
673
|
+
this.ctx.logger.warn("device-config derivation failed", {
|
|
674
|
+
tags: {
|
|
675
|
+
deviceId,
|
|
676
|
+
addonId: entry.providerAddonId
|
|
677
|
+
},
|
|
678
|
+
meta: {
|
|
679
|
+
capName: entry.capName,
|
|
680
|
+
error: msg
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
608
687
|
* Build the device-manager's own contribution to the aggregator — the
|
|
609
688
|
* device identity (id, stableId, addonId, type, online) + the
|
|
610
689
|
* driver-specific config exposed by the device class via
|
|
@@ -674,8 +753,8 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
674
753
|
}] : []
|
|
675
754
|
]
|
|
676
755
|
}];
|
|
677
|
-
const
|
|
678
|
-
if (
|
|
756
|
+
const driverResult = await this.resolveDriverConfigSchema(deviceId, hubLocal);
|
|
757
|
+
if (driverResult.status === "ok") for (const section of driverResult.schema.sections) sections.push({
|
|
679
758
|
id: section.id,
|
|
680
759
|
title: section.title,
|
|
681
760
|
tab: section.tab ?? "general",
|
|
@@ -684,9 +763,22 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
684
763
|
...section.description !== void 0 ? { description: section.description } : {},
|
|
685
764
|
...section.columns !== void 0 ? { columns: section.columns } : {}
|
|
686
765
|
});
|
|
766
|
+
else if (driverResult.status === "unavailable") sections.push({
|
|
767
|
+
id: "driver-settings-unavailable",
|
|
768
|
+
title: "Driver Settings",
|
|
769
|
+
tab: "general",
|
|
770
|
+
order: 1,
|
|
771
|
+
fields: [{
|
|
772
|
+
type: "info",
|
|
773
|
+
key: "driver-settings-unavailable-notice",
|
|
774
|
+
label: "Temporarily unavailable",
|
|
775
|
+
content: "Driver settings are temporarily unavailable — the addon may be restarting. Refresh the page in a moment.",
|
|
776
|
+
variant: "warning"
|
|
777
|
+
}]
|
|
778
|
+
});
|
|
687
779
|
return {
|
|
688
780
|
sections,
|
|
689
|
-
...
|
|
781
|
+
...driverResult.status === "ok" && driverResult.schema.tabs ? { tabs: [...driverResult.schema.tabs] } : {}
|
|
690
782
|
};
|
|
691
783
|
}
|
|
692
784
|
/**
|
|
@@ -699,7 +791,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
699
791
|
addonId: local,
|
|
700
792
|
nodeId: this.ctx.kernel.localNodeId ?? "hub"
|
|
701
793
|
};
|
|
702
|
-
const remote = this.remoteNativeCaps.get(deviceId)?.get("device-ops") ??
|
|
794
|
+
const remote = this.remoteNativeCaps.get(deviceId)?.get("device-ops") ?? this.resolveRemoteNativeCapFromRegistry("device-ops", deviceId);
|
|
703
795
|
return remote ? {
|
|
704
796
|
addonId: remote.addonId,
|
|
705
797
|
nodeId: remote.nodeId
|
|
@@ -735,7 +827,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
735
827
|
out[capName] = null;
|
|
736
828
|
return;
|
|
737
829
|
}
|
|
738
|
-
const provider = registry.
|
|
830
|
+
const provider = registry.getProviderForDevice(capName, input.deviceId);
|
|
739
831
|
if (!provider || typeof provider.getStatus !== "function") {
|
|
740
832
|
out[capName] = null;
|
|
741
833
|
return;
|
|
@@ -776,27 +868,54 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
776
868
|
* devices call `getSettingsUISchema()` directly; forked-worker devices
|
|
777
869
|
* go through the `device-ops.getSettingsSchema` cap method on the
|
|
778
870
|
* numeric-id-keyed native registry.
|
|
871
|
+
*
|
|
872
|
+
* Returns a discriminated result so callers can distinguish three states:
|
|
873
|
+
* 'ok' – schema obtained successfully
|
|
874
|
+
* 'none' – driver genuinely has no settings schema
|
|
875
|
+
* 'unavailable' – worker was unreachable after retries (transient)
|
|
779
876
|
*/
|
|
780
877
|
async resolveDriverConfigSchema(deviceId, hubLocal) {
|
|
781
878
|
if (hubLocal) {
|
|
782
879
|
const schema = hubLocal.device.getSettingsUISchema();
|
|
783
|
-
return schema.sections.length === 0 ?
|
|
880
|
+
return schema.sections.length === 0 ? { status: "none" } : {
|
|
881
|
+
status: "ok",
|
|
882
|
+
schema: toWireShape(schema)
|
|
883
|
+
};
|
|
784
884
|
}
|
|
785
885
|
const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
|
|
786
|
-
if (!ops) return
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
886
|
+
if (!ops) return { status: "none" };
|
|
887
|
+
const MAX_RETRIES = 2;
|
|
888
|
+
const RETRY_DELAY_MS = 750;
|
|
889
|
+
let lastErr;
|
|
890
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
891
|
+
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
892
|
+
try {
|
|
893
|
+
const schema = await ops.getSettingsSchema({ deviceId });
|
|
894
|
+
if (!schema) return { status: "none" };
|
|
895
|
+
const wire = schema;
|
|
896
|
+
return wire.sections.length === 0 ? { status: "none" } : {
|
|
897
|
+
status: "ok",
|
|
898
|
+
schema: toWireShape(wire)
|
|
899
|
+
};
|
|
900
|
+
} catch (err) {
|
|
901
|
+
lastErr = err;
|
|
902
|
+
if (!isTransientMoleculerError(err)) break;
|
|
903
|
+
if (attempt < MAX_RETRIES) this.ctx.logger.debug("cross-process getSettingsSchema transient failure, retrying", {
|
|
904
|
+
tags: { deviceId },
|
|
905
|
+
meta: {
|
|
906
|
+
attempt: attempt + 1,
|
|
907
|
+
maxRetries: MAX_RETRIES,
|
|
908
|
+
error: errMsg(err)
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
}
|
|
799
912
|
}
|
|
913
|
+
const msg = errMsg(lastErr);
|
|
914
|
+
this.ctx.logger.warn("cross-process getSettingsSchema failed after retries — driver settings temporarily unavailable", {
|
|
915
|
+
tags: { deviceId },
|
|
916
|
+
meta: { error: msg }
|
|
917
|
+
});
|
|
918
|
+
return { status: "unavailable" };
|
|
800
919
|
}
|
|
801
920
|
async updateDeviceField(input) {
|
|
802
921
|
if (input.writerCapName === "device-manager") {
|
|
@@ -816,16 +935,38 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
816
935
|
}
|
|
817
936
|
const registry = this.capabilityRegistry;
|
|
818
937
|
if (!registry) throw new Error("[device-manager] updateDeviceField requires capability registry — unavailable on this node");
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
938
|
+
const def = registry.getDefinition(input.writerCapName);
|
|
939
|
+
if (isDeviceConfigCap(def)) {
|
|
940
|
+
if (def.deviceConfig.ui.kind === "widget") return { success: true };
|
|
941
|
+
const dcProvider = registry.getProviderForDevice(input.writerCapName, input.deviceId);
|
|
942
|
+
if (!dcProvider) throw new Error(`[device-manager] no provider for device-config cap "${input.writerCapName}" on device ${input.deviceId}`);
|
|
943
|
+
await applyDerivedFormPatch(def.deviceConfig.ui.builderId, { [input.key]: input.value }, (profile, patch) => dcProvider.setProfile({
|
|
944
|
+
deviceId: input.deviceId,
|
|
945
|
+
profile,
|
|
946
|
+
patch
|
|
947
|
+
}));
|
|
948
|
+
return { success: true };
|
|
949
|
+
}
|
|
950
|
+
if (!def?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${input.writerCapName}" does not expose device settings`);
|
|
951
|
+
await this.resolveContributionProvider(registry, def, input.writerCapName, input.writerAddonId, input.deviceId).applyDeviceSettingsPatch({
|
|
823
952
|
deviceId: input.deviceId,
|
|
824
953
|
patch: { [input.key]: input.value }
|
|
825
954
|
});
|
|
826
955
|
return { success: true };
|
|
827
956
|
}
|
|
828
957
|
/**
|
|
958
|
+
* Resolve the `DeviceSettingsContribution` provider that owns a tagged
|
|
959
|
+
* field. System-scoped caps resolve via `getProviderByAddon`; native
|
|
960
|
+
* device-scoped caps (registered per-device via `registerNativeCap`)
|
|
961
|
+
* live in the separate native map and resolve via `getNativeProvider`.
|
|
962
|
+
* Throws only if BOTH lookups miss.
|
|
963
|
+
*/
|
|
964
|
+
resolveContributionProvider(registry, _def, writerCapName, writerAddonId, deviceId) {
|
|
965
|
+
const provider = registry.getProviderForDevice(writerCapName, deviceId);
|
|
966
|
+
if (provider) return provider;
|
|
967
|
+
throw new Error(`[device-manager] no provider for cap "${writerCapName}" (addon "${writerAddonId}") on device ${deviceId}`);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
829
970
|
* Batched counterpart of `updateDeviceField`. Groups changes by
|
|
830
971
|
* `(writerCapName, writerAddonId)` so each contributor receives a
|
|
831
972
|
* single `applyDeviceSettingsPatch` with all of its updates merged —
|
|
@@ -883,10 +1024,20 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
883
1024
|
}
|
|
884
1025
|
const registry = this.capabilityRegistry;
|
|
885
1026
|
if (!registry) throw new Error("[device-manager] capability registry unavailable");
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1027
|
+
const def = registry.getDefinition(group.writerCapName);
|
|
1028
|
+
if (isDeviceConfigCap(def)) {
|
|
1029
|
+
if (def.deviceConfig.ui.kind === "widget") return;
|
|
1030
|
+
const dcProvider = registry.getProviderForDevice(group.writerCapName, deviceId);
|
|
1031
|
+
if (!dcProvider) throw new Error(`[device-manager] no provider for device-config cap "${group.writerCapName}" on device ${deviceId}`);
|
|
1032
|
+
await applyDerivedFormPatch(def.deviceConfig.ui.builderId, group.patch, (profile, patch) => dcProvider.setProfile({
|
|
1033
|
+
deviceId,
|
|
1034
|
+
profile,
|
|
1035
|
+
patch
|
|
1036
|
+
}));
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (!def?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${group.writerCapName}" does not expose device settings`);
|
|
1040
|
+
await this.resolveContributionProvider(registry, def, group.writerCapName, group.writerAddonId, deviceId).applyDeviceSettingsPatch({
|
|
890
1041
|
deviceId,
|
|
891
1042
|
patch: group.patch
|
|
892
1043
|
});
|
|
@@ -1040,35 +1191,6 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
1040
1191
|
idToAddonId.set(m.id, key.slice(0, sep));
|
|
1041
1192
|
}
|
|
1042
1193
|
}
|
|
1043
|
-
if (cluster) cluster.broker.localBus.on("$node.connected", (payload) => {
|
|
1044
|
-
const connectedNodeId = payload.node.id;
|
|
1045
|
-
const lastSlash = connectedNodeId.lastIndexOf("/");
|
|
1046
|
-
if (lastSlash < 0) return;
|
|
1047
|
-
const connectedAddonId = connectedNodeId.slice(lastSlash + 1);
|
|
1048
|
-
if (connectedAddonId.length === 0) return;
|
|
1049
|
-
for (const [deviceId, ownerAddonId] of idToAddonId) {
|
|
1050
|
-
if (ownerAddonId !== connectedAddonId) continue;
|
|
1051
|
-
if (this.capabilityRegistry?.getNativeAddonId("device-ops", deviceId)) continue;
|
|
1052
|
-
let perDevice = this.remoteNativeCaps.get(deviceId);
|
|
1053
|
-
if (!perDevice) {
|
|
1054
|
-
perDevice = /* @__PURE__ */ new Map();
|
|
1055
|
-
this.remoteNativeCaps.set(deviceId, perDevice);
|
|
1056
|
-
}
|
|
1057
|
-
if (!perDevice.has("device-ops")) perDevice.set("device-ops", {
|
|
1058
|
-
addonId: connectedAddonId,
|
|
1059
|
-
nodeId: connectedNodeId
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
setTimeout(() => {
|
|
1063
|
-
this.discoverWorkerNativeCaps(connectedNodeId, connectedAddonId).catch((err) => {
|
|
1064
|
-
this.ctx.logger.warn("worker native-cap discovery failed", { meta: {
|
|
1065
|
-
nodeId: connectedNodeId,
|
|
1066
|
-
addonId: connectedAddonId,
|
|
1067
|
-
error: errMsg(err)
|
|
1068
|
-
} });
|
|
1069
|
-
});
|
|
1070
|
-
}, 500);
|
|
1071
|
-
});
|
|
1072
1194
|
const allocateNextDeviceId = async () => {
|
|
1073
1195
|
const current = (await readStore()).nextDeviceId ?? 1;
|
|
1074
1196
|
await settings.writeAddonStore({ nextDeviceId: current + 1 });
|
|
@@ -1091,12 +1213,14 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
1091
1213
|
* so the hub only synthesizes a cross-process proxy when the cap is
|
|
1092
1214
|
* actually published — never on speculative device ownership.
|
|
1093
1215
|
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1216
|
+
* Resolution order:
|
|
1217
|
+
* 1. Hub-local `capabilityRegistry` (in-process natives — fastest path).
|
|
1218
|
+
* 2. Push-fed `remoteNativeCaps` cache (`DeviceBindingsChanged` events
|
|
1219
|
+
* from forked workers — accurate in steady state).
|
|
1220
|
+
* 3. Handshake-fed `HubNodeRegistry` via `listClusterNativeCaps()`
|
|
1221
|
+
* (D3 re-handshake after device restore — covers the Moleculer
|
|
1222
|
+
* transport window where push events were lost). This is the
|
|
1223
|
+
* reliable replacement for the deleted `syncWorkerNativeCaps` pull.
|
|
1100
1224
|
*/
|
|
1101
1225
|
resolveNativeCapOwnerSync: (capName, deviceId) => {
|
|
1102
1226
|
const localAddonId = this.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
|
|
@@ -1109,7 +1233,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
1109
1233
|
addonId: remote.addonId,
|
|
1110
1234
|
nodeId: remote.nodeId
|
|
1111
1235
|
};
|
|
1112
|
-
return
|
|
1236
|
+
return this.resolveRemoteNativeCapFromRegistry(capName, deviceId);
|
|
1113
1237
|
},
|
|
1114
1238
|
/** Idempotent numeric-id reservation. Callers invoke this before
|
|
1115
1239
|
* constructing the owning `IDevice` so `DeviceContext.id` is bound
|