@camstack/server 1.2.113 → 1.2.114

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.
@@ -4171,6 +4171,14 @@ function createCapRouter_llm(getProvider, _createRemoteProxy) {
4171
4171
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4172
4172
  return p.listNodeModels(input);
4173
4173
  }),
4174
+ resolveModelRef: trpc_middleware_js_1.adminProcedure
4175
+ .input(types_55.llmCapability.methods.resolveModelRef.input.loose())
4176
+ .output(types_55.llmCapability.methods.resolveModelRef.output)
4177
+ .mutation(async ({ input, ctx }) => {
4178
+ const p = requireCapProvider('llm', () => getProvider(ctx));
4179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4180
+ return p.resolveModelRef(input);
4181
+ }),
4174
4182
  installModel: trpc_middleware_js_1.adminProcedure
4175
4183
  .input(types_55.llmCapability.methods.installModel.input.loose())
4176
4184
  .output(types_55.llmCapability.methods.installModel.output)
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * The hub's `onModuleInit` order, as one named, testable sequence.
4
+ *
5
+ * It was inline in `bootManual`. It is here because one seam in it is now
6
+ * load-bearing rather than incidental — {@link BootInitSequence.beforeAddonBoot}
7
+ * — and a seam nothing pins is a seam that moves.
8
+ *
9
+ * The order is derived from the dependency graph:
10
+ *
11
+ * 1. Moleculer — starts the broker and calls `EventBus.attachBroker()`.
12
+ * 2. AddonBridge — no cross-service deps.
13
+ * 3. AgentRegistry — subscribes to `broker.localBus.on('$node.connected')`.
14
+ * ── beforeAddonBoot ── the broker is live and NOTHING has booted yet.
15
+ * 4. AddonRegistry — boots EVERY addon. Minutes of work on a real hub.
16
+ * 5. …post-boot reconciles.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.runBootInitSequence = runBootInitSequence;
20
+ /** Run the boot init steps in dependency order. */
21
+ async function runBootInitSequence(sequence) {
22
+ await sequence.moleculer();
23
+ await sequence.addonBridge();
24
+ await sequence.agentRegistry();
25
+ if (sequence.beforeAddonBoot !== undefined)
26
+ await sequence.beforeAddonBoot();
27
+ await sequence.addonBoot();
28
+ if (sequence.afterAddonBoot !== undefined)
29
+ await sequence.afterAddonBoot();
30
+ }
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access -- pre-existing lint debt across this 2200-line orchestration class. The flagged sites (StorageService.setLocationManager / setSettingsBackend, LoggingService.addDestination, RouteRegistry, etc.) are typed as `unknown` by their owning services to break circular construction-order dependencies; runtime contracts are validated structurally. Tracked separately; do not amend in unrelated edits. */
2
3
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
4
  if (k2 === undefined) k2 = k;
4
5
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -36,29 +37,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
37
  exports.AddonRegistryService = void 0;
37
38
  exports.shouldEvictMissingOnDisk = shouldEvictMissingOnDisk;
38
39
  exports.shouldEmitProviderRegisteredReady = shouldEmitProviderRegisteredReady;
39
- /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access -- pre-existing lint debt across this 2200-line orchestration class. The flagged sites (StorageService.setLocationManager / setSettingsBackend, LoggingService.addDestination, RouteRegistry, etc.) are typed as `unknown` by their owning services to break circular construction-order dependencies; runtime contracts are validated structurally. Tracked separately; do not amend in unrelated edits. */
40
- const os = __importStar(require("node:os"));
41
- const addon_row_manifest_1 = require("./addon-row-manifest");
42
- const runner_convergence_1 = require("./runner-convergence");
43
- const package_dir_utils_1 = require("./package-dir-utils");
44
- const types_1 = require("@camstack/types");
45
- const prune_misplaced_addons_js_1 = require("./prune-misplaced-addons.js");
46
- const system_1 = require("@camstack/system");
47
- const system_2 = require("@camstack/system");
48
- const client_1 = require("@trpc/client");
49
- const system_3 = require("@camstack/system");
50
- const system_4 = require("@camstack/system");
51
- const system_5 = require("@camstack/system");
52
40
  const node_crypto_1 = require("node:crypto");
53
- const path = __importStar(require("node:path"));
54
41
  const fs = __importStar(require("node:fs"));
42
+ const os = __importStar(require("node:os"));
43
+ const path = __importStar(require("node:path"));
55
44
  const node_url_1 = require("node:url");
56
- const require_cache_js_1 = require("./require-cache.js");
45
+ const system_1 = require("@camstack/system");
46
+ const types_1 = require("@camstack/types");
47
+ const client_1 = require("@trpc/client");
48
+ const addon_call_gateway_js_1 = require("./addon-call-gateway.js");
49
+ const addon_row_manifest_1 = require("./addon-row-manifest");
57
50
  const addon_settings_provider_js_1 = require("./addon-settings-provider.js");
58
51
  const integration_visibility_js_1 = require("./integration-visibility.js");
