@camstack/server 1.1.71 → 1.1.73
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/core/cap-providers.js +0 -11
- package/dist/api/core/lifecycle-job-runner.js +0 -2
- package/dist/api/trpc/cap-mount-helpers.js +21 -9
- package/dist/api/trpc/generated-cap-routers.js +0 -8
- package/dist/api/trpc/trpc.router.js +49 -2
- package/dist/boot/post-boot.service.js +4 -17
- package/dist/boot/reconcile-lifecycle-jobs.js +6 -5
- package/dist/core/addon/addon-package.service.js +17 -388
- package/dist/launcher.js +9 -19
- package/dist/manual-boot.js +0 -27
- package/dist/server-root/index.js +68 -42
- package/package.json +8 -8
- package/dist/boot/resume-framework-swap.js +0 -119
- package/dist/launcher-framework-swap.js +0 -408
- package/dist/request-framework-swap.js +0 -41
|
@@ -1136,17 +1136,6 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1136
1136
|
(0, collection_preference_js_1.persistCollectionDisabled)(configService, input.capName, updated?.disabledProviders ?? []);
|
|
1137
1137
|
return { success: true };
|
|
1138
1138
|
},
|
|
1139
|
-
updateFrameworkPackage: async (input) => ps.updateFrameworkPackage({
|
|
1140
|
-
packageName: input.packageName,
|
|
1141
|
-
...(input.version !== undefined ? { version: input.version } : {}),
|
|
1142
|
-
...(ctx.user?.username !== undefined
|
|
1143
|
-
? { requestedBy: ctx.user.username }
|
|
1144
|
-
: ctx.user?.id !== undefined
|
|
1145
|
-
? { requestedBy: ctx.user.id }
|
|
1146
|
-
: {}),
|
|
1147
|
-
...(input.deferRestart !== undefined ? { deferRestart: input.deferRestart } : {}),
|
|
1148
|
-
runner: lifecycleRunner,
|
|
1149
|
-
}),
|
|
1150
1139
|
getVersions: async (input) => ps.getPackageVersions(input.name),
|
|
1151
1140
|
restartAddon: async (input) => ar.restartAddon(input.addonId),
|
|
1152
1141
|
retryLoad: async (input) => {
|
|
@@ -75,8 +75,6 @@ function createLifecycleJobRunner(deps) {
|
|
|
75
75
|
applyAddonUpdate: deps.applyAddonUpdate,
|
|
76
76
|
emit: deps.emit,
|
|
77
77
|
now: nowFn,
|
|
78
|
-
stageFramework: deps.stageFramework,
|
|
79
|
-
requestFrameworkSwap: deps.requestFrameworkSwap,
|
|
80
78
|
});
|
|
81
79
|
/**
|
|
82
80
|
* Set of jobIds currently executing — used to guard cancellation.
|
|
@@ -48,13 +48,25 @@ function requireDeviceScoped(registry, capName) {
|
|
|
48
48
|
message: `${String(capName)}.${prop}: input must carry numeric "deviceId"`,
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
// Fast path (unchanged): a per-device native provider registered for
|
|
52
|
+
// the exact (cap, deviceId) pair.
|
|
53
|
+
let provider = registry.getNativeProvider(capName, deviceId);
|
|
54
|
+
// D13 fallback: a device cap may be served by a WRAPPER / system
|
|
55
|
+
// provider bound to this device (e.g. the `virtual-doorbell` builtin
|
|
56
|
+
// registers a SINGLETON wrapper — no native). Consult the unified
|
|
57
|
+
// per-device resolver `getProviderForDevice` (native OR device-bound
|
|
58
|
+
// system) BEFORE the synthesize fallback + the final throw. This
|
|
59
|
+
// never runs when a native was found, keeping the fast path intact.
|
|
60
|
+
if (!provider) {
|
|
61
|
+
provider = registry.getProviderForDevice(capName, deviceId);
|
|
62
|
+
}
|
|
63
|
+
if (!provider) {
|
|
64
|
+
// No native and no device-bound provider. For `getStatus`, the
|
|
65
|
+
// device may still have a SYNTHESIZED status from device-links
|
|
66
|
+
// (battery/consumables wired from sibling sensors). Consult the
|
|
67
|
+
// resolver with a null base (the synthesize path) before failing —
|
|
68
|
+
// it returns null when the device has no links for this cap,
|
|
69
|
+
// preserving today's error.
|
|
58
70
|
if (prop === 'getStatus') {
|
|
59
71
|
const deviceManager = registry.getSingleton('device-manager');
|
|
60
72
|
const synthesized = await deviceManager?.resolveLinkedStatus?.({
|
|
@@ -70,14 +82,14 @@ function requireDeviceScoped(registry, capName) {
|
|
|
70
82
|
message: `Capability "${String(capName)}" not registered for device ${deviceId}`,
|
|
71
83
|
});
|
|
72
84
|
}
|
|
73
|
-
const fn =
|
|
85
|
+
const fn = provider[prop];
|
|
74
86
|
if (typeof fn !== 'function') {
|
|
75
87
|
throw new server_1.TRPCError({
|
|
76
88
|
code: 'NOT_IMPLEMENTED',
|
|
77
89
|
message: `Capability "${String(capName)}" provider for device ${deviceId} does not implement "${prop}"`,
|
|
78
90
|
});
|
|
79
91
|
}
|
|
80
|
-
const result = await fn.call(
|
|
92
|
+
const result = await fn.call(provider, input);
|
|
81
93
|
// Device-property-wiring overlay (read-time): only `getStatus`, and only
|
|
82
94
|
// when the device has links for this cap (resolveLinkedStatus returns
|
|
83
95
|
// null otherwise → base result untouched). One in-process singleton hop.
|
|
@@ -605,14 +605,6 @@ function createCapRouter_addons(getProvider, _createRemoteProxy) {
|
|
|
605
605
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
606
606
|
return p.setCapabilityProviderEnabled(input);
|
|
607
607
|
}),
|
|
608
|
-
updateFrameworkPackage: trpc_middleware_js_1.adminProcedure
|
|
609
|
-
.input(types_10.addonsCapability.methods.updateFrameworkPackage.input.loose())
|
|
610
|
-
.output(types_10.addonsCapability.methods.updateFrameworkPackage.output)
|
|
611
|
-
.mutation(async ({ input, ctx }) => {
|
|
612
|
-
const p = requireCapProvider('addons', () => getProvider(ctx));
|
|
613
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
614
|
-
return p.updateFrameworkPackage(input);
|
|
615
|
-
}),
|
|
616
608
|
getVersions: trpc_middleware_js_1.protectedProcedure
|
|
617
609
|
.input(types_10.addonsCapability.methods.getVersions.input.loose())
|
|
618
610
|
.output(types_10.addonsCapability.methods.getVersions.output)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.enrichInputWithUserAgent = enrichInputWithUserAgent;
|
|
4
4
|
exports.enrichInputWithRelayClass = enrichInputWithRelayClass;
|
|
5
|
+
exports.enrichHintsWithClientClass = enrichHintsWithClientClass;
|
|
5
6
|
exports.wrapWebrtcSessionProviderWithRelay = wrapWebrtcSessionProviderWithRelay;
|
|
6
7
|
exports.buildAppRouter = buildAppRouter;
|
|
7
8
|
const addon_settings_router_js_1 = require("../core/addon-settings.router.js");
|
|
@@ -87,6 +88,52 @@ function enrichInputWithRelayClass(input, clientClass) {
|
|
|
87
88
|
const relayOnly = FORCE_RELAY_REMOTE ? (0, client_ip_js_1.deriveRelayOnly)(clientClass) : false;
|
|
88
89
|
return { ...input, relayOnly };
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Initial adaptive rung for a genuinely-REMOTE viewer.
|
|
93
|
+
*
|
|
94
|
+
* A `remote` (public / CGNAT-4G / internet) client cannot reliably receive the
|
|
95
|
+
* high tier's large keyframe over a lossy relay path — LAN can, which is why
|
|
96
|
+
* adaptive works on LAN but a remote adaptive viewer sees a permanent black
|
|
97
|
+
* frame (ICE/DTLS up, no decodable frame). So a remote adaptive session STARTS
|
|
98
|
+
* on the smallest tier; the broker's `AdaptiveController` upgrades from there
|
|
99
|
+
* once the link proves healthy. `lan` and `vpn` (Tailscale, direct overlay)
|
|
100
|
+
* keep the full-quality default and are never biased.
|
|
101
|
+
*/
|
|
102
|
+
const REMOTE_INITIAL_ADAPTIVE_TIER = 'low';
|
|
103
|
+
/**
|
|
104
|
+
* Bias the INITIAL adaptive-tier selection for a genuinely-remote viewer by
|
|
105
|
+
* injecting a `prefersTier` hint the broker's `selectBestBroker` already
|
|
106
|
+
* honours. The hub is the only layer that knows the client's network class
|
|
107
|
+
* (the forked broker cannot see the HTTP request), so — exactly like
|
|
108
|
+
* `relayOnly` and the User-Agent — the class is read here and threaded down
|
|
109
|
+
* through the EXISTING `hints` cap field (no cap/schema change: `prefersTier`
|
|
110
|
+
* is already part of `webrtcClientHintsSchema`).
|
|
111
|
+
*
|
|
112
|
+
* Applied ONLY when:
|
|
113
|
+
* - the client class is `remote` (lan/vpn keep the full-quality default), and
|
|
114
|
+
* - the target is adaptive (`handleOffer` defaults an absent target to
|
|
115
|
+
* adaptive; a pinned profile/cam-stream is the operator's explicit choice
|
|
116
|
+
* and passes through untouched), and
|
|
117
|
+
* - the caller did not already set `prefersTier` (no client does today, but
|
|
118
|
+
* keep the trusted-injection rule symmetric with relayOnly/userAgent).
|
|
119
|
+
*
|
|
120
|
+
* Immutable — builds a NEW input (and a new `hints`), never mutates the
|
|
121
|
+
* caller's. On a downgrade re-offer the broker's controller-chosen tier intent
|
|
122
|
+
* overrides this hint, so a healthy remote session can still climb above `low`.
|
|
123
|
+
*/
|
|
124
|
+
function enrichHintsWithClientClass(input, clientClass) {
|
|
125
|
+
if (clientClass !== 'remote')
|
|
126
|
+
return input;
|
|
127
|
+
const isAdaptive = input.target === undefined || input.target.kind === 'adaptive';
|
|
128
|
+
if (!isAdaptive)
|
|
129
|
+
return input;
|
|
130
|
+
if (input.hints?.prefersTier !== undefined)
|
|
131
|
+
return input;
|
|
132
|
+
return {
|
|
133
|
+
...input,
|
|
134
|
+
hints: { ...input.hints, prefersTier: REMOTE_INITIAL_ADAPTIVE_TIER },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
90
137
|
/**
|
|
91
138
|
* Per-request wrapper around the resolved `webrtc-session` broker singleton.
|
|
92
139
|
*
|
|
@@ -125,8 +172,8 @@ function wrapWebrtcSessionProviderWithRelay(provider, ctx) {
|
|
|
125
172
|
const clientClass = (0, client_ip_js_1.classifyClientRequest)(ctx.req);
|
|
126
173
|
return {
|
|
127
174
|
...provider,
|
|
128
|
-
createSession: (input) => provider.createSession(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass)),
|
|
129
|
-
handleOffer: (input) => provider.handleOffer(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass)),
|
|
175
|
+
createSession: (input) => provider.createSession(enrichHintsWithClientClass(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass), clientClass)),
|
|
176
|
+
handleOffer: (input) => provider.handleOffer(enrichHintsWithClientClass(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass), clientClass)),
|
|
130
177
|
};
|
|
131
178
|
}
|
|
132
179
|
/**
|
|
@@ -4,7 +4,6 @@ exports.PostBootService = void 0;
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const system_1 = require("@camstack/system");
|
|
6
6
|
const types_1 = require("@camstack/types");
|
|
7
|
-
const resume_framework_swap_js_1 = require("./resume-framework-swap.js");
|
|
8
7
|
const reconcile_lifecycle_jobs_js_1 = require("./reconcile-lifecycle-jobs.js");
|
|
9
8
|
class PostBootService {
|
|
10
9
|
eventBus;
|
|
@@ -57,24 +56,12 @@ class PostBootService {
|
|
|
57
56
|
// restart, …). `readPendingRestart` clears the marker atomically so
|
|
58
57
|
// we never re-fire on a crash-loop boot.
|
|
59
58
|
this.emitRestartCompletedIfPending(dataPath);
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// healthy update).
|
|
65
|
-
await this.resumeFrameworkSwapIfPending(dataPath);
|
|
66
|
-
// Then re-drive every other non-terminal lifecycle job (addon task phases)
|
|
67
|
-
// from its on-disk checkpoint. Order matters: framework resume FIRST (marks
|
|
68
|
-
// the framework task `applied`→`done`), THEN addon reconcile (which leaves
|
|
69
|
-
// framework tasks untouched).
|
|
59
|
+
// Re-drive every non-terminal lifecycle job (addon task phases) from its
|
|
60
|
+
// on-disk checkpoint. Framework tasks are no longer applied by the engine
|
|
61
|
+
// (single-copy collapse — framework updates ship via applyServerUpdate);
|
|
62
|
+
// any stray framework task is skipped during reconcile.
|
|
70
63
|
await this.reconcileLifecycleJobsIfAny();
|
|
71
64
|
}
|
|
72
|
-
async resumeFrameworkSwapIfPending(dataDir) {
|
|
73
|
-
const result = await (0, resume_framework_swap_js_1.resumeFrameworkSwapJob)(dataDir);
|
|
74
|
-
if (result.resumed) {
|
|
75
|
-
this.logger.info('Framework update completed', { meta: { jobId: result.jobId } });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
65
|
async reconcileLifecycleJobsIfAny() {
|
|
79
66
|
const { resumed, failed } = await (0, reconcile_lifecycle_jobs_js_1.reconcileLifecycleJobsAtBoot)();
|
|
80
67
|
if (resumed + failed > 0) {
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
* resumes from its checkpoint (reusing a still-valid `stagedPath` or
|
|
9
9
|
* re-fetching, then applying).
|
|
10
10
|
*
|
|
11
|
-
* Best-effort
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Best-effort: this NEVER throws — any error (runner not initialized, journal
|
|
12
|
+
* corruption, …) is swallowed and reported as `{ resumed: 0, failed: 0 }` so the
|
|
13
|
+
* post-boot path can never crash the hub.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Framework tasks are no longer applied by the engine (single-copy collapse —
|
|
16
|
+
* framework updates ship via `applyServerUpdate`); any stray non-terminal
|
|
17
|
+
* framework task is marked `skipped` during reconcile so the job can finalize.
|
|
17
18
|
*/
|
|
18
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
20
|
exports.reconcileLifecycleJobsAtBoot = reconcileLifecycleJobsAtBoot;
|