@camstack/server 1.2.585 → 1.2.587
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 +47 -99
- package/dist/ui-serving/lazy-ui-serving.js +143 -0
- package/package.json +7 -7
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
|
-
|
|
1475
|
-
|
|
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
|
|
1502
|
-
if (!
|
|
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
|
|
1535
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.2.587",
|
|
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.
|
|
42
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
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.
|
|
47
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.328",
|
|
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.
|
|
52
|
-
"@camstack/types": "1.2.
|
|
53
|
-
"@camstack/ui-library": "1.2.
|
|
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",
|