59
- const addon_call_gateway_js_1 = require("./addon-call-gateway.js");
60
- const system_6 = require("@camstack/system");
61
- const types_2 = require("@camstack/types");
52
+ const package_dir_utils_1 = require("./package-dir-utils");
53
+ const prune_misplaced_addons_js_1 = require("./prune-misplaced-addons.js");
54
+ const require_cache_js_1 = require("./require-cache.js");
55
+ const runner_convergence_1 = require("./runner-convergence");
56
+ const runner_spawn_fanout_js_1 = require("./runner-spawn-fanout.js");
62
57
  /**
63
58
  * Decide whether an in-memory addon entry should be evicted because it is no
64
59
  * longer on disk (the uninstall path in `loadNewAddons`).
@@ -222,6 +217,11 @@ class AddonRegistryService {
222
217
  // each addon's initialize() (in-process path); Task 7.2 will dispatch
223
218
  // through this from the `api.addons.custom` tRPC procedure.
224
219
  customActionRegistry = new system_1.CustomActionRegistry();
220
+ /**
221
+ * Keeps `purgeRequireCacheUnder` + `import()` atomic per addon directory now
222
+ * that the boot runner plan overlaps (D171). See `runner-spawn-fanout.ts`.
223
+ */
224
+ moduleLoadSerializer = (0, runner_spawn_fanout_js_1.createKeyedSerializer)();
225
225
  /**
226
226
  * AddonIds whose group-runner disconnect is operator-initiated (update /
227
227
  * restart / uninstall). The Moleculer `$node.disconnected` handler skips
@@ -267,7 +267,7 @@ class AddonRegistryService {
267
267
  getByName: (capName) => reg.getSingleton(capName) ?? reg.getAllProviders(capName)[0] ?? null,
268
268
  };
269
269
  const client = (0, client_1.createTRPCClient)({
270
- links: [(0, system_2.localProviderLink)(resolver), (0, system_2.brokerTransportLink)(this.moleculer.broker)],
270
+ links: [(0, system_1.localProviderLink)(resolver), (0, system_1.brokerTransportLink)(this.moleculer.broker)],
271
271
  });
272
272
  this.brokerApi = client;
273
273
  }
@@ -399,7 +399,7 @@ class AddonRegistryService {
399
399
  // Register the addon-settings singleton provider — replaces the
400
400
  // former `$addonHost` Moleculer service. The provider resolves
401
401
  // addonId → local addon instance (hub) or remote Moleculer call.
402
- this.capabilityRegistry.declareCapability(system_6.addonSettingsCapability);
402
+ this.capabilityRegistry.declareCapability(system_1.addonSettingsCapability);
403
403
  // The SINGLE router for addon-level calls (routes / custom / settings).
404
404
  // It classifies in-process | hub-local-child (UDS) | remote-agent
405
405
  // (Moleculer) in ONE place — `resolveNode` reports only the BASE node
@@ -681,8 +681,16 @@ class AddonRegistryService {
681
681
  // `addon.error` — there is NO in-process-on-the-hub fallback (it
682
682
  // would violate one-addon-one-process). Retry policy for a failed
683
683
  // spawn is governed by the kernel circuit-breaker.
684
+ //
685
+ // BOUNDED-PARALLEL (D171). Entries within a plan are independent by
686
+ // construction — `buildAddonGroupPlan` keys by runner id and nothing reads
687
+ // the iteration order — while each one costs a fork plus a full addon
688
+ // bundle evaluated on the hub's event loop. Serially that was ~4 s of the
689
+ // measured 10 s kernel boot and grew with every addon shipped. The ordering
690
+ // that IS load-bearing is BETWEEN the plans (see the two call sites), not
691
+ // inside one.
684
692
  const spawnRunnerPlan = async (plan) => {
685
- for (const [runnerId, runnerAddons] of plan) {
693
+ await (0, runner_spawn_fanout_js_1.spawnPlanConcurrently)(plan, async (runnerId, runnerAddons) => {
686
694
  try {
687
695
  await this.initializeAddonGroup(runnerId, runnerAddons);
688
696
  for (const { addonId } of runnerAddons) {
@@ -701,7 +709,7 @@ class AddonRegistryService {
701
709
  meta: { runnerId, addonIds: runnerAddons.map((a) => a.addonId), error: msg },
702
710
  });
703
711
  }
704
- }
712
+ });
705
713
  };
706
714
  // An ISOLATED builtin is a builtin: everything it stores lives behind
707
715
  // `settings-store`, which is one of the in-process builtins booted below.
@@ -2420,7 +2428,7 @@ class AddonRegistryService {
2420
2428
  const store = this.capabilityRegistry.getProviderByAddon('settings-store', addonId);
2421
2429
  if (!store)
2422
2430
  return;
2423
- this.integrationRegistry = new system_4.IntegrationRegistry(store);
2431
+ this.integrationRegistry = new system_1.IntegrationRegistry(store);
2424
2432
  void this.integrationRegistry.initialize().then(() => {
2425
2433
  this.logger.info('IntegrationRegistry initialized', { meta: { phase: 'v2' } });
2426
2434
  });
@@ -2822,7 +2830,7 @@ class AddonRegistryService {
2822
2830
  registry.dataPlaneRegistry?.registerAddon(id, endpoints);
2823
2831
  },
2824
2832
  };
2825
- const dataPlaneFacility = (0, system_5.createAddonDataPlaneFacility)({
2833
+ const dataPlaneFacility = (0, system_1.createAddonDataPlaneFacility)({
2826
2834
  addonId,
2827
2835
  logger,
2828
2836
  sink: dataPlaneSink,
@@ -2859,7 +2867,7 @@ class AddonRegistryService {
2859
2867
  return registry.capabilityRegistry.getSingleton(capName) ?? undefined;
2860
2868
  },
2861
2869
  },
2862
- deps: new system_3.AddonDepsManager(dataDir, logger),
2870
+ deps: new system_1.AddonDepsManager(dataDir, logger),
2863
2871
  kernel: {
2864
2872
  localNodeId: this.broker.nodeID,
2865
2873
  storage: storageProvider ?? undefined,
@@ -2936,7 +2944,7 @@ class AddonRegistryService {
2936
2944
  let chain = this.disposerChains.get(addonId);
2937
2945
  if (chain == null) {
2938
2946
  const log = this.loggingService.createLogger().withTags({ addonId });
2939
- chain = new types_2.DisposerChain({
2947
+ chain = new types_1.DisposerChain({
2940
2948
  onError: (err, index) => {
2941
2949
  log.error(`Disposer #${index} threw during teardown`, {
2942
2950
  meta: { error: (0, types_1.errMsg)(err) },
@@ -3248,17 +3256,26 @@ class AddonRegistryService {
3248
3256
  // Dropping the addon's own modules from the require cache is what
3249
3257
  // actually re-reads the bundle — see `require-cache.ts` for the scoping
3250
3258
  // and the real-path match Node's cache keys demand.
3251
- (0, require_cache_js_1.purgeRequireCacheUnder)(path.dirname(entryPath));
3252
- // Kept for a genuinely ESM addon entry, where it IS the mechanism.
3253
- const cacheBustedUrl = `${(0, node_url_1.pathToFileURL)(entryPath).href}?t=${Date.now()}`;
3254
- // A plain `await import()` here is downleveled by tsc (the backend builds
3255
- // with `module: CommonJS`) into a `require()`-based shim. `require()` then
3256
- // treats the `?t=…` cache-bust query as part of the literal filename and
3257
- // throws MODULE_NOT_FOUND for EVERY forked addon — so no forked addon's
3258
- // custom-action catalog ever loaded. Use a native dynamic import the
3259
- // compiler won't rewrite, so Node's ESM loader handles the query string.
3260
- const nativeImport = new Function('specifier', 'return import(specifier)');
3261
- const modUnknown = await nativeImport(cacheBustedUrl);
3259
+ //
3260
+ // The purge mutates the PROCESS-GLOBAL require cache, so purge-then-import
3261
+ // must stay atomic per directory now that the boot plan overlaps (D171):
3262
+ // `@camstack/addon-remote-storage` ships three addon ids whose entries all
3263
+ // resolve into one `dist/`, and a sibling's purge landing mid-import would
3264
+ // evict a module graph while it is being evaluated. Different directories
3265
+ // still load concurrently.
3266
+ const modUnknown = await this.moduleLoadSerializer.run(path.dirname(entryPath), async () => {
3267
+ (0, require_cache_js_1.purgeRequireCacheUnder)(path.dirname(entryPath));
3268
+ // Kept for a genuinely ESM addon entry, where it IS the mechanism.
3269
+ const cacheBustedUrl = `${(0, node_url_1.pathToFileURL)(entryPath).href}?t=${Date.now()}`;
3270
+ // A plain `await import()` here is downleveled by tsc (the backend builds
3271
+ // with `module: CommonJS`) into a `require()`-based shim. `require()` then
3272
+ // treats the `?t=…` cache-bust query as part of the literal filename and
3273
+ // throws MODULE_NOT_FOUND for EVERY forked addon — so no forked addon's
3274
+ // custom-action catalog ever loaded. Use a native dynamic import the
3275
+ // compiler won't rewrite, so Node's ESM loader handles the query string.
3276
+ const nativeImport = new Function('specifier', 'return import(specifier)');
3277
+ return nativeImport(cacheBustedUrl);
3278
+ });
3262
3279
  catalog =
3263
3280
  modUnknown && typeof modUnknown === 'object'
3264
3281
  ? modUnknown['customActions']
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * What makes the boot runner plan safe to OVERLAP.
4
+ *
5
+ * `spawnRunnerPlan` used to be `for (…of plan) await initializeAddonGroup(…)`.
6
+ * The fork itself is cheap — `$process.spawnRunner` returns as soon as
7
+ * `child_process.spawn` hands back a pid, and the child's capabilities land
8
+ * later on its INFO heartbeat — but every iteration ALSO does a dynamic
9
+ * `import()` of the addon's built bundle inside the hub process, purely to read
10
+ * its `customActions` export. Forty runners' module graphs, evaluated one after
11
+ * another, were ~4 s of the measured 10 s kernel boot, and it grows linearly
12
+ * with the addon count (docs/design/boot-to-usable-camera.md, item B).
13
+ *
14
+ * Nothing in a plan is ordered: `buildAddonGroupPlan` keys by runner id and
15
+ * `resolveRunnerId` is `execution.group ?? addonId`, so entries are independent
16
+ * by construction. The ordering that IS load-bearing lives BETWEEN the plans
17
+ * (forked addons → in-process builtins → isolated builtins) and is untouched.
18
+ *
19
+ * Two primitives, because overlapping the loop exposes exactly one shared
20
+ * resource: Node's `require.cache`.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.RUNNER_SPAWN_CONCURRENCY = void 0;
24
+ exports.spawnPlanConcurrently = spawnPlanConcurrently;
25
+ exports.createKeyedSerializer = createKeyedSerializer;
26
+ /**
27
+ * Concurrent runner spawns during boot.
28
+ *
29
+ * Each unit is a `child_process.spawn` (kernel work) plus a full CJS module
30
+ * graph evaluated ON the hub's event loop. The bound protects the hub, not the
31
+ * code: unbounded, forty simultaneous module evaluations would starve the very
32
+ * event loop that has to answer the children's UDS handshakes. Eight keeps the
33
+ * fork syscalls overlapped while never leaving more than eight bundles being
34
+ * parsed at once.
35
+ */
36
+ exports.RUNNER_SPAWN_CONCURRENCY = 8;
37
+ /**
38
+ * Run `spawnOne` over every plan entry with at most `concurrency` in flight.
39
+ *
40
+ * Same lane-pool shape as `BaseDeviceProvider.onRestoreDevices` and
41
+ * `hydrateDevicesFromStorage` (D167) — one queue, N lanes, each lane pulling
42
+ * the next index until the queue is drained. `spawnOne` owns its own failure
43
+ * handling; a unit that settles (however it settles) frees its lane.
44
+ */
45
+ async function spawnPlanConcurrently(plan, spawnOne, concurrency = exports.RUNNER_SPAWN_CONCURRENCY) {
46
+ const queue = [...plan.entries()];
47
+ if (queue.length === 0)
48
+ return;
49
+ let next = 0;
50
+ const lane = async () => {
51
+ for (;;) {
52
+ const unit = queue[next++];
53
+ if (unit === undefined)
54
+ return;
55
+ await spawnOne(unit[0], unit[1]);
56
+ }
57
+ };
58
+ const lanes = Math.min(Math.max(1, concurrency), queue.length);
59
+ await Promise.all(Array.from({ length: lanes }, lane));
60
+ }
61
+ /**
62
+ * Serialise work that shares a key, while different keys run freely.
63
+ *
64
+ * THE reason this exists: loading a forked addon's custom-action catalog first
65
+ * calls `purgeRequireCacheUnder(dirname(entryPath))`, which mutates the
66
+ * PROCESS-GLOBAL `require.cache`. Serially that is always purge-then-import.
67
+ * Overlapped, one addon's purge can fire while a sibling's `import()` of the
68
+ * same directory is still resolving, evicting half of a module graph that is
69
+ * being evaluated — duplicate module instances, or a partially-purged one.
70
+ *
71
+ * It is not hypothetical: `@camstack/addon-remote-storage` ships three addon
72
+ * ids (`sftp-storage`, `s3-storage`, `webdav-storage`) whose entries all
73
+ * resolve into the same `dist/` directory, each on its own runner, all in the
74
+ * same plan.
75
+ *
76
+ * Keyed by the purge scope, so the bound above still applies across packages
77
+ * and only true siblings queue behind each other.
78
+ */
79
+ function createKeyedSerializer() {
80
+ const chains = new Map();
81
+ return {
82
+ run(key, work) {
83
+ const previous = chains.get(key) ?? Promise.resolve();
84
+ // `.then(work, work)` — a predecessor that REJECTED must not cancel its
85
+ // successors, and must not leave the chain permanently rejected.
86
+ const result = previous.then(work, work);
87
+ // The chain link swallows the outcome; only the caller sees it.
88
+ const link = result.then(() => undefined, () => undefined);
89
+ chains.set(key, link);
90
+ void link.then(() => {
91
+ // Drop the key once this link is the tail — keeps the map from growing
92
+ // one entry per addon directory for the life of the process.
93
+ if (chains.get(key) === link)
94
+ chains.delete(key);
95
+ });
96
+ return result;
97
+ },
98
+ };
99
+ }
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ /* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument -- pre-existing lint debt across this 800+ line bootstrap module. The flagged sites cross typed boundaries (Fastify request typing, AddonRouteRegistry, AuthService inherited methods) where the projectService context can't trace inheritance chains. Tracked separately; do not amend in unrelated edits. */
2
3
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
4
  if (k2 === undefined) k2 = k;
