@camstack/server 1.1.38 → 1.1.40
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/api/trpc/cap-mount-helpers.js +0 -63
- package/dist/api/trpc/cap-router-runtime.js +11 -33
- package/dist/api/trpc/core-cap-bridge.js +22 -0
- package/dist/api/trpc/generated-cap-mounts.js +1 -13
- package/dist/api/trpc/generated-cap-routers.js +341 -491
- package/dist/api/trpc/trpc.router.js +30 -48
- package/dist/core/addon/framework-live-sync.js +69 -19
- package/dist/core/moleculer/moleculer.service.js +11 -0
- package/package.json +1 -1
|
@@ -17,10 +17,8 @@ const repl_router_js_1 = require("../core/repl.router.js");
|
|
|
17
17
|
const settings_backend_router_js_1 = require("../core/settings-backend.router.js");
|
|
18
18
|
const stream_probe_router_js_1 = require("../core/stream-probe.router.js");
|
|
19
19
|
const system_events_router_js_1 = require("../core/system-events.router.js");
|
|
20
|
-
const cap_mount_helpers_js_1 = require("./cap-mount-helpers.js");
|
|
21
20
|
const cap_router_runtime_js_1 = require("./cap-router-runtime.js");
|
|
22
21
|
const client_ip_js_1 = require("./client-ip.js");
|
|
23
|
-
const generated_cap_routers_1 = require("./generated-cap-routers");
|
|
24
22
|
const trpc_middleware_1 = require("./trpc.middleware");
|
|
25
23
|
/**
|
|
26
24
|
* Merge the server-read User-Agent into a signaling call's
|
|
@@ -67,15 +65,18 @@ function wrapWebrtcSessionProviderWithRelay(provider, ctx) {
|
|
|
67
65
|
};
|
|
68
66
|
}
|
|
69
67
|
/**
|
|
70
|
-
* Build the
|
|
71
|
-
* `mount: { kind: '
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* registry
|
|
75
|
-
*
|
|
68
|
+
* Build the `server-provided` provider factory map (BIG PLAN 2). Caps marked
|
|
69
|
+
* `mount: { kind: 'server-provided' }` have their provider supplied by the
|
|
70
|
+
* server, keyed by cap NAME. The factory either composes backend services
|
|
71
|
+
* (`system`, `nodes`, `integrations`, `addons`, `network-quality`, `toast`) or
|
|
72
|
+
* transforms a registry provider (`webrtc-session` wraps the broker to inject
|
|
73
|
+
* the request User-Agent). The runtime builder mounts the route from the live
|
|
74
|
+
* def schemas and resolves the provider through this map instead of the
|
|
75
|
+
* registry — keeping backend composition in `@camstack/server` (the
|
|
76
|
+
* self-sufficiency invariant: `@camstack/system` never touches a backend
|
|
76
77
|
* service). Each factory receives the per-request tRPC `ctx`.
|
|
77
78
|
*/
|
|
78
|
-
function
|
|
79
|
+
function buildServerProviders(services) {
|
|
79
80
|
const wrap = (fn) => (ctx) => fn(ctx);
|
|
80
81
|
return {
|
|
81
82
|
'network-quality': wrap(() => (0, cap_providers_js_1.buildNetworkQualityProvider)(services.networkQualityService)),
|
|
@@ -84,6 +85,15 @@ function buildServiceProviders(services) {
|
|
|
84
85
|
integrations: wrap(() => (0, cap_providers_js_1.buildIntegrationsProvider)(services.addonRegistry, services.eventBus, services.loggingService, services.capabilityRegistry)),
|
|
85
86
|
nodes: wrap(() => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry)),
|
|
86
87
|
addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx)),
|
|
88
|
+
// webrtc-session — the resolved broker singleton, wrapped per request to
|
|
89
|
+
// inject the server-read User-Agent into signaling attribution (the broker
|
|
90
|
+
// is a forked addon that can't see the HTTP request). Cross-node (agent-
|
|
91
|
+
// hosted broker) calls route through the generic remote-proxy leg and skip
|
|
92
|
+
// this local factory, exactly as the retired custom mount did.
|
|
93
|
+
'webrtc-session': wrap((ctx) => {
|
|
94
|
+
const provider = services.capabilityRegistry?.getSingleton('webrtc-session') ?? null;
|
|
95
|
+
return provider ? wrapWebrtcSessionProviderWithRelay(provider, ctx) : null;
|
|
96
|
+
}),
|
|
87
97
|
};
|
|
88
98
|
}
|
|
89
99
|
/**
|
|
@@ -102,11 +112,10 @@ function buildServiceProviders(services) {
|
|
|
102
112
|
* full procedure inference and `generate-api-types.ts` resolves the same
|
|
103
113
|
* expanded type as before.
|
|
104
114
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* to fill.
|
|
115
|
+
* Spread order: the dynamic caps first, then the hand-written core (non-cap)
|
|
116
|
+
* routers. Every cap — including the former `custom` ones — is now built by the
|
|
117
|
+
* runtime builder; the server only supplies `server-provided` provider factories
|
|
118
|
+
* (`buildServerProviders`), never a hand-written cap-router override.
|
|
110
119
|
*/
|
|
111
120
|
function buildCapabilityRouters(services) {
|
|
112
121
|
// ── Dynamic cap routers (value) → static cap-router map (type). ──────
|
|
@@ -120,13 +129,15 @@ function buildCapabilityRouters(services) {
|
|
|
120
129
|
const dynamicCaps = (0, cap_router_runtime_js_1.buildRuntimeCapRouters)({
|
|
121
130
|
capabilityRegistry: services.capabilityRegistry,
|
|
122
131
|
moleculer: services.moleculer,
|
|
123
|
-
|
|
132
|
+
serverProviders: buildServerProviders(services),
|
|
124
133
|
});
|
|
125
134
|
return {
|
|
126
|
-
// ── Runtime-built cap routers:
|
|
127
|
-
// `singleton` / `collection` / `device-native` / `
|
|
128
|
-
// / `hub-only`.
|
|
129
|
-
//
|
|
135
|
+
// ── Runtime-built cap routers: EVERY cap whose mount is
|
|
136
|
+
// `singleton` / `collection` / `device-native` / `server-provided`
|
|
137
|
+
// / `hub-only`. Only `skip` caps are excluded (never mounted). The
|
|
138
|
+
// former `custom` caps are gone: `snapshot-provider` was dead
|
|
139
|
+
// (deleted); `webrtc-session` is now a `server-provided` cap resolved
|
|
140
|
+
// by `buildServerProviders`. ──────
|
|
130
141
|
...dynamicCaps,
|
|
131
142
|
// ── Non-cap (core) routers — hand-written, single-impl ──────────
|
|
132
143
|
notifications: (0, notifications_router_js_1.createNotificationsRouter)(services.notificationService),
|
|
@@ -153,35 +164,6 @@ function buildCapabilityRouters(services) {
|
|
|
153
164
|
// offline-node history (Track A "Forget node"); read side is push-only.
|
|
154
165
|
clusterNodes: (0, cluster_nodes_router_js_1.createClusterNodesRouter)(services.agentRegistry),
|
|
155
166
|
auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry),
|
|
156
|
-
// ── Cap overrides: `mount: { kind: 'custom' }` ──────────────────
|
|
157
|
-
// `snapshot-provider.supportsDevice` is an OR across providers;
|
|
158
|
-
// `getSnapshot` picks the first one that claims the device. The
|
|
159
|
-
// generic builder's first-provider/concat collection resolver can't
|
|
160
|
-
// model this — we hand-write the probe + fan-out logic.
|
|
161
|
-
snapshotProvider: (0, generated_cap_routers_1.createCapRouter_snapshotProvider)((_ctx) => {
|
|
162
|
-
const reg = services.capabilityRegistry;
|
|
163
|
-
if (!reg)
|
|
164
|
-
return null;
|
|
165
|
-
const providers = reg.getCollection('snapshot-provider');
|
|
166
|
-
if (!providers || providers.length === 0)
|
|
167
|
-
return null;
|
|
168
|
-
const supportsDevice = (0, cap_mount_helpers_js_1.anySupports)(providers, 'supportsDevice');
|
|
169
|
-
const getSnapshot = (0, cap_mount_helpers_js_1.firstSupported)(providers, 'supportsDevice', 'getSnapshot');
|
|
170
|
-
return { supportsDevice, getSnapshot };
|
|
171
|
-
}),
|
|
172
|
-
// `webrtc-session` — server-detected remote → relay-only. The broker
|
|
173
|
-
// (a forked addon) can't see the HTTP request, so it can't tell a LAN
|
|
174
|
-
// viewer from a remote one. We override `getProvider` to return a
|
|
175
|
-
// per-request provider whose `createSession` carries a server-computed
|
|
176
|
-
// `relayOnly` flag derived from the client IP in `ctx.req`. Remote
|
|
177
|
-
// (CGNAT/4G) viewers force TURN-relay-only ICE; LAN viewers keep the
|
|
178
|
-
// direct host/srflx path. All other methods delegate straight through,
|
|
179
|
-
// and the cross-node remote-proxy routing is preserved (forked/agent-
|
|
180
|
-
// hosted brokers).
|
|
181
|
-
webrtcSession: (0, generated_cap_routers_1.createCapRouter_webrtcSession)((ctx) => {
|
|
182
|
-
const provider = services.capabilityRegistry?.getSingleton('webrtc-session') ?? null;
|
|
183
|
-
return provider ? wrapWebrtcSessionProviderWithRelay(provider, ctx) : null;
|
|
184
|
-
}, (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
185
167
|
// NOT MOUNTED — `mount: { kind: 'skip' }` legacy provider shapes
|
|
186
168
|
// (positional args / sync returns) that don't match the codegen
|
|
187
169
|
// routers' {input}-object + Promise<T> contract. The runtime builder
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.resolveHubClosurePackageDir = resolveHubClosurePackageDir;
|
|
37
37
|
exports.resolveOverlayPackageDir = resolveOverlayPackageDir;
|
|
38
38
|
exports.computeDistBuildId = computeDistBuildId;
|
|
39
|
+
exports.swapDist = swapDist;
|
|
39
40
|
exports.syncFrameworkDist = syncFrameworkDist;
|
|
40
41
|
/**
|
|
41
42
|
* Framework live-sync — make a `camstack deploy` of a framework package
|
|
@@ -202,17 +203,27 @@ function computeDistBuildId(distDir) {
|
|
|
202
203
|
// ---------------------------------------------------------------------------
|
|
203
204
|
// Atomic dist swap (rename-aside)
|
|
204
205
|
// ---------------------------------------------------------------------------
|
|
206
|
+
function isCrossDeviceError(err) {
|
|
207
|
+
return err instanceof Error && err.code === 'EXDEV';
|
|
208
|
+
}
|
|
205
209
|
/**
|
|
206
|
-
* Replace `<targetPkgDir>/dist` with a copy of `sourceDistDir
|
|
210
|
+
* Replace `<targetPkgDir>/dist` with a copy of `sourceDistDir`.
|
|
211
|
+
*
|
|
212
|
+
* 1. COPY `sourceDistDir` → a staging dir INSIDE the target dir. The live
|
|
213
|
+
* `dist/` is untouched, so a mid-copy failure cannot corrupt it.
|
|
214
|
+
* 2. Evict the current `dist/` aside (rename), then rename the staging dir into
|
|
215
|
+
* place. Both renames are metadata ops WITHIN the target fs (atomic). If the
|
|
216
|
+
* final swap fails, the previous `dist/` is restored.
|
|
207
217
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
218
|
+
* OVERLAYFS FALLBACK: the hub-closure target (`/opt/.../@camstack/system`) lives
|
|
219
|
+
* on the container's overlay rootfs. A `dist/` materialized from a LOWER image
|
|
220
|
+
* layer cannot be `rename()`d (overlayfs returns `EXDEV: cross-device link`),
|
|
221
|
+
* so the atomic aside/swap is impossible there. On EXDEV we fall back to a
|
|
222
|
+
* copy-aside restore point + in-place replace. This opens a brief non-atomic
|
|
223
|
+
* window, but it is safe here: the running process already holds the old
|
|
224
|
+
* modules in memory (nothing reads `dist/` until the scheduled restart), and
|
|
225
|
+
* the copy-aside is kept until the replace completes so a failure is
|
|
226
|
+
* recoverable.
|
|
216
227
|
*/
|
|
217
228
|
async function swapDist(sourceDistDir, targetPkgDir) {
|
|
218
229
|
const targetDist = path.join(targetPkgDir, 'dist');
|
|
@@ -227,24 +238,63 @@ async function swapDist(sourceDistDir, targetPkgDir) {
|
|
|
227
238
|
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
228
239
|
throw err;
|
|
229
240
|
}
|
|
230
|
-
// Step 2 — atomic swap within the target fs.
|
|
231
241
|
const hadDist = fs.existsSync(targetDist);
|
|
242
|
+
// How the previous dist was preserved: 'rename' (moved aside, same fs),
|
|
243
|
+
// 'copy' (overlayfs EXDEV — copied aside then removed in place), or 'none'.
|
|
244
|
+
let asideKind = 'none';
|
|
245
|
+
const restorePreviousDist = async () => {
|
|
246
|
+
if (asideKind === 'none' || fs.existsSync(targetDist))
|
|
247
|
+
return;
|
|
248
|
+
if (asideKind === 'rename') {
|
|
249
|
+
await fs.promises.rename(aside, targetDist).catch(() => undefined);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
await fs.promises
|
|
253
|
+
.cp(aside, targetDist, { recursive: true, force: true })
|
|
254
|
+
.catch(() => undefined);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
// Step 2 — evict the current dist. Prefer the atomic rename; fall back to
|
|
258
|
+
// copy-aside + in-place remove on overlayfs EXDEV.
|
|
232
259
|
if (hadDist) {
|
|
233
|
-
|
|
260
|
+
try {
|
|
261
|
+
await fs.promises.rename(targetDist, aside);
|
|
262
|
+
asideKind = 'rename';
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
if (!isCrossDeviceError(err)) {
|
|
266
|
+
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
await fs.promises.cp(targetDist, aside, { recursive: true, force: true });
|
|
270
|
+
await fs.promises.rm(targetDist, { recursive: true, force: true });
|
|
271
|
+
asideKind = 'copy';
|
|
272
|
+
}
|
|
234
273
|
}
|
|
274
|
+
// Step 3 — move the incoming dist into place; fall back to copy on EXDEV.
|
|
235
275
|
try {
|
|
236
276
|
await fs.promises.rename(incoming, targetDist);
|
|
237
277
|
}
|
|
238
|
-
catch (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
278
|
+
catch (err) {
|
|
279
|
+
if (isCrossDeviceError(err)) {
|
|
280
|
+
try {
|
|
281
|
+
await fs.promises.cp(incoming, targetDist, { recursive: true, force: true });
|
|
282
|
+
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
283
|
+
}
|
|
284
|
+
catch (copyErr) {
|
|
285
|
+
await restorePreviousDist();
|
|
286
|
+
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
287
|
+
throw copyErr;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
await restorePreviousDist();
|
|
292
|
+
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
293
|
+
throw err;
|
|
242
294
|
}
|
|
243
|
-
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
244
|
-
throw swapErr;
|
|
245
295
|
}
|
|
246
|
-
// Step
|
|
247
|
-
if (
|
|
296
|
+
// Step 4 — best-effort cleanup of the evicted copy.
|
|
297
|
+
if (asideKind !== 'none') {
|
|
248
298
|
await fs.promises.rm(aside, { recursive: true, force: true }).catch(() => undefined);
|
|
249
299
|
}
|
|
250
300
|
}
|
|
@@ -6,6 +6,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
6
6
|
const system_1 = require("@camstack/system");
|
|
7
7
|
const types_1 = require("@camstack/types");
|
|
8
8
|
const cap_router_runtime_js_1 = require("../../api/trpc/cap-router-runtime.js");
|
|
9
|
+
const core_cap_bridge_js_1 = require("../../api/trpc/core-cap-bridge.js");
|
|
9
10
|
const cap_call_fn_js_1 = require("./cap-call-fn.js");
|
|
10
11
|
const cap_route_authority_js_1 = require("./cap-route-authority.js");
|
|
11
12
|
class MoleculerService {
|
|
@@ -280,6 +281,16 @@ class MoleculerService {
|
|
|
280
281
|
return null;
|
|
281
282
|
return [];
|
|
282
283
|
},
|
|
284
|
+
// Hub-core `$`-service discriminant for the general
|
|
285
|
+
// provider-not-yet-registered recovery. A resolver miss on a NON-core
|
|
286
|
+
// cap (any addon-provided system/singleton cap, e.g.
|
|
287
|
+
// `pipeline-orchestrator`) means its broker-less forked provider hasn't
|
|
288
|
+
// registered YET — the unpinned broker fallback is a dead path that
|
|
289
|
+
// 30s-deadlines into `${cap}.${cap}.${method}`. Feeding the handler this
|
|
290
|
+
// predicate lets it retry the resolver + throw a precise error instead.
|
|
291
|
+
// Derived from the already-maintained core-router list, so a new
|
|
292
|
+
// multi-node addon cap is covered with no change here.
|
|
293
|
+
isCoreServiceCap: (capName) => (0, core_cap_bridge_js_1.isCoreServiceCapName)(capName),
|
|
283
294
|
logger: {
|
|
284
295
|
warn: (msg, meta) => logger.warn(msg, meta !== null && meta !== undefined
|
|
285
296
|
? { meta: meta }
|