@camstack/core 0.1.38 → 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 +17 -0
- 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 +95 -5
- 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 +95 -5
- 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 +419 -97
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +419 -98
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TokenPayload, IScopedLogger } from '@camstack/types';
|
|
1
|
+
import { TokenPayload, IScopedLogger, TokenScope } from '@camstack/types';
|
|
2
2
|
export type { TokenPayload };
|
|
3
3
|
export type AuthConfigReader = {
|
|
4
4
|
get<T>(path: string): T;
|
|
@@ -90,6 +90,17 @@ export interface SsoBridgeClaims {
|
|
|
90
90
|
* issuers set it.
|
|
91
91
|
*/
|
|
92
92
|
readonly hubUrl?: string;
|
|
93
|
+
/** Permission scopes baked into the token by the OAuth account-linking
|
|
94
|
+
* grant. Absent on ordinary SSO-login bridge tokens. */
|
|
95
|
+
readonly scopes?: readonly TokenScope[];
|
|
96
|
+
/** OAuth authorization-code binding — set only on `oauth-code` tokens. */
|
|
97
|
+
readonly redirectUri?: string;
|
|
98
|
+
readonly integrationId?: string;
|
|
99
|
+
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
100
|
+
readonly jti?: string;
|
|
101
|
+
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
102
|
+
* tokens so the verify path can check the session is not revoked. */
|
|
103
|
+
readonly sessionId?: string;
|
|
93
104
|
}
|
|
94
105
|
export interface TotpChallengeClaims {
|
|
95
106
|
readonly userId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-manager.d.ts","sourceRoot":"","sources":["../../src/auth/auth-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"auth-manager.d.ts","sourceRoot":"","sources":["../../src/auth/auth-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TokenScope, MethodAccess } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* True if the scope set grants `access` on device-scoped capabilities.
|
|
4
|
+
* A `category:device` grant covers every device cap (the broad grant).
|
|
5
|
+
* Mirrors the OR-semantics of the existing token scope matcher.
|
|
6
|
+
*/
|
|
7
|
+
export declare function scopesAllowDeviceCap(scopes: readonly TokenScope[], access: MethodAccess): boolean;
|
|
8
|
+
//# sourceMappingURL=scope-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-matcher.d.ts","sourceRoot":"","sources":["../../src/auth/scope-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE/D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAIjG"}
|
|
@@ -9,7 +9,6 @@ export interface TotpStatus {
|
|
|
9
9
|
}
|
|
10
10
|
export declare class TotpManager {
|
|
11
11
|
private readonly store;
|
|
12
|
-
private readonly opts;
|
|
13
12
|
constructor(store: SettingsStoreClient, opts?: {
|
|
14
13
|
/** ±N 30-second windows tolerated. Default 1 = ±30 s clock skew. */
|
|
15
14
|
readonly window?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"totp-manager.d.ts","sourceRoot":"","sources":["../../src/auth/totp-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAoD1D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CACpC;AAED,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,KAAK;
|
|
1
|
+
{"version":3,"file":"totp-manager.d.ts","sourceRoot":"","sources":["../../src/auth/totp-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAoD1D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CACpC;AAED,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,mBAAmB,EAC3C,IAAI,GAAE;QACJ,oEAAoE;QACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KACpB;IAOR;;;;;;;;OAQG;IACG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAoBvE;;;;;;OAMG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAe7D;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5C;;;;;OAKG;IACG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO5D;;;OAGG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;YAStC,IAAI;CAQnB"}
|
|
@@ -16,6 +16,21 @@ export declare class AddonWidgetsAggregatorAddon extends BaseAddon {
|
|
|
16
16
|
constructor();
|
|
17
17
|
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
18
18
|
protected onShutdown(): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Strip the `@<nodeId>` suffix that the CapabilityBridge appends to
|
|
21
|
+
* collection-provider registry keys for cross-node addons (see
|
|
22
|
+
* `moleculer.service.ts` — `registryKey = ${addonId}@${nodeId}`).
|
|
23
|
+
*
|
|
24
|
+
* The widget bundle is hub-resident (the same npm package ships to
|
|
25
|
+
* every node and the hub keeps a copy on disk keyed by the bare
|
|
26
|
+
* manifest id), so both the static-file URL and the admin-ui widget
|
|
27
|
+
* namespace must use the bare addon id. Without this, a widget
|
|
28
|
+
* source running on a remote agent yields a `bundleUrl` like
|
|
29
|
+
* `/api/addon-widgets/pipeline-analytics@agent-0/pipeline-analytics/remoteEntry.js`
|
|
30
|
+
* — the embedded `@<node>/<group>` makes the static-file route's
|
|
31
|
+
* `:addonId` param mismatch the registered provider and 404.
|
|
32
|
+
*/
|
|
33
|
+
private bareAddonId;
|
|
19
34
|
private aggregate;
|
|
20
35
|
private scheduleRetry;
|
|
21
36
|
private retrySource;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-widgets-aggregator.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.ts"],"names":[],"mappings":"AAuBA,OAAO,EACL,SAAS,EAMT,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAA;AA4BxB,qBAAa,2BAA4B,SAAQ,SAAS;IACxD,QAAQ,CAAC,EAAE,8BAA6B;IAExC,OAAO,CAAC,aAAa,CAA6B;IAElD;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+C;IAExE,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;;cAIhD,YAAY,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;cAW/C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"addon-widgets-aggregator.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.ts"],"names":[],"mappings":"AAuBA,OAAO,EACL,SAAS,EAMT,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAA;AA4BxB,qBAAa,2BAA4B,SAAQ,SAAS;IACxD,QAAQ,CAAC,EAAE,8BAA6B;IAExC,OAAO,CAAC,aAAa,CAA6B;IAElD;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+C;IAExE,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;;cAIhD,YAAY,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;cAW/C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3C;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,WAAW;YAKL,SAAS;IA2DvB,OAAO,CAAC,aAAa;YAYP,WAAW;IAoCzB;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,iBAAiB;YAcX,YAAY;CAgB3B;AAED,eAAe,2BAA2B,CAAA"}
|
|
@@ -77,17 +77,37 @@ var AddonWidgetsAggregatorAddon = class extends _camstack_types.BaseAddon {
|
|
|
77
77
|
this.retryTimers.clear();
|
|
78
78
|
this.lastGood.clear();
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Strip the `@<nodeId>` suffix that the CapabilityBridge appends to
|
|
82
|
+
* collection-provider registry keys for cross-node addons (see
|
|
83
|
+
* `moleculer.service.ts` — `registryKey = ${addonId}@${nodeId}`).
|
|
84
|
+
*
|
|
85
|
+
* The widget bundle is hub-resident (the same npm package ships to
|
|
86
|
+
* every node and the hub keeps a copy on disk keyed by the bare
|
|
87
|
+
* manifest id), so both the static-file URL and the admin-ui widget
|
|
88
|
+
* namespace must use the bare addon id. Without this, a widget
|
|
89
|
+
* source running on a remote agent yields a `bundleUrl` like
|
|
90
|
+
* `/api/addon-widgets/pipeline-analytics@agent-0/pipeline-analytics/remoteEntry.js`
|
|
91
|
+
* — the embedded `@<node>/<group>` makes the static-file route's
|
|
92
|
+
* `:addonId` param mismatch the registered provider and 404.
|
|
93
|
+
*/
|
|
94
|
+
bareAddonId(registryKey) {
|
|
95
|
+
const at = registryKey.indexOf("@");
|
|
96
|
+
return at === -1 ? registryKey : registryKey.slice(0, at);
|
|
97
|
+
}
|
|
80
98
|
async aggregate() {
|
|
81
99
|
const entries = this.capabilities?.getCollectionEntries("addon-widgets-source") ?? [];
|
|
82
100
|
const out = [];
|
|
83
101
|
const seenIds = /* @__PURE__ */ new Set();
|
|
84
|
-
for (const [
|
|
102
|
+
for (const [registryKey, source] of entries) {
|
|
103
|
+
const addonId = registryKey;
|
|
104
|
+
const publicAddonId = this.bareAddonId(registryKey);
|
|
85
105
|
seenIds.add(addonId);
|
|
86
106
|
try {
|
|
87
107
|
const enriched = (await Promise.resolve(source.listWidgets())).map((w) => ({
|
|
88
108
|
...w,
|
|
89
|
-
addonId,
|
|
90
|
-
bundleUrl: this.makeBundleUrl(
|
|
109
|
+
addonId: publicAddonId,
|
|
110
|
+
bundleUrl: this.makeBundleUrl(publicAddonId, w.bundle)
|
|
91
111
|
}));
|
|
92
112
|
for (const item of enriched) out.push(item);
|
|
93
113
|
this.lastGood.set(addonId, enriched);
|
|
@@ -125,11 +145,12 @@ var AddonWidgetsAggregatorAddon = class extends _camstack_types.BaseAddon {
|
|
|
125
145
|
const found = (this.capabilities?.getCollectionEntries("addon-widgets-source") ?? []).find(([id]) => id === sourceId);
|
|
126
146
|
if (!found) return;
|
|
127
147
|
const [addonId, source] = found;
|
|
148
|
+
const publicAddonId = this.bareAddonId(addonId);
|
|
128
149
|
try {
|
|
129
150
|
const enriched = (await Promise.resolve(source.listWidgets())).map((w) => ({
|
|
130
151
|
...w,
|
|
131
|
-
addonId,
|
|
132
|
-
bundleUrl: this.makeBundleUrl(
|
|
152
|
+
addonId: publicAddonId,
|
|
153
|
+
bundleUrl: this.makeBundleUrl(publicAddonId, w.bundle)
|
|
133
154
|
}));
|
|
134
155
|
this.lastGood.set(addonId, enriched);
|
|
135
156
|
this.ctx.logger.info("addon-widgets-source recovered after retry", { meta: {
|
|
@@ -146,7 +167,7 @@ var AddonWidgetsAggregatorAddon = class extends _camstack_types.BaseAddon {
|
|
|
146
167
|
},
|
|
147
168
|
category: _camstack_types.EventCategory.AddonWidgetReady,
|
|
148
169
|
data: {
|
|
149
|
-
addonId,
|
|
170
|
+
addonId: publicAddonId,
|
|
150
171
|
recovered: true
|
|
151
172
|
}
|
|
152
173
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-widgets-aggregator.addon.js","names":[],"sources":["../../../src/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.ts"],"sourcesContent":["/**\n * Addon Widgets Aggregator — hub-local builtin that owns the singleton\n * `addon-widgets` cap.\n *\n * Mirrors `addon-pages-aggregator` exactly: walks every registered\n * `addon-widgets-source` (collection) provider and emits an enriched\n * widget metadata list with versioned `bundleUrl`s pointing at\n * `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem\n * `mtime` cache-buster lets the browser pick up addon rebuilds without\n * manual reload.\n *\n * The static file endpoint (`/api/addon-widgets/:addonId/*`) is served\n * by `AddonWidgetsService.resolveBundle()` on the server side; this\n * addon only owns the listing surface.\n *\n * Why a builtin: same reasoning as `addon-pages-aggregator`. The\n * aggregator is the de-facto \"addon-widgets provider\" — addons own caps,\n * not the server. Living in `@camstack/core/builtins` keeps the surface\n * symmetrical with `system-config`, `local-auth`, etc.\n */\nimport * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { randomUUID } from 'node:crypto'\nimport {\n BaseAddon,\n EventCategory,\n addonWidgetsCapability,\n errMsg,\n type IAddonWidgetsAggregatorProvider,\n type IAddonWidgetsSourceProvider,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface ResolvedPaths {\n readonly addonsDir: string\n}\n\n/**\n * Inferred from the cap definition — equivalent to:\n * `z.infer<typeof EnrichedWidgetMetadataSchema>` but reuses the\n * provider's return type so the aggregator stays in lockstep with the\n * cap if its shape evolves.\n */\ntype EnrichedWidget = Awaited<ReturnType<IAddonWidgetsAggregatorProvider['listWidgets']>>[number]\ntype RawWidget = Awaited<ReturnType<IAddonWidgetsSourceProvider['listWidgets']>>[number]\n\n/**\n * Backoff schedule (ms) used to retry sources that failed during a\n * `listWidgets()` round-trip — typically because the cap was just\n * registered (provider connected via Moleculer) but the worker-side\n * action registration hadn't propagated yet, so `Service '...listWidgets'\n * is not found on '<node>'` raced ahead of the call.\n *\n * On success we re-emit `AddonWidgetReady` so admin-ui invalidates its\n * `addonWidgets.listWidgets` query and the registry populates without a\n * page reload.\n */\nconst RETRY_BACKOFF_MS: readonly number[] = [500, 1500, 4000]\n\nexport class AddonWidgetsAggregatorAddon extends BaseAddon {\n readonly id = 'addon-widgets-aggregator'\n\n private resolvedPaths: ResolvedPaths | null = null\n\n /**\n * Last successful `listWidgets()` snapshot per source. Used as the\n * \"stale-but-valid\" fallback when a source transiently fails — drops\n * happen often enough during boot (Moleculer service-discovery\n * window) that swallowing the error and returning empty would leave\n * the dashboard with nothing for several seconds. Keeping the\n * previous good entry means a flake is invisible to the operator.\n */\n private readonly lastGood = new Map<string, readonly EnrichedWidget[]>()\n\n /** In-flight retry guards keyed by sourceAddonId. Avoids double-scheduling. */\n private readonly retryTimers = new Map<string, NodeJS.Timeout>()\n\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.resolvedPaths = await this.resolvePaths()\n\n const provider: IAddonWidgetsAggregatorProvider = {\n listWidgets: async (): Promise<readonly EnrichedWidget[]> => this.aggregate(),\n }\n\n this.ctx.logger.info('Initialized — aggregating addon-widgets-source providers')\n return [{ capability: addonWidgetsCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n for (const t of this.retryTimers.values()) clearTimeout(t)\n this.retryTimers.clear()\n this.lastGood.clear()\n }\n\n // ── Aggregation ───────────────────────────────────────────────────\n\n private async aggregate(): Promise<readonly EnrichedWidget[]> {\n // `getCollectionEntries` returns `[addonId, provider]` tuples — the\n // raw `addon-widgets-source` cap doesn't carry an `id` on the\n // provider (unlike the legacy `IAddonPageProvider`), so we lean on\n // the registry to attribute each contribution back to its addon.\n const entries = this.capabilities?.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source') ?? []\n const out: EnrichedWidget[] = []\n const seenIds = new Set<string>()\n\n for (const [addonId, source] of entries) {\n seenIds.add(addonId)\n try {\n const widgets = await Promise.resolve(source.listWidgets())\n const enriched: EnrichedWidget[] = widgets.map((w: RawWidget) => ({\n ...w,\n addonId,\n bundleUrl: this.makeBundleUrl(addonId, w.bundle),\n }))\n for (const item of enriched) out.push(item)\n // Cache successful snapshot — used as fallback on next failure.\n this.lastGood.set(addonId, enriched)\n } catch (err: unknown) {\n const message = errMsg(err)\n this.ctx.logger.warn('addon-widgets-source provider failed', {\n meta: { sourceId: addonId, error: message },\n })\n // Fall back to the last-good snapshot for this source so a\n // transient Moleculer service-discovery race doesn't blank\n // the dashboard.\n const cached = this.lastGood.get(addonId)\n if (cached !== undefined) {\n for (const item of cached) out.push(item)\n this.ctx.logger.info('addon-widgets-source falling back to cached snapshot', {\n meta: { sourceId: addonId, cachedWidgets: cached.length },\n })\n }\n // Schedule a background retry. On success we re-emit\n // `AddonWidgetReady` so admin-ui's queryClient invalidates and\n // any newly-loaded widgets show up without a manual reload.\n this.scheduleRetry(addonId)\n }\n }\n\n // Drop cache entries for sources that have disappeared from the\n // registry — keeps the fallback aligned with the live collection.\n for (const cachedId of this.lastGood.keys()) {\n if (!seenIds.has(cachedId)) this.lastGood.delete(cachedId)\n }\n\n return out\n }\n\n // ── Retry on transient Moleculer race ─────────────────────────────\n\n private scheduleRetry(sourceId: string, attempt = 0): void {\n if (attempt >= RETRY_BACKOFF_MS.length) return\n if (this.retryTimers.has(sourceId)) return // already pending\n\n const delayMs = RETRY_BACKOFF_MS[attempt] ?? RETRY_BACKOFF_MS[RETRY_BACKOFF_MS.length - 1]!\n const timer = setTimeout(() => {\n this.retryTimers.delete(sourceId)\n void this.retrySource(sourceId, attempt)\n }, delayMs)\n this.retryTimers.set(sourceId, timer)\n }\n\n private async retrySource(sourceId: string, attempt: number): Promise<void> {\n const entries = this.capabilities?.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source') ?? []\n const found = entries.find(([id]) => id === sourceId)\n if (!found) return // provider went away; nothing to retry\n const [addonId, source] = found\n\n try {\n const widgets = await Promise.resolve(source.listWidgets())\n const enriched: EnrichedWidget[] = widgets.map((w: RawWidget) => ({\n ...w,\n addonId,\n bundleUrl: this.makeBundleUrl(addonId, w.bundle),\n }))\n this.lastGood.set(addonId, enriched)\n this.ctx.logger.info('addon-widgets-source recovered after retry', {\n meta: { sourceId: addonId, attempt: attempt + 1, widgets: enriched.length },\n })\n // Re-emit AddonWidgetReady so admin-ui invalidates and refetches.\n this.ctx.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: this.id },\n category: EventCategory.AddonWidgetReady,\n data: { addonId, recovered: true },\n })\n } catch (err: unknown) {\n this.ctx.logger.debug('addon-widgets-source retry failed', {\n meta: { sourceId, attempt: attempt + 1, error: errMsg(err) },\n })\n this.scheduleRetry(sourceId, attempt + 1)\n }\n }\n\n // ── Bundle URL stamping ──────────────────────────────────────────\n\n /**\n * Build `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. Falls back\n * to `Date.now()` when the bundle path can't be stat'd (remote addon\n * with no local file, addon not yet on disk, etc.) — the browser\n * just gets a fresh URL on each call instead of cache-friendly mtime.\n */\n private makeBundleUrl(addonId: string, bundle: string): string {\n const bundlePath = this.resolveBundlePath(addonId, bundle)\n let mtime = Date.now()\n if (bundlePath !== null) {\n try { mtime = fs.statSync(bundlePath).mtimeMs }\n catch { /* remote addon — no local file */ }\n }\n const v = Math.floor(mtime)\n return `/api/addon-widgets/${addonId}/${bundle}?v=${v}`\n }\n\n private resolveBundlePath(addonId: string, bundle: string): string | null {\n const paths = this.resolvedPaths\n if (!paths) return null\n const addonDistPath = path.join(paths.addonsDir, '@camstack', `addon-${addonId}`, 'dist')\n const resolvedBase = path.resolve(addonDistPath)\n const resolvedFile = path.resolve(addonDistPath, bundle)\n if (!resolvedFile.startsWith(resolvedBase + path.sep) && resolvedFile !== resolvedBase) {\n return null\n }\n return resolvedFile\n }\n\n // ── Path resolution ──────────────────────────────────────────────\n\n private async resolvePaths(): Promise<ResolvedPaths> {\n const fallback: ResolvedPaths = { addonsDir: path.resolve('camstack-data', 'addons') }\n if (!this.ctx.settings) return fallback\n try {\n const server = await this.ctx.settings.getSection('server')\n const dataPath = typeof server['dataPath'] === 'string' && server['dataPath']\n ? server['dataPath']\n : 'camstack-data'\n return { addonsDir: path.resolve(dataPath, 'addons') }\n } catch (err: unknown) {\n this.ctx.logger.debug('Failed to read server.dataPath — falling back', {\n meta: { error: errMsg(err) },\n })\n return fallback\n }\n }\n}\n\nexport default AddonWidgetsAggregatorAddon\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"addon-widgets-aggregator.addon.js","names":[],"sources":["../../../src/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.ts"],"sourcesContent":["/**\n * Addon Widgets Aggregator — hub-local builtin that owns the singleton\n * `addon-widgets` cap.\n *\n * Mirrors `addon-pages-aggregator` exactly: walks every registered\n * `addon-widgets-source` (collection) provider and emits an enriched\n * widget metadata list with versioned `bundleUrl`s pointing at\n * `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem\n * `mtime` cache-buster lets the browser pick up addon rebuilds without\n * manual reload.\n *\n * The static file endpoint (`/api/addon-widgets/:addonId/*`) is served\n * by `AddonWidgetsService.resolveBundle()` on the server side; this\n * addon only owns the listing surface.\n *\n * Why a builtin: same reasoning as `addon-pages-aggregator`. The\n * aggregator is the de-facto \"addon-widgets provider\" — addons own caps,\n * not the server. Living in `@camstack/core/builtins` keeps the surface\n * symmetrical with `system-config`, `local-auth`, etc.\n */\nimport * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { randomUUID } from 'node:crypto'\nimport {\n BaseAddon,\n EventCategory,\n addonWidgetsCapability,\n errMsg,\n type IAddonWidgetsAggregatorProvider,\n type IAddonWidgetsSourceProvider,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface ResolvedPaths {\n readonly addonsDir: string\n}\n\n/**\n * Inferred from the cap definition — equivalent to:\n * `z.infer<typeof EnrichedWidgetMetadataSchema>` but reuses the\n * provider's return type so the aggregator stays in lockstep with the\n * cap if its shape evolves.\n */\ntype EnrichedWidget = Awaited<ReturnType<IAddonWidgetsAggregatorProvider['listWidgets']>>[number]\ntype RawWidget = Awaited<ReturnType<IAddonWidgetsSourceProvider['listWidgets']>>[number]\n\n/**\n * Backoff schedule (ms) used to retry sources that failed during a\n * `listWidgets()` round-trip — typically because the cap was just\n * registered (provider connected via Moleculer) but the worker-side\n * action registration hadn't propagated yet, so `Service '...listWidgets'\n * is not found on '<node>'` raced ahead of the call.\n *\n * On success we re-emit `AddonWidgetReady` so admin-ui invalidates its\n * `addonWidgets.listWidgets` query and the registry populates without a\n * page reload.\n */\nconst RETRY_BACKOFF_MS: readonly number[] = [500, 1500, 4000]\n\nexport class AddonWidgetsAggregatorAddon extends BaseAddon {\n readonly id = 'addon-widgets-aggregator'\n\n private resolvedPaths: ResolvedPaths | null = null\n\n /**\n * Last successful `listWidgets()` snapshot per source. Used as the\n * \"stale-but-valid\" fallback when a source transiently fails — drops\n * happen often enough during boot (Moleculer service-discovery\n * window) that swallowing the error and returning empty would leave\n * the dashboard with nothing for several seconds. Keeping the\n * previous good entry means a flake is invisible to the operator.\n */\n private readonly lastGood = new Map<string, readonly EnrichedWidget[]>()\n\n /** In-flight retry guards keyed by sourceAddonId. Avoids double-scheduling. */\n private readonly retryTimers = new Map<string, NodeJS.Timeout>()\n\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.resolvedPaths = await this.resolvePaths()\n\n const provider: IAddonWidgetsAggregatorProvider = {\n listWidgets: async (): Promise<readonly EnrichedWidget[]> => this.aggregate(),\n }\n\n this.ctx.logger.info('Initialized — aggregating addon-widgets-source providers')\n return [{ capability: addonWidgetsCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n for (const t of this.retryTimers.values()) clearTimeout(t)\n this.retryTimers.clear()\n this.lastGood.clear()\n }\n\n // ── Aggregation ───────────────────────────────────────────────────\n\n /**\n * Strip the `@<nodeId>` suffix that the CapabilityBridge appends to\n * collection-provider registry keys for cross-node addons (see\n * `moleculer.service.ts` — `registryKey = ${addonId}@${nodeId}`).\n *\n * The widget bundle is hub-resident (the same npm package ships to\n * every node and the hub keeps a copy on disk keyed by the bare\n * manifest id), so both the static-file URL and the admin-ui widget\n * namespace must use the bare addon id. Without this, a widget\n * source running on a remote agent yields a `bundleUrl` like\n * `/api/addon-widgets/pipeline-analytics@agent-0/pipeline-analytics/remoteEntry.js`\n * — the embedded `@<node>/<group>` makes the static-file route's\n * `:addonId` param mismatch the registered provider and 404.\n */\n private bareAddonId(registryKey: string): string {\n const at = registryKey.indexOf('@')\n return at === -1 ? registryKey : registryKey.slice(0, at)\n }\n\n private async aggregate(): Promise<readonly EnrichedWidget[]> {\n // `getCollectionEntries` returns `[addonId, provider]` tuples — the\n // raw `addon-widgets-source` cap doesn't carry an `id` on the\n // provider (unlike the legacy `IAddonPageProvider`), so we lean on\n // the registry to attribute each contribution back to its addon.\n const entries = this.capabilities?.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source') ?? []\n const out: EnrichedWidget[] = []\n const seenIds = new Set<string>()\n\n for (const [registryKey, source] of entries) {\n // Cache/retry bookkeeping keys on the full registry key so the\n // same addon on multiple nodes stays distinct; the emitted widget\n // metadata uses the bare id (node-agnostic, filesystem-resolvable).\n const addonId = registryKey\n const publicAddonId = this.bareAddonId(registryKey)\n seenIds.add(addonId)\n try {\n const widgets = await Promise.resolve(source.listWidgets())\n const enriched: EnrichedWidget[] = widgets.map((w: RawWidget) => ({\n ...w,\n addonId: publicAddonId,\n bundleUrl: this.makeBundleUrl(publicAddonId, w.bundle),\n }))\n for (const item of enriched) out.push(item)\n // Cache successful snapshot — used as fallback on next failure.\n this.lastGood.set(addonId, enriched)\n } catch (err: unknown) {\n const message = errMsg(err)\n this.ctx.logger.warn('addon-widgets-source provider failed', {\n meta: { sourceId: addonId, error: message },\n })\n // Fall back to the last-good snapshot for this source so a\n // transient Moleculer service-discovery race doesn't blank\n // the dashboard.\n const cached = this.lastGood.get(addonId)\n if (cached !== undefined) {\n for (const item of cached) out.push(item)\n this.ctx.logger.info('addon-widgets-source falling back to cached snapshot', {\n meta: { sourceId: addonId, cachedWidgets: cached.length },\n })\n }\n // Schedule a background retry. On success we re-emit\n // `AddonWidgetReady` so admin-ui's queryClient invalidates and\n // any newly-loaded widgets show up without a manual reload.\n this.scheduleRetry(addonId)\n }\n }\n\n // Drop cache entries for sources that have disappeared from the\n // registry — keeps the fallback aligned with the live collection.\n for (const cachedId of this.lastGood.keys()) {\n if (!seenIds.has(cachedId)) this.lastGood.delete(cachedId)\n }\n\n return out\n }\n\n // ── Retry on transient Moleculer race ─────────────────────────────\n\n private scheduleRetry(sourceId: string, attempt = 0): void {\n if (attempt >= RETRY_BACKOFF_MS.length) return\n if (this.retryTimers.has(sourceId)) return // already pending\n\n const delayMs = RETRY_BACKOFF_MS[attempt] ?? RETRY_BACKOFF_MS[RETRY_BACKOFF_MS.length - 1]!\n const timer = setTimeout(() => {\n this.retryTimers.delete(sourceId)\n void this.retrySource(sourceId, attempt)\n }, delayMs)\n this.retryTimers.set(sourceId, timer)\n }\n\n private async retrySource(sourceId: string, attempt: number): Promise<void> {\n const entries = this.capabilities?.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source') ?? []\n const found = entries.find(([id]) => id === sourceId)\n if (!found) return // provider went away; nothing to retry\n const [addonId, source] = found\n const publicAddonId = this.bareAddonId(addonId)\n\n try {\n const widgets = await Promise.resolve(source.listWidgets())\n const enriched: EnrichedWidget[] = widgets.map((w: RawWidget) => ({\n ...w,\n addonId: publicAddonId,\n bundleUrl: this.makeBundleUrl(publicAddonId, w.bundle),\n }))\n this.lastGood.set(addonId, enriched)\n this.ctx.logger.info('addon-widgets-source recovered after retry', {\n meta: { sourceId: addonId, attempt: attempt + 1, widgets: enriched.length },\n })\n // Re-emit AddonWidgetReady so admin-ui invalidates and refetches.\n this.ctx.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: this.id },\n category: EventCategory.AddonWidgetReady,\n data: { addonId: publicAddonId, recovered: true },\n })\n } catch (err: unknown) {\n this.ctx.logger.debug('addon-widgets-source retry failed', {\n meta: { sourceId, attempt: attempt + 1, error: errMsg(err) },\n })\n this.scheduleRetry(sourceId, attempt + 1)\n }\n }\n\n // ── Bundle URL stamping ──────────────────────────────────────────\n\n /**\n * Build `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. Falls back\n * to `Date.now()` when the bundle path can't be stat'd (remote addon\n * with no local file, addon not yet on disk, etc.) — the browser\n * just gets a fresh URL on each call instead of cache-friendly mtime.\n */\n private makeBundleUrl(addonId: string, bundle: string): string {\n const bundlePath = this.resolveBundlePath(addonId, bundle)\n let mtime = Date.now()\n if (bundlePath !== null) {\n try { mtime = fs.statSync(bundlePath).mtimeMs }\n catch { /* remote addon — no local file */ }\n }\n const v = Math.floor(mtime)\n return `/api/addon-widgets/${addonId}/${bundle}?v=${v}`\n }\n\n private resolveBundlePath(addonId: string, bundle: string): string | null {\n const paths = this.resolvedPaths\n if (!paths) return null\n const addonDistPath = path.join(paths.addonsDir, '@camstack', `addon-${addonId}`, 'dist')\n const resolvedBase = path.resolve(addonDistPath)\n const resolvedFile = path.resolve(addonDistPath, bundle)\n if (!resolvedFile.startsWith(resolvedBase + path.sep) && resolvedFile !== resolvedBase) {\n return null\n }\n return resolvedFile\n }\n\n // ── Path resolution ──────────────────────────────────────────────\n\n private async resolvePaths(): Promise<ResolvedPaths> {\n const fallback: ResolvedPaths = { addonsDir: path.resolve('camstack-data', 'addons') }\n if (!this.ctx.settings) return fallback\n try {\n const server = await this.ctx.settings.getSection('server')\n const dataPath = typeof server['dataPath'] === 'string' && server['dataPath']\n ? server['dataPath']\n : 'camstack-data'\n return { addonsDir: path.resolve(dataPath, 'addons') }\n } catch (err: unknown) {\n this.ctx.logger.debug('Failed to read server.dataPath — falling back', {\n meta: { error: errMsg(err) },\n })\n return fallback\n }\n }\n}\n\nexport default AddonWidgetsAggregatorAddon\n"],"mappings":"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"}
|
|
@@ -70,17 +70,37 @@ var AddonWidgetsAggregatorAddon = class extends BaseAddon {
|
|
|
70
70
|
this.retryTimers.clear();
|
|
71
71
|
this.lastGood.clear();
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Strip the `@<nodeId>` suffix that the CapabilityBridge appends to
|
|
75
|
+
* collection-provider registry keys for cross-node addons (see
|
|
76
|
+
* `moleculer.service.ts` — `registryKey = ${addonId}@${nodeId}`).
|
|
77
|
+
*
|
|
78
|
+
* The widget bundle is hub-resident (the same npm package ships to
|
|
79
|
+
* every node and the hub keeps a copy on disk keyed by the bare
|
|
80
|
+
* manifest id), so both the static-file URL and the admin-ui widget
|
|
81
|
+
* namespace must use the bare addon id. Without this, a widget
|
|
82
|
+
* source running on a remote agent yields a `bundleUrl` like
|
|
83
|
+
* `/api/addon-widgets/pipeline-analytics@agent-0/pipeline-analytics/remoteEntry.js`
|
|
84
|
+
* — the embedded `@<node>/<group>` makes the static-file route's
|
|
85
|
+
* `:addonId` param mismatch the registered provider and 404.
|
|
86
|
+
*/
|
|
87
|
+
bareAddonId(registryKey) {
|
|
88
|
+
const at = registryKey.indexOf("@");
|
|
89
|
+
return at === -1 ? registryKey : registryKey.slice(0, at);
|
|
90
|
+
}
|
|
73
91
|
async aggregate() {
|
|
74
92
|
const entries = this.capabilities?.getCollectionEntries("addon-widgets-source") ?? [];
|
|
75
93
|
const out = [];
|
|
76
94
|
const seenIds = /* @__PURE__ */ new Set();
|
|
77
|
-
for (const [
|
|
95
|
+
for (const [registryKey, source] of entries) {
|
|
96
|
+
const addonId = registryKey;
|
|
97
|
+
const publicAddonId = this.bareAddonId(registryKey);
|
|
78
98
|
seenIds.add(addonId);
|
|
79
99
|
try {
|
|
80
100
|
const enriched = (await Promise.resolve(source.listWidgets())).map((w) => ({
|
|
81
101
|
...w,
|
|
82
|
-
addonId,
|
|
83
|
-
bundleUrl: this.makeBundleUrl(
|
|
102
|
+
addonId: publicAddonId,
|
|
103
|
+
bundleUrl: this.makeBundleUrl(publicAddonId, w.bundle)
|
|
84
104
|
}));
|
|
85
105
|
for (const item of enriched) out.push(item);
|
|
86
106
|
this.lastGood.set(addonId, enriched);
|
|
@@ -118,11 +138,12 @@ var AddonWidgetsAggregatorAddon = class extends BaseAddon {
|
|
|
118
138
|
const found = (this.capabilities?.getCollectionEntries("addon-widgets-source") ?? []).find(([id]) => id === sourceId);
|
|
119
139
|
if (!found) return;
|
|
120
140
|
const [addonId, source] = found;
|
|
141
|
+
const publicAddonId = this.bareAddonId(addonId);
|
|
121
142
|
try {
|
|
122
143
|
const enriched = (await Promise.resolve(source.listWidgets())).map((w) => ({
|
|
123
144
|
...w,
|
|
124
|
-
addonId,
|
|
125
|
-
bundleUrl: this.makeBundleUrl(
|
|
145
|
+
addonId: publicAddonId,
|
|
146
|
+
bundleUrl: this.makeBundleUrl(publicAddonId, w.bundle)
|
|
126
147
|
}));
|
|
127
148
|
this.lastGood.set(addonId, enriched);
|
|
128
149
|
this.ctx.logger.info("addon-widgets-source recovered after retry", { meta: {
|
|
@@ -139,7 +160,7 @@ var AddonWidgetsAggregatorAddon = class extends BaseAddon {
|
|
|
139
160
|
},
|
|
140
161
|
category: EventCategory.AddonWidgetReady,
|
|
141
162
|
data: {
|
|
142
|
-
addonId,
|
|
163
|
+
addonId: publicAddonId,
|
|
143
164
|
recovered: true
|
|
144
165
|
}
|
|
145
166
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-widgets-aggregator.addon.mjs","names":[],"sources":["../../../src/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.ts"],"sourcesContent":["/**\n * Addon Widgets Aggregator — hub-local builtin that owns the singleton\n * `addon-widgets` cap.\n *\n * Mirrors `addon-pages-aggregator` exactly: walks every registered\n * `addon-widgets-source` (collection) provider and emits an enriched\n * widget metadata list with versioned `bundleUrl`s pointing at\n * `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem\n * `mtime` cache-buster lets the browser pick up addon rebuilds without\n * manual reload.\n *\n * The static file endpoint (`/api/addon-widgets/:addonId/*`) is served\n * by `AddonWidgetsService.resolveBundle()` on the server side; this\n * addon only owns the listing surface.\n *\n * Why a builtin: same reasoning as `addon-pages-aggregator`. The\n * aggregator is the de-facto \"addon-widgets provider\" — addons own caps,\n * not the server. Living in `@camstack/core/builtins` keeps the surface\n * symmetrical with `system-config`, `local-auth`, etc.\n */\nimport * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { randomUUID } from 'node:crypto'\nimport {\n BaseAddon,\n EventCategory,\n addonWidgetsCapability,\n errMsg,\n type IAddonWidgetsAggregatorProvider,\n type IAddonWidgetsSourceProvider,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface ResolvedPaths {\n readonly addonsDir: string\n}\n\n/**\n * Inferred from the cap definition — equivalent to:\n * `z.infer<typeof EnrichedWidgetMetadataSchema>` but reuses the\n * provider's return type so the aggregator stays in lockstep with the\n * cap if its shape evolves.\n */\ntype EnrichedWidget = Awaited<ReturnType<IAddonWidgetsAggregatorProvider['listWidgets']>>[number]\ntype RawWidget = Awaited<ReturnType<IAddonWidgetsSourceProvider['listWidgets']>>[number]\n\n/**\n * Backoff schedule (ms) used to retry sources that failed during a\n * `listWidgets()` round-trip — typically because the cap was just\n * registered (provider connected via Moleculer) but the worker-side\n * action registration hadn't propagated yet, so `Service '...listWidgets'\n * is not found on '<node>'` raced ahead of the call.\n *\n * On success we re-emit `AddonWidgetReady` so admin-ui invalidates its\n * `addonWidgets.listWidgets` query and the registry populates without a\n * page reload.\n */\nconst RETRY_BACKOFF_MS: readonly number[] = [500, 1500, 4000]\n\nexport class AddonWidgetsAggregatorAddon extends BaseAddon {\n readonly id = 'addon-widgets-aggregator'\n\n private resolvedPaths: ResolvedPaths | null = null\n\n /**\n * Last successful `listWidgets()` snapshot per source. Used as the\n * \"stale-but-valid\" fallback when a source transiently fails — drops\n * happen often enough during boot (Moleculer service-discovery\n * window) that swallowing the error and returning empty would leave\n * the dashboard with nothing for several seconds. Keeping the\n * previous good entry means a flake is invisible to the operator.\n */\n private readonly lastGood = new Map<string, readonly EnrichedWidget[]>()\n\n /** In-flight retry guards keyed by sourceAddonId. Avoids double-scheduling. */\n private readonly retryTimers = new Map<string, NodeJS.Timeout>()\n\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.resolvedPaths = await this.resolvePaths()\n\n const provider: IAddonWidgetsAggregatorProvider = {\n listWidgets: async (): Promise<readonly EnrichedWidget[]> => this.aggregate(),\n }\n\n this.ctx.logger.info('Initialized — aggregating addon-widgets-source providers')\n return [{ capability: addonWidgetsCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n for (const t of this.retryTimers.values()) clearTimeout(t)\n this.retryTimers.clear()\n this.lastGood.clear()\n }\n\n // ── Aggregation ───────────────────────────────────────────────────\n\n private async aggregate(): Promise<readonly EnrichedWidget[]> {\n // `getCollectionEntries` returns `[addonId, provider]` tuples — the\n // raw `addon-widgets-source` cap doesn't carry an `id` on the\n // provider (unlike the legacy `IAddonPageProvider`), so we lean on\n // the registry to attribute each contribution back to its addon.\n const entries = this.capabilities?.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source') ?? []\n const out: EnrichedWidget[] = []\n const seenIds = new Set<string>()\n\n for (const [addonId, source] of entries) {\n seenIds.add(addonId)\n try {\n const widgets = await Promise.resolve(source.listWidgets())\n const enriched: EnrichedWidget[] = widgets.map((w: RawWidget) => ({\n ...w,\n addonId,\n bundleUrl: this.makeBundleUrl(addonId, w.bundle),\n }))\n for (const item of enriched) out.push(item)\n // Cache successful snapshot — used as fallback on next failure.\n this.lastGood.set(addonId, enriched)\n } catch (err: unknown) {\n const message = errMsg(err)\n this.ctx.logger.warn('addon-widgets-source provider failed', {\n meta: { sourceId: addonId, error: message },\n })\n // Fall back to the last-good snapshot for this source so a\n // transient Moleculer service-discovery race doesn't blank\n // the dashboard.\n const cached = this.lastGood.get(addonId)\n if (cached !== undefined) {\n for (const item of cached) out.push(item)\n this.ctx.logger.info('addon-widgets-source falling back to cached snapshot', {\n meta: { sourceId: addonId, cachedWidgets: cached.length },\n })\n }\n // Schedule a background retry. On success we re-emit\n // `AddonWidgetReady` so admin-ui's queryClient invalidates and\n // any newly-loaded widgets show up without a manual reload.\n this.scheduleRetry(addonId)\n }\n }\n\n // Drop cache entries for sources that have disappeared from the\n // registry — keeps the fallback aligned with the live collection.\n for (const cachedId of this.lastGood.keys()) {\n if (!seenIds.has(cachedId)) this.lastGood.delete(cachedId)\n }\n\n return out\n }\n\n // ── Retry on transient Moleculer race ─────────────────────────────\n\n private scheduleRetry(sourceId: string, attempt = 0): void {\n if (attempt >= RETRY_BACKOFF_MS.length) return\n if (this.retryTimers.has(sourceId)) return // already pending\n\n const delayMs = RETRY_BACKOFF_MS[attempt] ?? RETRY_BACKOFF_MS[RETRY_BACKOFF_MS.length - 1]!\n const timer = setTimeout(() => {\n this.retryTimers.delete(sourceId)\n void this.retrySource(sourceId, attempt)\n }, delayMs)\n this.retryTimers.set(sourceId, timer)\n }\n\n private async retrySource(sourceId: string, attempt: number): Promise<void> {\n const entries = this.capabilities?.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source') ?? []\n const found = entries.find(([id]) => id === sourceId)\n if (!found) return // provider went away; nothing to retry\n const [addonId, source] = found\n\n try {\n const widgets = await Promise.resolve(source.listWidgets())\n const enriched: EnrichedWidget[] = widgets.map((w: RawWidget) => ({\n ...w,\n addonId,\n bundleUrl: this.makeBundleUrl(addonId, w.bundle),\n }))\n this.lastGood.set(addonId, enriched)\n this.ctx.logger.info('addon-widgets-source recovered after retry', {\n meta: { sourceId: addonId, attempt: attempt + 1, widgets: enriched.length },\n })\n // Re-emit AddonWidgetReady so admin-ui invalidates and refetches.\n this.ctx.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: this.id },\n category: EventCategory.AddonWidgetReady,\n data: { addonId, recovered: true },\n })\n } catch (err: unknown) {\n this.ctx.logger.debug('addon-widgets-source retry failed', {\n meta: { sourceId, attempt: attempt + 1, error: errMsg(err) },\n })\n this.scheduleRetry(sourceId, attempt + 1)\n }\n }\n\n // ── Bundle URL stamping ──────────────────────────────────────────\n\n /**\n * Build `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. Falls back\n * to `Date.now()` when the bundle path can't be stat'd (remote addon\n * with no local file, addon not yet on disk, etc.) — the browser\n * just gets a fresh URL on each call instead of cache-friendly mtime.\n */\n private makeBundleUrl(addonId: string, bundle: string): string {\n const bundlePath = this.resolveBundlePath(addonId, bundle)\n let mtime = Date.now()\n if (bundlePath !== null) {\n try { mtime = fs.statSync(bundlePath).mtimeMs }\n catch { /* remote addon — no local file */ }\n }\n const v = Math.floor(mtime)\n return `/api/addon-widgets/${addonId}/${bundle}?v=${v}`\n }\n\n private resolveBundlePath(addonId: string, bundle: string): string | null {\n const paths = this.resolvedPaths\n if (!paths) return null\n const addonDistPath = path.join(paths.addonsDir, '@camstack', `addon-${addonId}`, 'dist')\n const resolvedBase = path.resolve(addonDistPath)\n const resolvedFile = path.resolve(addonDistPath, bundle)\n if (!resolvedFile.startsWith(resolvedBase + path.sep) && resolvedFile !== resolvedBase) {\n return null\n }\n return resolvedFile\n }\n\n // ── Path resolution ──────────────────────────────────────────────\n\n private async resolvePaths(): Promise<ResolvedPaths> {\n const fallback: ResolvedPaths = { addonsDir: path.resolve('camstack-data', 'addons') }\n if (!this.ctx.settings) return fallback\n try {\n const server = await this.ctx.settings.getSection('server')\n const dataPath = typeof server['dataPath'] === 'string' && server['dataPath']\n ? server['dataPath']\n : 'camstack-data'\n return { addonsDir: path.resolve(dataPath, 'addons') }\n } catch (err: unknown) {\n this.ctx.logger.debug('Failed to read server.dataPath — falling back', {\n meta: { error: errMsg(err) },\n })\n return fallback\n }\n }\n}\n\nexport default AddonWidgetsAggregatorAddon\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"addon-widgets-aggregator.addon.mjs","names":[],"sources":["../../../src/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.ts"],"sourcesContent":["/**\n * Addon Widgets Aggregator — hub-local builtin that owns the singleton\n * `addon-widgets` cap.\n *\n * Mirrors `addon-pages-aggregator` exactly: walks every registered\n * `addon-widgets-source` (collection) provider and emits an enriched\n * widget metadata list with versioned `bundleUrl`s pointing at\n * `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem\n * `mtime` cache-buster lets the browser pick up addon rebuilds without\n * manual reload.\n *\n * The static file endpoint (`/api/addon-widgets/:addonId/*`) is served\n * by `AddonWidgetsService.resolveBundle()` on the server side; this\n * addon only owns the listing surface.\n *\n * Why a builtin: same reasoning as `addon-pages-aggregator`. The\n * aggregator is the de-facto \"addon-widgets provider\" — addons own caps,\n * not the server. Living in `@camstack/core/builtins` keeps the surface\n * symmetrical with `system-config`, `local-auth`, etc.\n */\nimport * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { randomUUID } from 'node:crypto'\nimport {\n BaseAddon,\n EventCategory,\n addonWidgetsCapability,\n errMsg,\n type IAddonWidgetsAggregatorProvider,\n type IAddonWidgetsSourceProvider,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface ResolvedPaths {\n readonly addonsDir: string\n}\n\n/**\n * Inferred from the cap definition — equivalent to:\n * `z.infer<typeof EnrichedWidgetMetadataSchema>` but reuses the\n * provider's return type so the aggregator stays in lockstep with the\n * cap if its shape evolves.\n */\ntype EnrichedWidget = Awaited<ReturnType<IAddonWidgetsAggregatorProvider['listWidgets']>>[number]\ntype RawWidget = Awaited<ReturnType<IAddonWidgetsSourceProvider['listWidgets']>>[number]\n\n/**\n * Backoff schedule (ms) used to retry sources that failed during a\n * `listWidgets()` round-trip — typically because the cap was just\n * registered (provider connected via Moleculer) but the worker-side\n * action registration hadn't propagated yet, so `Service '...listWidgets'\n * is not found on '<node>'` raced ahead of the call.\n *\n * On success we re-emit `AddonWidgetReady` so admin-ui invalidates its\n * `addonWidgets.listWidgets` query and the registry populates without a\n * page reload.\n */\nconst RETRY_BACKOFF_MS: readonly number[] = [500, 1500, 4000]\n\nexport class AddonWidgetsAggregatorAddon extends BaseAddon {\n readonly id = 'addon-widgets-aggregator'\n\n private resolvedPaths: ResolvedPaths | null = null\n\n /**\n * Last successful `listWidgets()` snapshot per source. Used as the\n * \"stale-but-valid\" fallback when a source transiently fails — drops\n * happen often enough during boot (Moleculer service-discovery\n * window) that swallowing the error and returning empty would leave\n * the dashboard with nothing for several seconds. Keeping the\n * previous good entry means a flake is invisible to the operator.\n */\n private readonly lastGood = new Map<string, readonly EnrichedWidget[]>()\n\n /** In-flight retry guards keyed by sourceAddonId. Avoids double-scheduling. */\n private readonly retryTimers = new Map<string, NodeJS.Timeout>()\n\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.resolvedPaths = await this.resolvePaths()\n\n const provider: IAddonWidgetsAggregatorProvider = {\n listWidgets: async (): Promise<readonly EnrichedWidget[]> => this.aggregate(),\n }\n\n this.ctx.logger.info('Initialized — aggregating addon-widgets-source providers')\n return [{ capability: addonWidgetsCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n for (const t of this.retryTimers.values()) clearTimeout(t)\n this.retryTimers.clear()\n this.lastGood.clear()\n }\n\n // ── Aggregation ───────────────────────────────────────────────────\n\n /**\n * Strip the `@<nodeId>` suffix that the CapabilityBridge appends to\n * collection-provider registry keys for cross-node addons (see\n * `moleculer.service.ts` — `registryKey = ${addonId}@${nodeId}`).\n *\n * The widget bundle is hub-resident (the same npm package ships to\n * every node and the hub keeps a copy on disk keyed by the bare\n * manifest id), so both the static-file URL and the admin-ui widget\n * namespace must use the bare addon id. Without this, a widget\n * source running on a remote agent yields a `bundleUrl` like\n * `/api/addon-widgets/pipeline-analytics@agent-0/pipeline-analytics/remoteEntry.js`\n * — the embedded `@<node>/<group>` makes the static-file route's\n * `:addonId` param mismatch the registered provider and 404.\n */\n private bareAddonId(registryKey: string): string {\n const at = registryKey.indexOf('@')\n return at === -1 ? registryKey : registryKey.slice(0, at)\n }\n\n private async aggregate(): Promise<readonly EnrichedWidget[]> {\n // `getCollectionEntries` returns `[addonId, provider]` tuples — the\n // raw `addon-widgets-source` cap doesn't carry an `id` on the\n // provider (unlike the legacy `IAddonPageProvider`), so we lean on\n // the registry to attribute each contribution back to its addon.\n const entries = this.capabilities?.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source') ?? []\n const out: EnrichedWidget[] = []\n const seenIds = new Set<string>()\n\n for (const [registryKey, source] of entries) {\n // Cache/retry bookkeeping keys on the full registry key so the\n // same addon on multiple nodes stays distinct; the emitted widget\n // metadata uses the bare id (node-agnostic, filesystem-resolvable).\n const addonId = registryKey\n const publicAddonId = this.bareAddonId(registryKey)\n seenIds.add(addonId)\n try {\n const widgets = await Promise.resolve(source.listWidgets())\n const enriched: EnrichedWidget[] = widgets.map((w: RawWidget) => ({\n ...w,\n addonId: publicAddonId,\n bundleUrl: this.makeBundleUrl(publicAddonId, w.bundle),\n }))\n for (const item of enriched) out.push(item)\n // Cache successful snapshot — used as fallback on next failure.\n this.lastGood.set(addonId, enriched)\n } catch (err: unknown) {\n const message = errMsg(err)\n this.ctx.logger.warn('addon-widgets-source provider failed', {\n meta: { sourceId: addonId, error: message },\n })\n // Fall back to the last-good snapshot for this source so a\n // transient Moleculer service-discovery race doesn't blank\n // the dashboard.\n const cached = this.lastGood.get(addonId)\n if (cached !== undefined) {\n for (const item of cached) out.push(item)\n this.ctx.logger.info('addon-widgets-source falling back to cached snapshot', {\n meta: { sourceId: addonId, cachedWidgets: cached.length },\n })\n }\n // Schedule a background retry. On success we re-emit\n // `AddonWidgetReady` so admin-ui's queryClient invalidates and\n // any newly-loaded widgets show up without a manual reload.\n this.scheduleRetry(addonId)\n }\n }\n\n // Drop cache entries for sources that have disappeared from the\n // registry — keeps the fallback aligned with the live collection.\n for (const cachedId of this.lastGood.keys()) {\n if (!seenIds.has(cachedId)) this.lastGood.delete(cachedId)\n }\n\n return out\n }\n\n // ── Retry on transient Moleculer race ─────────────────────────────\n\n private scheduleRetry(sourceId: string, attempt = 0): void {\n if (attempt >= RETRY_BACKOFF_MS.length) return\n if (this.retryTimers.has(sourceId)) return // already pending\n\n const delayMs = RETRY_BACKOFF_MS[attempt] ?? RETRY_BACKOFF_MS[RETRY_BACKOFF_MS.length - 1]!\n const timer = setTimeout(() => {\n this.retryTimers.delete(sourceId)\n void this.retrySource(sourceId, attempt)\n }, delayMs)\n this.retryTimers.set(sourceId, timer)\n }\n\n private async retrySource(sourceId: string, attempt: number): Promise<void> {\n const entries = this.capabilities?.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source') ?? []\n const found = entries.find(([id]) => id === sourceId)\n if (!found) return // provider went away; nothing to retry\n const [addonId, source] = found\n const publicAddonId = this.bareAddonId(addonId)\n\n try {\n const widgets = await Promise.resolve(source.listWidgets())\n const enriched: EnrichedWidget[] = widgets.map((w: RawWidget) => ({\n ...w,\n addonId: publicAddonId,\n bundleUrl: this.makeBundleUrl(publicAddonId, w.bundle),\n }))\n this.lastGood.set(addonId, enriched)\n this.ctx.logger.info('addon-widgets-source recovered after retry', {\n meta: { sourceId: addonId, attempt: attempt + 1, widgets: enriched.length },\n })\n // Re-emit AddonWidgetReady so admin-ui invalidates and refetches.\n this.ctx.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: this.id },\n category: EventCategory.AddonWidgetReady,\n data: { addonId: publicAddonId, recovered: true },\n })\n } catch (err: unknown) {\n this.ctx.logger.debug('addon-widgets-source retry failed', {\n meta: { sourceId, attempt: attempt + 1, error: errMsg(err) },\n })\n this.scheduleRetry(sourceId, attempt + 1)\n }\n }\n\n // ── Bundle URL stamping ──────────────────────────────────────────\n\n /**\n * Build `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. Falls back\n * to `Date.now()` when the bundle path can't be stat'd (remote addon\n * with no local file, addon not yet on disk, etc.) — the browser\n * just gets a fresh URL on each call instead of cache-friendly mtime.\n */\n private makeBundleUrl(addonId: string, bundle: string): string {\n const bundlePath = this.resolveBundlePath(addonId, bundle)\n let mtime = Date.now()\n if (bundlePath !== null) {\n try { mtime = fs.statSync(bundlePath).mtimeMs }\n catch { /* remote addon — no local file */ }\n }\n const v = Math.floor(mtime)\n return `/api/addon-widgets/${addonId}/${bundle}?v=${v}`\n }\n\n private resolveBundlePath(addonId: string, bundle: string): string | null {\n const paths = this.resolvedPaths\n if (!paths) return null\n const addonDistPath = path.join(paths.addonsDir, '@camstack', `addon-${addonId}`, 'dist')\n const resolvedBase = path.resolve(addonDistPath)\n const resolvedFile = path.resolve(addonDistPath, bundle)\n if (!resolvedFile.startsWith(resolvedBase + path.sep) && resolvedFile !== resolvedBase) {\n return null\n }\n return resolvedFile\n }\n\n // ── Path resolution ──────────────────────────────────────────────\n\n private async resolvePaths(): Promise<ResolvedPaths> {\n const fallback: ResolvedPaths = { addonsDir: path.resolve('camstack-data', 'addons') }\n if (!this.ctx.settings) return fallback\n try {\n const server = await this.ctx.settings.getSection('server')\n const dataPath = typeof server['dataPath'] === 'string' && server['dataPath']\n ? server['dataPath']\n : 'camstack-data'\n return { addonsDir: path.resolve(dataPath, 'addons') }\n } catch (err: unknown) {\n this.ctx.logger.debug('Failed to read server.dataPath — falling back', {\n meta: { error: errMsg(err) },\n })\n return fallback\n }\n }\n}\n\nexport default AddonWidgetsAggregatorAddon\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { StreamProfile, StreamProfilePatch } from '@camstack/types';
|
|
2
|
+
/** A framework-derived contribution shape — sections[] (+ optional tabs[]). */
|
|
3
|
+
export interface DerivedContributionShape {
|
|
4
|
+
readonly tabs?: ReadonlyArray<{
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon: string;
|
|
8
|
+
order?: number;
|
|
9
|
+
}>;
|
|
10
|
+
readonly sections: ReadonlyArray<{
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
tab?: string;
|
|
14
|
+
order?: number;
|
|
15
|
+
description?: string;
|
|
16
|
+
columns?: 1 | 2 | 3 | 4;
|
|
17
|
+
fields: readonly unknown[];
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/** A per-profile mutation callback the cap's provider supplies. */
|
|
21
|
+
export type ProfileSetter = (profile: StreamProfile, patch: StreamProfilePatch) => Promise<void>;
|
|
22
|
+
/** The single registered builderId today. New device-config caps add their own. */
|
|
23
|
+
export declare const STREAM_PARAMS_BUILDER_ID: "stream-params";
|
|
24
|
+
/**
|
|
25
|
+
* Build the device-detail form section for a `derived-form` device-config
|
|
26
|
+
* cap. Returns null when the camera exposes no configurable property.
|
|
27
|
+
*/
|
|
28
|
+
export declare function deriveFormContribution(builderId: string, options: unknown, status: unknown): DerivedContributionShape | null;
|
|
29
|
+
/**
|
|
30
|
+
* Route a flat form patch back through the cap's per-profile setter.
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyDerivedFormPatch(builderId: string, patch: Record<string, unknown>, setProfile: ProfileSetter): Promise<void>;
|
|
33
|
+
//# sourceMappingURL=device-config-contribution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-config-contribution.d.ts","sourceRoot":"","sources":["../../../src/builtins/device-manager/device-config-contribution.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAGV,aAAa,EACb,kBAAkB,EAEnB,MAAM,iBAAiB,CAAA;AAExB,+EAA+E;AAC/E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1F,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;QAC/B,EAAE,EAAE,MAAM,CAAA;QACV,KAAK,EAAE,MAAM,CAAA;QACb,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACvB,MAAM,EAAE,SAAS,OAAO,EAAE,CAAA;KAC3B,CAAC,CAAA;CACH;AAED,mEAAmE;AACnE,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEhG,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,EAAG,eAAwB,CAAA;AAEhE;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,OAAO,GACd,wBAAwB,GAAG,IAAI,CAoBjC;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,UAAU,EAAE,aAAa,GACxB,OAAO,CAAC,IAAI,CAAC,CAQf"}
|
|
@@ -65,11 +65,21 @@ export declare class DeviceManagerAddon extends BaseAddon {
|
|
|
65
65
|
private static readonly RUNTIME_STATE_DEBOUNCE_MS;
|
|
66
66
|
/**
|
|
67
67
|
* Cross-process native-provider cache: deviceId (numeric) → capName → { addonId, nodeId }.
|
|
68
|
-
* Kept in sync with `
|
|
69
|
-
* workers
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
68
|
+
* Kept in sync with `DeviceBindingsChanged` push events emitted by forked
|
|
69
|
+
* workers on `ctx.registerNativeCap` / device removal. Union'd into
|
|
70
|
+
* `getBindings` so hub-side consumers see every native cap regardless of
|
|
71
|
+
* which process owns the IDevice.
|
|
72
|
+
*
|
|
73
|
+
* No persistence — entries re-populate when the worker re-handshakes or
|
|
74
|
+
* re-emits its native-cap registrations after restart. Entries that were
|
|
75
|
+
* lost in the Moleculer transport handshake window are recovered lazily:
|
|
76
|
+
* `resolveNativeCapOwnerSync` and `getBindings` fall through to
|
|
77
|
+
* `ctx.kernel.listClusterNativeCaps()` (the handshake-fed
|
|
78
|
+
* `HubNodeRegistry`) when the push-based cache misses.
|
|
79
|
+
*
|
|
80
|
+
* The previous pull-based recovery (`syncWorkerNativeCaps`, driven by
|
|
81
|
+
* `$node.connected` + `addon.restarted`) has been removed in Task 13 —
|
|
82
|
+
* the D3 re-handshake after device restore is the reliable replacement.
|
|
73
83
|
*/
|
|
74
84
|
private remoteNativeCaps;
|
|
75
85
|
/** Wait for a device-provider by addonId, returning null on timeout. */
|
|
@@ -80,20 +90,17 @@ export declare class DeviceManagerAddon extends BaseAddon {
|
|
|
80
90
|
private writeBindingsStore;
|
|
81
91
|
private resolveWrapperNodeId;
|
|
82
92
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* `
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* connected node, calls each service's `$listDeviceIds` action to
|
|
90
|
-
* fetch the deviceIds the worker has registered that cap on, then
|
|
91
|
-
* writes entries into `remoteNativeCaps`.
|
|
93
|
+
* Resolve a remote native cap entry for a given `(capName, deviceId)` by
|
|
94
|
+
* consulting the handshake-fed `HubNodeRegistry` via
|
|
95
|
+
* `ctx.kernel.listClusterNativeCaps()`. Called when the push-based
|
|
96
|
+
* `remoteNativeCaps` cache misses — covers the Moleculer transport
|
|
97
|
+
* handshake window where `DeviceBindingsChanged` events were lost but the
|
|
98
|
+
* D3 re-handshake (post device restore) has already populated the registry.
|
|
92
99
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
100
|
+
* Returns `null` when the entry is genuinely not present in the cluster
|
|
101
|
+
* view (cap not registered on any worker for that device).
|
|
95
102
|
*/
|
|
96
|
-
private
|
|
103
|
+
private resolveRemoteNativeCapFromRegistry;
|
|
97
104
|
getBindings(input: {
|
|
98
105
|
deviceId: number;
|
|
99
106
|
}): Promise<{
|
|
@@ -124,6 +131,21 @@ export declare class DeviceManagerAddon extends BaseAddon {
|
|
|
124
131
|
*/
|
|
125
132
|
private lookupPersistedStableId;
|
|
126
133
|
getDeviceAggregate(deviceId: number, kind: 'settings' | 'live'): Promise<ContributionShape | null>;
|
|
134
|
+
/**
|
|
135
|
+
* D14: framework-derived device-config contribution.
|
|
136
|
+
*
|
|
137
|
+
* `kind === 'live'` — device-config caps contribute nothing to the live
|
|
138
|
+
* aggregate (they hold editable config, not live observables).
|
|
139
|
+
*
|
|
140
|
+
* `ui.kind === 'widget'` — emits a single structural `type:'widget'`
|
|
141
|
+
* section; the widget self-persists via the cap's own mutations.
|
|
142
|
+
*
|
|
143
|
+
* `ui.kind === 'derived-form'` — calls `getOptions`/`getStatus` on the
|
|
144
|
+
* bound provider, runs the registered pure builder, and returns the
|
|
145
|
+
* derived form sections. Returns null when the camera exposes nothing
|
|
146
|
+
* configurable or the provider is not yet registered.
|
|
147
|
+
*/
|
|
148
|
+
private deriveDeviceConfigContribution;
|
|
127
149
|
/**
|
|
128
150
|
* Build the device-manager's own contribution to the aggregator — the
|
|
129
151
|
* device identity (id, stableId, addonId, type, online) + the
|
|
@@ -172,6 +194,11 @@ export declare class DeviceManagerAddon extends BaseAddon {
|
|
|
172
194
|
* devices call `getSettingsUISchema()` directly; forked-worker devices
|
|
173
195
|
* go through the `device-ops.getSettingsSchema` cap method on the
|
|
174
196
|
* numeric-id-keyed native registry.
|
|
197
|
+
*
|
|
198
|
+
* Returns a discriminated result so callers can distinguish three states:
|
|
199
|
+
* 'ok' – schema obtained successfully
|
|
200
|
+
* 'none' – driver genuinely has no settings schema
|
|
201
|
+
* 'unavailable' – worker was unreachable after retries (transient)
|
|
175
202
|
*/
|
|
176
203
|
private resolveDriverConfigSchema;
|
|
177
204
|
updateDeviceField(input: {
|
|
@@ -183,6 +210,14 @@ export declare class DeviceManagerAddon extends BaseAddon {
|
|
|
183
210
|
}): Promise<{
|
|
184
211
|
success: true;
|
|
185
212
|
}>;
|
|
213
|
+
/**
|
|
214
|
+
* Resolve the `DeviceSettingsContribution` provider that owns a tagged
|
|
215
|
+
* field. System-scoped caps resolve via `getProviderByAddon`; native
|
|
216
|
+
* device-scoped caps (registered per-device via `registerNativeCap`)
|
|
217
|
+
* live in the separate native map and resolve via `getNativeProvider`.
|
|
218
|
+
* Throws only if BOTH lookups miss.
|
|
219
|
+
*/
|
|
220
|
+
private resolveContributionProvider;
|
|
186
221
|
/**
|
|
187
222
|
* Batched counterpart of `updateDeviceField`. Groups changes by
|
|
188
223
|
* `(writerCapName, writerAddonId)` so each contributor receives a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-manager.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/device-manager/device-manager.addon.ts"],"names":[],"mappings":"AAyBA,OAAO,EACL,oBAAoB,EACpB,SAAS,
|
|
1
|
+
{"version":3,"file":"device-manager.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/device-manager/device-manager.addon.ts"],"names":[],"mappings":"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"}
|