4
5
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -36,23 +37,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
38
  };
38
39
  Object.defineProperty(exports, "__esModule", { value: true });
39
- /* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument -- pre-existing lint debt across this 800+ line bootstrap module. The flagged sites cross typed boundaries (Fastify request typing, AddonRouteRegistry, AuthService inherited methods) where the projectService context can't trace inheritance chains. Tracked separately; do not amend in unrelated edits. */
40
- const fastify_1 = require("@trpc/server/adapters/fastify");
41
- const ws_1 = require("@trpc/server/adapters/ws");
42
- const compress_1 = __importDefault(require("@fastify/compress"));
43
- const cookie_1 = __importDefault(require("@fastify/cookie"));
44
- const sse_no_compression_js_1 = require("./api/sse-no-compression.js");
45
- const ws_2 = require("ws");
40
+ const node_child_process_1 = require("node:child_process");
46
41
  const fs = __importStar(require("node:fs"));
47
42
  const path = __importStar(require("node:path"));
48
- const node_child_process_1 = require("node:child_process");
49
- const single_copy_cleanup_runner_js_1 = require("./single-copy-cleanup-runner.js");
50
- const logging_service_1 = require("./core/logging/logging.service");
51
- const event_bus_service_1 = require("./core/events/event-bus.service");
52
- const config_service_1 = require("./core/config/config.service");
53
- const auth_service_1 = require("./core/auth/auth.service");
54
- const share_token_service_1 = require("./core/auth/share-token.service");
55
- const handoff_code_service_1 = require("./core/auth/handoff-code.service");
56
43
  // Boot-time capability declaration runs over the auto-generated
