@camstack/server 1.2.586 → 1.2.588

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/main.js CHANGED
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
+ const lazy_ui_serving_js_1 = require("./ui-serving/lazy-ui-serving.js");
40
41
  const node_child_process_1 = require("node:child_process");
41
42
  const fs = __importStar(require("node:fs"));
42
43
  const path = __importStar(require("node:path"));
@@ -1471,8 +1472,43 @@ async function bootstrap() {
1471
1472
  await addonRegistry.setAppRouter(appRouter);
1472
1473
  console.log('[bootstrap] AddonRegistry wired with tRPC direct caller');
1473
1474
  }
1474
- const adminUiState = { staticDir: null, indexPath: null };
1475
- const viewerUiState = { staticDir: null, indexPath: null };
1475
+ // ScopedTokenManager and admin user creation handled by local-auth addon.
1476
+ // Both UI bundles are resolved LAZILY and single-flight (D876): a request that
1477
+ // finds nothing served triggers one attempt, and a re-registered provider
1478
+ // (addon update/restart) moves the served directory.
1479
+ const uiLogger = (level, message) => {
1480
+ if (level === 'error')
1481
+ console.error(`[bootstrap] ${message}`);
1482
+ else if (level === 'warn')
1483
+ console.warn(`[bootstrap] ${message}`);
1484
+ else
1485
+ console.log(`[bootstrap] ${message}`);
1486
+ };
1487
+ const hubUiProvider = (capName) => {
1488
+ // HUB-NODE-SCOPED resolution — never the cluster-global singleton. An agent
1489
+ // node registers the same cap (agent-only placement) and its `getStaticDir()`
1490
+ // answers with a path on the agent's filesystem.
1491
+ const capRegistry = app.get(addon_registry_service_1.AddonRegistryService).getCapabilityRegistry();
1492
+ return capRegistry?.getSingletonForNode(capName, 'hub') ?? null;
1493
+ };
1494
+ const adminUi = (0, lazy_ui_serving_js_1.createLazyUiServing)({
1495
+ label: 'Admin UI',
1496
+ // CAMSTACK_SKIP_ADMIN_UI_WAIT: e2e runs that do not need the SPA.
1497
+ lookup: () => process.env['CAMSTACK_SKIP_ADMIN_UI_WAIT'] === '1' ? null : hubUiProvider('admin-ui'),
1498
+ log: uiLogger,
1499
+ onServed: ({ staticDir, indexPath }) => {
1500
+ spaIndexHtml = indexPath;
1501
+ (0, precompressed_asset_js_1.warmPrecompressedAssets)(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
1502
+ },
1503
+ });
1504
+ const viewerUi = (0, lazy_ui_serving_js_1.createLazyUiServing)({
1505
+ label: 'Viewer UI',
1506
+ lookup: () => hubUiProvider('viewer-ui'),
1507
+ log: uiLogger,
1508
+ onServed: ({ staticDir }) => {
1509
+ (0, precompressed_asset_js_1.warmPrecompressedAssets)(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
1510
+ },
1511
+ });
1476
1512
  const VIEWER_MOUNT = '/viewer/camstack';
1477
1513
  // Register UI routes before Fastify starts, but resolve their forked
1478
1514
  // providers after listen. During a cold boot the control plane is therefore
@@ -1498,10 +1534,11 @@ async function bootstrap() {
1498
1534
  return reply.type('text/html').send(fs.createReadStream(webrtcTestPath));
1499
1535
  });
1500
1536
  fastify.get('/viewer/*', async (request, reply) => {
1501
- const { staticDir, indexPath } = viewerUiState;
1502
- if (!staticDir || !indexPath) {
1537
+ const served = await viewerUi.get();
1538
+ if (!served) {
1503
1539
  return reply.status(503).send({ error: 'Viewer UI is starting' });
1504
1540
  }
1541
+ const { staticDir, indexPath } = served;
1505
1542
  const pathOnly = request.url.split('?')[0] ?? request.url;
1506
1543
  let rel = '';
1507
1544
  if (pathOnly.startsWith(`${VIEWER_MOUNT}/`)) {
@@ -1531,10 +1568,11 @@ async function bootstrap() {
1531
1568
  url.startsWith('/viewer')) {
1532
1569
  return reply.callNotFound();
1533
1570
  }
1534
- const { staticDir, indexPath } = adminUiState;
1535
- if (!staticDir || !indexPath) {
1571
+ const served = await adminUi.get();
1572
+ if (!served) {
1536
1573
  return reply.status(503).send({ error: 'Admin UI is starting' });
1537
1574
  }
1575
+ const { staticDir, indexPath } = served;
1538
1576
  const pathOnly = url.split('?')[0] ?? url;
1539
1577
  // The shell asked for BY NAME is still the shell: it gets the mount
1540
1578
  // injection and the no-store policy, never the asset branch.
@@ -1551,102 +1589,12 @@ async function bootstrap() {
1551
1589
  // it is mounted; without the header the shell is served as before.
1552
1590
  return (0, spa_mount_1.sendSpaShell)(reply, indexPath, request.headers);
1553
1591
  });
1592
+ // Boot-time attempt, after listen. Later attempts are driven by requests.
1554
1593
  const resolveAdminUi = async () => {
1555
- try {
1556
- const bootAddonRegistry = app.get(addon_registry_service_1.AddonRegistryService);
1557
- const capRegistry = bootAddonRegistry.getCapabilityRegistry();
1558
- // HUB-NODE-SCOPED resolution — never the cluster-global singleton.
1559
- // Agent nodes register the same `admin-ui` cap through their own
1560
- // agent-ui addon (placement: agent-only), and the cluster-elected
1561
- // active provider can land on the AGENT's registration depending on
1562
- // node boot order. Its `getStaticDir()` then answers with a path on
1563
- // the agent's filesystem — nonexistent here — and the SPA catch-all
1564
- // silently never registers (every `GET /` 404s until the next
1565
- // restart re-rolls the election). `getSingletonForNode(cap, 'hub')`
1566
- // only ever resolves providers hosted on THIS node.
1567
- let adminUI = capRegistry?.getSingletonForNode('admin-ui', 'hub');
1568
- // CAMSTACK_SKIP_ADMIN_UI_WAIT bypasses the background capability poll in
1569
- // e2e runs that do not need the SPA.
1570
- const skipAdminUIWait = process.env['CAMSTACK_SKIP_ADMIN_UI_WAIT'] === '1';
1571
- if (!adminUI && capRegistry && !skipAdminUIWait) {
1572
- // Forked-addon spawn + Moleculer registration + tRPC hydration
1573
- // can take ~15-20s on cold boot, especially when several runners
1574
- // are spawning in parallel. The previous 10s window was racing
1575
- // the admin-ui runner's boot — fastify-static didn't register and
1576
- // every `GET /` returned 404. Bump to 60s; we only pay this
1577
- // wait once at boot.
1578
- const ADMIN_UI_WAIT_MS = 60_000;
1579
- const POLL_MS = 200;
1580
- const deadline = Date.now() + ADMIN_UI_WAIT_MS;
1581
- while (!adminUI && Date.now() < deadline) {
1582
- await new Promise((r) => setTimeout(r, POLL_MS));
1583
- adminUI = capRegistry.getSingletonForNode('admin-ui', 'hub');
1584
- }
1585
- }
1586
- if (adminUI) {
1587
- const { staticDir } = await adminUI.getStaticDir();
1588
- const indexPath = path.join(staticDir, 'index.html');
1589
- if (fs.existsSync(staticDir) && fs.existsSync(indexPath)) {
1590
- spaIndexHtml = indexPath;
1591
- adminUiState.staticDir = staticDir;
1592
- adminUiState.indexPath = indexPath;
1593
- (0, precompressed_asset_js_1.warmPrecompressedAssets)(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
1594
- const { version } = await adminUI.getVersion();
1595
- console.log(`[bootstrap] Admin UI served from: ${staticDir} (v${version})`);
1596
- }
1597
- else {
1598
- console.warn(`[bootstrap] Admin UI dist not found at: ${staticDir} — run 'npm run build' in addon-admin-ui`);
1599
- }
1600
- }
1601
- else {
1602
- console.warn('[bootstrap] admin-ui capability not registered — no static file serving');
1603
- }
1604
- }
1605
- catch (err) {
1606
- console.error('[bootstrap] Failed to set up admin UI static serving:', err);
1607
- }
1594
+ await adminUi.get();
1608
1595
  };
1609
1596
  const resolveViewerUi = async () => {
1610
- try {
1611
- const bootAddonRegistry = app.get(addon_registry_service_1.AddonRegistryService);
1612
- const capRegistry = bootAddonRegistry.getCapabilityRegistry();
1613
- // Hub-node-scoped for the same reason as admin-ui above: a remote
1614
- // node's registration must never win the election for the provider
1615
- // whose staticDir this process reads from local disk.
1616
- let viewerUI = capRegistry?.getSingletonForNode('viewer-ui', 'hub');
1617
- if (!viewerUI && capRegistry) {
1618
- // The viewer-ui addon runs in its own forked runner and registers a few
1619
- // seconds after boot. Poll briefly (shorter than admin-ui's 60s — the
1620
- // viewer is non-critical and admin-ui already gated the slow cold boot).
1621
- const VIEWER_UI_WAIT_MS = 30_000;
1622
- const POLL_MS = 200;
1623
- const deadline = Date.now() + VIEWER_UI_WAIT_MS;
1624
- while (!viewerUI && Date.now() < deadline) {
1625
- await new Promise((r) => setTimeout(r, POLL_MS));
1626
- viewerUI = capRegistry.getSingletonForNode('viewer-ui', 'hub');
1627
- }
1628
- }
1629
- if (viewerUI) {
1630
- const { staticDir } = await viewerUI.getStaticDir();
1631
- const indexPath = path.join(staticDir, 'index.html');
1632
- if (fs.existsSync(staticDir) && fs.existsSync(indexPath)) {
1633
- viewerUiState.staticDir = staticDir;
1634
- viewerUiState.indexPath = indexPath;
1635
- (0, precompressed_asset_js_1.warmPrecompressedAssets)(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
1636
- const { version } = await viewerUI.getVersion();
1637
- console.log(`[bootstrap] Viewer UI served from: ${staticDir} at ${VIEWER_MOUNT} (v${version})`);
1638
- }
1639
- else {
1640
- console.warn(`[bootstrap] Viewer UI dist not found at: ${staticDir} — run 'npm run build' in addon-viewer-ui`);
1641
- }
1642
- }
1643
- else {
1644
- console.warn('[bootstrap] viewer-ui capability not registered — viewer not served');
1645
- }
1646
- }
1647
- catch (err) {
1648
- console.error('[bootstrap] Failed to set up viewer UI static serving:', err);
1649
- }
1597
+ await viewerUi.get();
1650
1598
  };
1651
1599
  // Establish the logging mirror and re-arm any diagnostic window that was
1652
1600
  // armed BEFORE this boot.
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createLazyUiServing = createLazyUiServing;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const DEFAULT_ATTEMPT_GAP_MS = 2_000;
40
+ const DEFAULT_REQUEST_WAIT_MS = 4_000;
41
+ /** Wait for `work`, but never longer than `ms`: a hung provider RPC must not hang every request. */
42
+ async function waitAtMost(work, ms) {
43
+ let timer;
44
+ const deadline = new Promise((resolve) => {
45
+ timer = setTimeout(resolve, ms);
46
+ });
47
+ try {
48
+ await Promise.race([work, deadline]);
49
+ }
50
+ finally {
51
+ clearTimeout(timer);
52
+ }
53
+ }
54
+ /**
55
+ * Lazy, single-flight resolution of a forked UI provider (D876).
56
+ *
57
+ * A request that finds nothing served triggers (or joins) ONE attempt; the next
58
+ * request tries again, at most once per `minAttemptGapMs`. Once served, the
59
+ * provider object identity is compared on each request: a re-registered
60
+ * provider (addon update/restart) or a vanished index re-resolves, so the
61
+ * served directory follows the provider. No timers, no retry loop.
62
+ */
63
+ function createLazyUiServing(options) {
64
+ const gapMs = options.minAttemptGapMs ?? DEFAULT_ATTEMPT_GAP_MS;
65
+ const waitMs = options.requestWaitMs ?? DEFAULT_REQUEST_WAIT_MS;
66
+ const now = options.now ?? Date.now;
67
+ let state = null;
68
+ let servedProvider = null;
69
+ let inFlight = null;
70
+ let lastAttemptAt = Number.NEGATIVE_INFINITY;
71
+ let lastNotice = '';
72
+ const notice = (level, message) => {
73
+ if (message === lastNotice)
74
+ return;
75
+ lastNotice = message;
76
+ options.log(level, message);
77
+ };
78
+ const attempt = async () => {
79
+ try {
80
+ const provider = options.lookup();
81
+ if (!provider) {
82
+ if (!state) {
83
+ notice('warn', `${options.label}: capability not registered yet, answering 503 until it is`);
84
+ }
85
+ return;
86
+ }
87
+ const { staticDir } = await provider.getStaticDir();
88
+ const indexPath = path.join(staticDir, 'index.html');
89
+ if (!fs.existsSync(staticDir) || !fs.existsSync(indexPath)) {
90
+ if (state && state.staticDir !== staticDir)
91
+ state = null;
92
+ notice('warn', `${options.label}: dist not found at ${staticDir}`);
93
+ return;
94
+ }
95
+ if (state?.staticDir === staticDir) {
96
+ servedProvider = provider;
97
+ return;
98
+ }
99
+ // The version only labels the log line: a failing read must not keep the
100
+ // OLD directory served behind a provider we already marked as current.
101
+ const version = await provider
102
+ .getVersion()
103
+ .then((v) => v.version)
104
+ .catch(() => 'unknown');
105
+ const next = { staticDir, indexPath };
106
+ state = next;
107
+ servedProvider = provider;
108
+ notice('info', `${options.label} served from: ${staticDir} (v${version})`);
109
+ options.onServed?.(next, version);
110
+ }
111
+ catch (err) {
112
+ notice('error', `${options.label}: resolution failed: ${err instanceof Error ? err.message : String(err)}`);
113
+ }
114
+ };
115
+ const startAttempt = () => {
116
+ if (inFlight)
117
+ return inFlight;
118
+ lastAttemptAt = now();
119
+ inFlight = attempt().finally(() => {
120
+ inFlight = null;
121
+ });
122
+ return inFlight;
123
+ };
124
+ const needsAttempt = () => {
125
+ if (!state)
126
+ return true;
127
+ if (!fs.existsSync(state.indexPath))
128
+ return true;
129
+ const current = options.lookup();
130
+ return current !== null && current !== servedProvider;
131
+ };
132
+ return {
133
+ async get() {
134
+ if (inFlight) {
135
+ await waitAtMost(inFlight, waitMs);
136
+ }
137
+ else if (needsAttempt() && now() - lastAttemptAt >= gapMs) {
138
+ await waitAtMost(startAttempt(), waitMs);
139
+ }
140
+ return state && fs.existsSync(state.indexPath) ? state : null;
141
+ },
142
+ };
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.586",
3
+ "version": "1.2.588",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,19 +38,19 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.431",
42
- "@camstack/addon-agent-ui": "1.2.238",
41
+ "@camstack/addon-admin-ui": "1.2.432",
42
+ "@camstack/addon-agent-ui": "1.2.239",
43
43
  "@camstack/addon-auth": "1.2.148",
44
44
  "@camstack/addon-decoder-nodeav": "1.2.123",
45
45
  "@camstack/addon-notifiers": "1.2.135",
46
46
  "@camstack/addon-pipeline": "1.2.374",
47
- "@camstack/addon-pipeline-orchestrator": "1.2.328",
47
+ "@camstack/addon-pipeline-orchestrator": "1.2.329",
48
48
  "@camstack/addon-post-analysis": "1.2.372",
49
49
  "@camstack/develop": "0.1.11",
50
50
  "@camstack/sdk": "1.2.132",
51
- "@camstack/system": "1.2.440",
52
- "@camstack/types": "1.2.376",
53
- "@camstack/ui-library": "1.2.294",
51
+ "@camstack/system": "1.2.441",
52
+ "@camstack/types": "1.2.377",
53
+ "@camstack/ui-library": "1.2.295",
54
54
  "@fastify/compress": "^9.0.0",
55
55
  "@fastify/cookie": "^11.0.2",
56
56
  "@fastify/cors": "^11.2.0",