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