57
44
  // `ALL_CAPABILITY_DEFINITIONS` array — every `*.cap.ts` file that ships
58
45
  // with `@camstack/types` is included automatically. Adding a new cap
@@ -64,39 +51,50 @@ const handoff_code_service_1 = require("./core/auth/handoff-code.service");
64
51
  // capability surface refreshes on `camstack deploy packages/system` with no new
65
52
  // server image. `@camstack/types` is never deployed to the hub.
66
53
  const system_1 = require("@camstack/system");
67
- const stream_probe_service_1 = require("./core/streaming/stream-probe.service");
68
- const feature_service_1 = require("./core/feature/feature.service");
69
- const agent_registry_service_1 = require("./core/agent/agent-registry.service");
70
- const moleculer_service_1 = require("./core/moleculer/moleculer.service");
54
+ const compress_1 = __importDefault(require("@fastify/compress"));
55
+ const cookie_1 = __importDefault(require("@fastify/cookie"));
56
+ const fastify_1 = require("@trpc/server/adapters/fastify");
57
+ const ws_1 = require("@trpc/server/adapters/ws");
58
+ const ws_2 = require("ws");
59
+ const addon_upload_1 = require("./api/addon-upload");
60
+ const auth_whoami_1 = require("./api/auth-whoami");
61
+ const health_routes_1 = require("./api/health/health.routes");
71
62
  const model_distributor_js_1 = require("./api/model-distributor.js");
72
- const addon_registry_service_1 = require("./core/addon/addon-registry.service");
73
- const addon_package_service_1 = require("./core/addon/addon-package.service");
74
- const repl_engine_service_1 = require("./core/repl/repl-engine.service");
75
- const network_quality_service_1 = require("./core/network/network-quality.service");
76
- const storage_service_1 = require("./core/storage/storage.service");
77
- const addon_bridge_service_1 = require("./core/addon-bridge/addon-bridge.service");
78
- const addon_pages_service_1 = require("./core/addon-pages/addon-pages.service");
79
- const addon_widgets_service_1 = require("./core/addon-widgets/addon-widgets.service");
80
- const trpc_router_1 = require("./api/trpc/trpc.router");
63
+ const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
64
+ const server_upload_1 = require("./api/server-upload");
65
+ const sse_no_compression_js_1 = require("./api/sse-no-compression.js");
66
+ const precompressed_asset_js_1 = require("./api/static/precompressed-asset.js");
67
+ const spa_static_1 = require("./api/static/spa-static");
81
68
  const core_cap_bridge_1 = require("./api/trpc/core-cap-bridge");
82
69
  const trpc_context_1 = require("./api/trpc/trpc.context");
70
+ const trpc_router_1 = require("./api/trpc/trpc.router");
83
71
  const trpc_error_principal_1 = require("./api/trpc/trpc-error-principal");
84
- const addon_upload_1 = require("./api/addon-upload");
85
- const server_upload_1 = require("./api/server-upload");
86
- const auth_whoami_1 = require("./api/auth-whoami");
87
- const system_2 = require("@camstack/system");
88
- const session_cookie_js_1 = require("./auth/session-cookie.js");
89
72
  const addon_route_jwt_gate_js_1 = require("./auth/addon-route-jwt-gate.js");
90
- const health_routes_1 = require("./api/health/health.routes");
91
- const spa_static_1 = require("./api/static/spa-static");
92
- const precompressed_asset_js_1 = require("./api/static/precompressed-asset.js");
93
- const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
94
- const system_3 = require("@camstack/system");
73
+ const session_cookie_js_1 = require("./auth/session-cookie.js");
95
74
  const boot_config_1 = require("./boot/boot-config");
96
- const manual_boot_1 = require("./manual-boot");
75
+ const integration_id_backfill_1 = require("./boot/integration-id-backfill");
97
76
  const post_boot_service_1 = require("./boot/post-boot.service");
77
+ const addon_package_service_1 = require("./core/addon/addon-package.service");
78
+ const addon_registry_service_1 = require("./core/addon/addon-registry.service");
79
+ const addon_bridge_service_1 = require("./core/addon-bridge/addon-bridge.service");
80
+ const addon_pages_service_1 = require("./core/addon-pages/addon-pages.service");
81
+ const addon_widgets_service_1 = require("./core/addon-widgets/addon-widgets.service");
82
+ const agent_registry_service_1 = require("./core/agent/agent-registry.service");
83
+ const auth_service_1 = require("./core/auth/auth.service");
84
+ const handoff_code_service_1 = require("./core/auth/handoff-code.service");
85
+ const share_token_service_1 = require("./core/auth/share-token.service");
86
+ const config_service_1 = require("./core/config/config.service");
87
+ const event_bus_service_1 = require("./core/events/event-bus.service");
88
+ const feature_service_1 = require("./core/feature/feature.service");
89
+ const logging_service_1 = require("./core/logging/logging.service");
90
+ const moleculer_service_1 = require("./core/moleculer/moleculer.service");
91
+ const network_quality_service_1 = require("./core/network/network-quality.service");
92
+ const repl_engine_service_1 = require("./core/repl/repl-engine.service");
98
93
  const server_update_service_1 = require("./core/server-update/server-update.service");
99
- const integration_id_backfill_1 = require("./boot/integration-id-backfill");
94
+ const storage_service_1 = require("./core/storage/storage.service");
95
+ const stream_probe_service_1 = require("./core/streaming/stream-probe.service");
96
+ const manual_boot_1 = require("./manual-boot");
97
+ const single_copy_cleanup_runner_js_1 = require("./single-copy-cleanup-runner.js");
100
98
  // ---- Process-level error handlers ----
101
99
  process.on('uncaughtException', (err) => {
102
100
  console.error('[uncaughtException] Unhandled exception — server will continue:', err);
@@ -233,9 +231,9 @@ async function bootstrap() {
233
231
  // 256KB pages it committed at a sawtooth peak, and an ordinary major GC does not
234
232
  // return them. `createV8Reclaimer()` returns undefined if V8 declines, and the
235
233
  // heartbeat then behaves exactly as it did before.
236
- const heapReclaimer = (0, system_2.createV8Reclaimer)();
234
+ const heapReclaimer = (0, system_1.createV8Reclaimer)();
237
235
  const heapReclaim = heapReclaimer === undefined ? undefined : { reclaim: heapReclaimer };
238
- (0, system_2.startHeapWatch)('hub-main', undefined, undefined, heapReclaim);
236
+ (0, system_1.startHeapWatch)('hub-main', undefined, undefined, heapReclaim);
239
237
  // Clean up orphaned processes from previous crashes before starting
240
238
  cleanupOrphanProcesses();
241
239
  // SPA fallback — set later when admin UI is resolved, used by addon route catch-all
@@ -323,8 +321,8 @@ async function bootstrap() {
323
321
  // If registration fails, start in degraded mode: serve a health warning endpoint.
324
322
  // Instantiate new core services
325
323
  const loggingService = app.get(logging_service_1.LoggingService);
326
- const addonRouteRegistry = new system_3.AddonRouteRegistry();
327
- const dataPlaneRegistry = new system_3.DataPlaneRegistry();
324
+ const addonRouteRegistry = new system_1.AddonRouteRegistry();
325
+ const dataPlaneRegistry = new system_1.DataPlaneRegistry();
328
326
  // Use Fastify-managed notification/toast wrappers (globally provided by NotificationModule)
329
327
  const { NotificationServiceWrapper } = await Promise.resolve().then(() => __importStar(require('./core/notification/notification-wrapper.service')));
330
328
  const { ToastServiceWrapper } = await Promise.resolve().then(() => __importStar(require('./core/notification/toast-wrapper.service')));
@@ -369,9 +367,68 @@ async function bootstrap() {
369
367
  // and the distributed metrics-provider capability — no manual wiring needed.
370
368
  let trpcRegistered = false;
371
369
  let appRouter = null;
372
- // Run app.init() FIRST so onModuleInit hooks complete (PipelineWiring, AddonBridge, etc.)
373
- // before we build the tRPC router which depends on their results.
374
- await app.init();
370
+ // Share-view (`csv_*`) tokens hub-resident mint/verify/revoke over the
371
+ // settings backend (lazy getter: the backend lands after the sqlite-storage
372
+ // builtin registers; the service resolves it per call).
373
+ const shareTokenService = new share_token_service_1.ShareTokenService(() => addonRegistry.getSettingsBackend(), loggingService.createLogger('share-tokens'));
374
+ // One-time native-app login handoff codes (in-memory, 60s TTL, single-use) —
375
+ // minted by `auth.createHandoffCode`, redeemed by the viewer app via the
376
+ // public `auth.redeemHandoffCode`.
377
+ const handoffCodeService = new handoff_code_service_1.HandoffCodeService();
378
+ const buildRouter = () => (0, trpc_router_1.buildAppRouter)({
379
+ authService: app.get(auth_service_1.AuthService),
380
+ shareTokenService,
381
+ handoffCodeService,
382
+ configService: config,
383
+ featureService: app.get(feature_service_1.FeatureService),
384
+ loggingService,
385
+ eventBus: app.get(event_bus_service_1.EventBusService),
386
+ agentRegistry: app.get(agent_registry_service_1.AgentRegistryService),
387
+ moleculer: app.get(moleculer_service_1.MoleculerService),
388
+ addonRegistry,
389
+ replEngine: app.get(repl_engine_service_1.ReplEngineService),
390
+ networkQualityService: app.get(network_quality_service_1.NetworkQualityService),
391
+ addonBridge: app.get(addon_bridge_service_1.AddonBridgeService),
392
+ addonPackageService: app.get(addon_package_service_1.AddonPackageService),
393
+ notificationService,
394
+ toastService,
395
+ capabilityRegistry,
396
+ streamProbe: app.get(stream_probe_service_1.StreamProbeService),
397
+ serverUpdateService: app.get(server_update_service_1.ServerUpdateService),
398
+ });
399
+ // ── The core-cap mesh bridge, mounted BEFORE a single addon boots (D171) ──
400
+ //
401
+ // `$core-caps` exposes the hub's core (non-addon) tRPC routers on the mesh so
402
+ // a forked addon's `ctx.api.<coreCap>` resolves. It used to be registered
403
+ // after `app.init()` returned — and `app.init()` IS the addon boot, so for
404
+ // the whole of it every core-cap call was structurally unanswerable:
405
+ // `brokerTransportLink` polled the full 30 s discovery deadline and
406
+ // fast-failed, with no boot-window retry (core caps are excluded from it).
407
+ // One such await held `device-manager`, and with it every camera (D167).
408
+ //
409
+ // The router is safe to build here: `buildCapabilityRouters` is driven by
410
+ // `ALL_CAPABILITY_DEFINITIONS` (declared above, before init) and every
411
+ // provider — registry singleton or `server-provided` factory — is resolved
412
+ // PER REQUEST, not at build time. That is the two-phase shape: the route is
413
+ // on the mesh immediately, each provider completes on its own schedule, and
414
+ // a caller that is early gets an immediate, typed answer instead of 30 s of
415
+ // silence. The same router instance is handed to Fastify after init, so
416
+ // there is exactly one.
417
+ await app.init({
418
+ beforeAddonBoot: () => {
419
+ try {
420
+ appRouter = buildRouter();
421
+ app.get(moleculer_service_1.MoleculerService).registerCoreCapService((0, core_cap_bridge_1.buildCoreCapService)(appRouter));
422
+ console.log('[bootstrap] core-cap mesh bridge registered ($core-caps) — before addon boot');
423
+ }
424
+ catch (err) {
425
+ // Never abort boot for this: a hub without the bridge is degraded
426
+ // (core-cap calls fast-fail), a hub that refuses to start is down.
427
+ appRouter = null;
428
+ console.warn('[bootstrap] Failed to register core-cap mesh bridge early:', err);
429
+ }
430
+ },
431
+ });
375
432
  console.log('[bootstrap] app.init() complete — all onModuleInit hooks have run');
376
433
  // Mark registry as ready — providers from app.init() are now registered
377
434
  capabilityRegistry.ready();
@@ -429,35 +486,19 @@ async function bootstrap() {
429
486
  }
430
487
  try {
431
488
  const authService = app.get(auth_service_1.AuthService);
432
- // Share-view (`csv_*`) tokens hub-resident mint/verify/revoke over
433
- // the settings backend (lazy getter: the backend lands after the
434
- // sqlite-storage builtin registers; the service resolves it per call).
435
- const shareTokenService = new share_token_service_1.ShareTokenService(() => addonRegistry.getSettingsBackend(), loggingService.createLogger('share-tokens'));
436
- // One-time native-app login handoff codes (in-memory, 60s TTL,
437
- // single-use) — minted by `auth.createHandoffCode`, redeemed by the
438
- // viewer app via the public `auth.redeemHandoffCode`.
439
- const handoffCodeService = new handoff_code_service_1.HandoffCodeService();
440
- appRouter = (0, trpc_router_1.buildAppRouter)({
441
- authService,
442
- shareTokenService,
443
- handoffCodeService,
444
- configService: config,
445
- featureService: app.get(feature_service_1.FeatureService),
446
- loggingService,
447
- eventBus: app.get(event_bus_service_1.EventBusService),
448
- agentRegistry: app.get(agent_registry_service_1.AgentRegistryService),
449
- moleculer: app.get(moleculer_service_1.MoleculerService),
450
- addonRegistry,
451
- replEngine: app.get(repl_engine_service_1.ReplEngineService),
452
- networkQualityService: app.get(network_quality_service_1.NetworkQualityService),
453
- addonBridge: app.get(addon_bridge_service_1.AddonBridgeService),
454
- addonPackageService: app.get(addon_package_service_1.AddonPackageService),
455
- notificationService,
456
- toastService,
457
- capabilityRegistry,
458
- streamProbe: app.get(stream_probe_service_1.StreamProbeService),
459
- serverUpdateService: app.get(server_update_service_1.ServerUpdateService),
460
- });
489
+ // Normally already built and bridged onto the mesh in `beforeAddonBoot`
490
+ // above this is the fallback for the case where that failed, so a hub
491
+ // whose mesh bridge could not be mounted still serves HTTP.
492
+ if (appRouter === null) {
493
+ appRouter = buildRouter();
494
+ try {
495
+ app.get(moleculer_service_1.MoleculerService).registerCoreCapService((0, core_cap_bridge_1.buildCoreCapService)(appRouter));
496
+ console.log('[bootstrap] core-cap mesh bridge registered ($core-caps) — late fallback');
497
+ }
498
+ catch (err) {
499
+ console.warn('[bootstrap] Failed to register core-cap mesh bridge:', err);
500
+ }
501
+ }
461
502
  await fastify.register(fastify_1.fastifyTRPCPlugin, {
462
503
  prefix: '/trpc',
463
504
  trpcOptions: {
@@ -487,17 +528,6 @@ async function bootstrap() {
487
528
  },
488
529
  });
489
530
  trpcRegistered = true;
490
- // Mount the `$core-caps` Moleculer service so forked addons /
491
- // remote agents can reach the hub's core (non-addon) tRPC routers
492
- // through `ctx.api.<coreCap>`. Without it those calls hang in
493
- // `brokerTransportLink`'s unbounded discovery wait.
494
- try {
495
- moleculer.registerCoreCapService((0, core_cap_bridge_1.buildCoreCapService)(appRouter));
496
- console.log('[bootstrap] core-cap mesh bridge registered ($core-caps)');
497
- }
498
- catch (err) {
499
- console.warn('[bootstrap] Failed to register core-cap mesh bridge:', err);
500
- }
501
531
  // Register addon-pages static file endpoint
502
532
  const addonPagesService = app.get(addon_pages_service_1.AddonPagesService);
503
533
  fastify.get('/api/addon-pages/:addonId/*', async (request, reply) => {
@@ -873,7 +903,7 @@ async function bootstrap() {
873
903
  : Buffer.isBuffer(request.body)
874
904
  ? request.body
875
905
  : Buffer.from(typeof request.body === 'string' ? request.body : JSON.stringify(request.body));
876
- (0, system_3.proxyToUpstream)({
906
+ (0, system_1.proxyToUpstream)({
877
907
  baseUrl: dpMatch.endpoint.baseUrl,
878
908
  secret: dpMatch.endpoint.secret,
879
909
  upstreamPath,
@@ -46,10 +46,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.bootManual = bootManual;
47
47
  const node_crypto_1 = require("node:crypto");
48
48
  const path = __importStar(require("node:path"));
49
+ const system_1 = require("@camstack/system");
49
50
  const types_1 = require("@camstack/types");
50
51
  const fastify_1 = __importDefault(require("fastify"));
51
52
  const addon_upload_1 = require("./api/addon-upload");
52
53
  const cap_providers_1 = require("./api/core/cap-providers");
54
+ const init_sequence_1 = require("./boot/init-sequence");
53
55
  const post_boot_service_1 = require("./boot/post-boot.service");
54
56
  const addon_package_service_1 = require("./core/addon/addon-package.service");
55
57
  const addon_registry_service_1 = require("./core/addon/addon-registry.service");
@@ -109,6 +111,17 @@ async function bootManual(opts) {
109
111
  const toastWrapper = new toast_wrapper_service_1.ToastServiceWrapper();
110
112
  // ---- Layer 1: need ConfigService / LoggingService -----------------------
111
113
  const loggingService = new logging_service_1.LoggingService(configService);
114
+ // Bootstrap stdout, attached at the FIRST moment a LogManager exists.
115
+ //
116
+ // The real console destination is a core builtin that boots in pass 3, after
117
+ // the entire runner plan: a measured boot produced no stdout at all for ~40 s
118
+ // and then flushed the whole backlog newest-first, so every boot
119
+ // investigation needed a parser (docs/design/boot-to-usable-camera.md, F).
120
+ // This sink carries the window and is detached — without a second flush —
121
+ // the moment `console-logging` registers.
122
+ const bootstrapConsole = new system_1.ConsoleDestination();
123
+ void bootstrapConsole.initialize();
124
+ loggingService.attachBootstrapConsole(bootstrapConsole);
112
125
  const eventBusService = new event_bus_service_1.EventBusService(configService);
113
126
  // Wire device-name cache for log formatter — subscribes here so the
114
127
  // very first DeviceRegistered event populates the cache before any
@@ -325,27 +338,31 @@ async function bootManual(opts) {
325
338
  process.once('SIGTERM', handler);
326
339
  process.once('SIGINT', handler);
327
340
  },
328
- async init() {
329
- // Init order (dependency-graph-derived):
330
- // 1. MoleculerService starts broker and calls EventBus.attachBroker()
331
- // 2. AddonBridgeService — no cross-service deps
332
- // 3. AgentRegistryService subscribes to broker.localBus.on('$node.connected')
333
- // 4. AddonRegistryService boots every addon (needs broker live + config)
334
- await moleculerService.onModuleInit();
335
- await addonBridgeService.onModuleInit();
336
- agentRegistryService.onModuleInit();
337
- await addonRegistryService.onModuleInit();
338
- // Reconcile already-connected agents now that the hub's installed
339
- // addon set is populated. Agents online before the hub started never
340
- // fire a fresh `$node.connected` event, so the per-connect trigger
341
- // alone would miss them this catches the boot-time case (e.g. a
342
- // long-running dev-agent with a now-stale addon). Fire-and-forget so
343
- // an unreachable agent never delays boot.
344
- void agentRegistryService.reconcileConnectedAgents();
345
- // Topology emitter must boot AFTER AgentRegistry + AddonRegistry —
346
- // its initial snapshot reads both. Subscribes to lifecycle events
347
- // for incremental snapshots and emits a periodic safety net.
348
- topologyEmitterService.onModuleInit();
341
+ async init(hooks = {}) {
342
+ // The order and the `beforeAddonBoot` seam live in `runBootInitSequence`
343
+ // — see `boot/init-sequence.ts` for why that seam is load-bearing.
344
+ await (0, init_sequence_1.runBootInitSequence)({
345
+ moleculer: () => moleculerService.onModuleInit(),
346
+ addonBridge: () => addonBridgeService.onModuleInit(),
347
+ agentRegistry: () => {
348
+ agentRegistryService.onModuleInit();
349
+ },
350
+ ...(hooks.beforeAddonBoot !== undefined ? { beforeAddonBoot: hooks.beforeAddonBoot } : {}),
351
+ addonBoot: () => addonRegistryService.onModuleInit(),
352
+ afterAddonBoot: () => {
353
+ // Reconcile already-connected agents now that the hub's installed
354
+ // addon set is populated. Agents online before the hub started never
355
+ // fire a fresh `$node.connected` event, so the per-connect trigger
356
+ // alone would miss them this catches the boot-time case (e.g. a
357
+ // long-running dev-agent with a now-stale addon). Fire-and-forget so
358
+ // an unreachable agent never delays boot.
359
+ void agentRegistryService.reconcileConnectedAgents();
360
+ // Topology emitter must boot AFTER AgentRegistry + AddonRegistry
361
+ // its initial snapshot reads both. Subscribes to lifecycle events
362
+ // for incremental snapshots and emits a periodic safety net.
363
+ topologyEmitterService.onModuleInit();
364
+ },
365
+ });
349
366
  },
350
367
  async listen(port, host) {
351
368
  await fastify.listen({ port, host });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.113",
3
+ "version": "1.2.114",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -33,19 +33,19 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@camstack/addon-admin-ui": "1.2.62",
36
+ "@camstack/addon-admin-ui": "1.2.63",
37
37
  "@camstack/addon-agent-ui": "1.2.17",
38
38
  "@camstack/addon-auth": "1.2.19",
39
39
  "@camstack/addon-decoder-nodeav": "1.2.16",
40
40
  "@camstack/addon-notifiers": "1.2.21",
41
- "@camstack/addon-pipeline": "1.2.79",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.59",
41
+ "@camstack/addon-pipeline": "1.2.80",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.60",
43
43
  "@camstack/addon-post-analysis": "1.2.78",
44
44
  "@camstack/sdk": "1.2.19",
45
45
  "@camstack/shm-ring": "1.1.16",
46
- "@camstack/system": "1.2.91",
47
- "@camstack/types": "1.2.75",
48
- "@camstack/ui-library": "1.2.52",
46
+ "@camstack/system": "1.2.92",
47
+ "@camstack/types": "1.2.76",
48
+ "@camstack/ui-library": "1.2.53",
49
49
  "@fastify/compress": "^9.0.0",
50
50
  "@fastify/cookie": "^11.0.2",
51
51
  "@fastify/cors": "^11.2.0",