@teamscala/service-boot 0.23.540 → 0.23.541
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/package.json +3 -1
- package/src/apply-logger-config.test.ts +13 -8
- package/src/boot-shell.test.ts +57 -0
- package/src/boot-shell.ts +20 -0
- package/src/error-page-logo.test.ts +35 -15
- package/src/orpc-contract-rows.test.ts +10 -9
- package/src/page-server-loader-graph.test.ts +10 -6
- package/src/readiness-gate.ts +4 -0
- package/src/register-registry-page-route/meta-title.test.ts +8 -8
- package/src/resolve-brand-favicon.test.ts +13 -11
- package/src/resolve-theme-mode.test.ts +52 -27
- package/src/resolvers/fetch-org-brand.test.ts +23 -12
- package/src/resolvers/website-by-host-map.test.ts +32 -16
- package/src/run-boot-hook-list.test.ts +10 -8
- package/src/run-boot-hooks.test.ts +11 -25
- package/src/scala-tools-currency-watchdog.test.ts +14 -6
- package/src/scala-tools-currency-watchdog.ts +4 -0
- package/src/two-phase-full-app.test.ts +21 -0
- package/src/two-phase-full-app.ts +5 -1
- package/src/two-phase-health-gate.test.ts +24 -0
- package/src/two-phase-minimal-app.test.ts +2 -2
- package/src/two-phase-readiness.test.ts +84 -34
- package/src/two-phase-readiness.ts +33 -7
- package/src/types.test.ts +3 -4
- package/src/ui-service.test.ts +2 -2
- package/src/ui-service.ts +49 -24
- package/src/ws-boot-self-test.test.ts +15 -8
package/src/ui-service.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @status generated — ts-api-smoke template (scala-test-suites
|
|
3
|
-
* templates/ts_api_smoke.pkl @1.
|
|
3
|
+
* templates/ts_api_smoke.pkl @1.5.0) over the exports map of @teamscala/service-boot.
|
|
4
4
|
* ./ui-service declares 1 runtime export(s) + 3 type(s);
|
|
5
5
|
* this suite asserts the import resolves, the runtime surface matches
|
|
6
6
|
* exactly, and every export carries its declared kind. Regenerate from
|
|
7
|
-
* inputs/server-service-boot
|
|
7
|
+
* inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
|
|
8
8
|
* overwritten.
|
|
9
9
|
*/
|
|
10
10
|
|
package/src/ui-service.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
import { recordEventDurable } from "@teamscala/event-log/record-event";
|
|
7
7
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
8
8
|
import { BootFatalError } from "@teamscala/os/errors/boot-error";
|
|
9
|
-
import { Hono } from "hono";
|
|
9
|
+
import type { Hono } from "hono";
|
|
10
|
+
import { createBootShellApp } from "./boot-shell.ts";
|
|
10
11
|
import type { ServiceInstallConfig } from "./load-service-install-row.ts";
|
|
12
|
+
import { swapApp } from "./mutable-app-ref.ts";
|
|
11
13
|
import { setReady } from "./readiness-gate.ts";
|
|
12
14
|
import { resolveServicePort } from "./resolve-service-port.ts";
|
|
13
15
|
import { buildMinimalApp } from "./two-phase-minimal-app.ts";
|
|
@@ -42,14 +44,6 @@ export async function bootUiService(
|
|
|
42
44
|
const log = getAppLogger();
|
|
43
45
|
const startTime = Date.now();
|
|
44
46
|
|
|
45
|
-
const minimalAppResult = await buildMinimalApp(
|
|
46
|
-
config,
|
|
47
|
-
serviceSlug,
|
|
48
|
-
databaseUrl,
|
|
49
|
-
deps.upgradeWebSocket!,
|
|
50
|
-
deps.getSharedDb,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
47
|
const isRailway = Boolean(process.env.RAILWAY_ENVIRONMENT);
|
|
54
48
|
const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
|
|
55
49
|
const declaredAppPort = config.app_port ?? config.port;
|
|
@@ -73,19 +67,44 @@ export async function bootUiService(
|
|
|
73
67
|
}
|
|
74
68
|
const port = requireValidPort(resolution.port, serviceSlug);
|
|
75
69
|
|
|
76
|
-
const
|
|
77
|
-
const minimalDuration = minimalReadyTime - startTime;
|
|
78
|
-
log.info(
|
|
79
|
-
`[ui-service-boot:${serviceSlug}] Minimal app composed on port ${port} in ${minimalDuration}ms`,
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
setReady();
|
|
70
|
+
const shell = createBootShellApp(serviceSlug);
|
|
83
71
|
log.info(
|
|
84
|
-
`[ui-service-boot:${serviceSlug}]
|
|
72
|
+
`[ui-service-boot:${serviceSlug}] Bind-first boot shell composed — the port answers before the mount chain runs`,
|
|
85
73
|
);
|
|
86
74
|
|
|
87
|
-
|
|
75
|
+
void (async () => {
|
|
76
|
+
let phase: "phase-1" | "phase-2" = "phase-1";
|
|
77
|
+
let minimalDuration = 0;
|
|
88
78
|
try {
|
|
79
|
+
const PHASE1_TIMEOUT_MS = 120_000;
|
|
80
|
+
const minimalAppResult = await Promise.race([
|
|
81
|
+
buildMinimalApp(
|
|
82
|
+
config,
|
|
83
|
+
serviceSlug,
|
|
84
|
+
databaseUrl,
|
|
85
|
+
deps.upgradeWebSocket!,
|
|
86
|
+
deps.getSharedDb,
|
|
87
|
+
),
|
|
88
|
+
new Promise<never>((_, reject) =>
|
|
89
|
+
setTimeout(
|
|
90
|
+
() => reject(new Error(`buildMinimalApp timed out after ${PHASE1_TIMEOUT_MS / 1000}s — phase 1 hung silently, see step logs`)),
|
|
91
|
+
PHASE1_TIMEOUT_MS,
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
swapApp(minimalAppResult.app, "minimal");
|
|
97
|
+
minimalDuration = Date.now() - startTime;
|
|
98
|
+
log.info(
|
|
99
|
+
`[ui-service-boot:${serviceSlug}] Minimal app composed on port ${port} in ${minimalDuration}ms`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
setReady();
|
|
103
|
+
log.info(
|
|
104
|
+
`[ui-service-boot:${serviceSlug}] Ready gate flipped - MCP tools can now connect (full app building in background)`,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
phase = "phase-2";
|
|
89
108
|
const PHASE2_TIMEOUT_MS = 120_000;
|
|
90
109
|
const fullAppResult = await Promise.race([
|
|
91
110
|
buildFullApp(minimalAppResult, deps),
|
|
@@ -103,7 +122,7 @@ export async function bootUiService(
|
|
|
103
122
|
|
|
104
123
|
if (fullAppResult.clientBuildFailed !== undefined) {
|
|
105
124
|
log.error(
|
|
106
|
-
`[ui-service-boot:${serviceSlug}] Full app swapped in after ${fullDuration}ms with NO servable client build — the process stays up serving API, MCP and jobs; pages and /health
|
|
125
|
+
`[ui-service-boot:${serviceSlug}] Full app swapped in after ${fullDuration}ms with NO servable client build — the process stays up serving API, MCP and jobs; pages answer the branded holding page and /health reports degraded. buildFullApp recorded boot.client-build-failed with the reason.`,
|
|
107
126
|
);
|
|
108
127
|
return;
|
|
109
128
|
}
|
|
@@ -113,13 +132,19 @@ export async function bootUiService(
|
|
|
113
132
|
);
|
|
114
133
|
} catch (err) {
|
|
115
134
|
const message = err instanceof Error ? err.message : String(err);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
if (phase === "phase-1") {
|
|
136
|
+
log.error(
|
|
137
|
+
`[ui-service-boot:${serviceSlug}] Phase-1 boot failed — exiting so the supervisor restarts a service whose mount chain did not complete: ${message}`,
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
log.error(
|
|
141
|
+
`[ui-service-boot:${serviceSlug}] Phase-2 boot failed — exiting so the supervisor restarts a service that serves a fresh client build: ${message}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
119
144
|
await recordEventDurable({
|
|
120
145
|
id: crypto.randomUUID(),
|
|
121
146
|
kind: "service.boot.failed",
|
|
122
|
-
payload: { service_slug: serviceSlug, phase
|
|
147
|
+
payload: { service_slug: serviceSlug, phase, message: message.slice(0, 2000) },
|
|
123
148
|
occurredAt: new Date(),
|
|
124
149
|
});
|
|
125
150
|
process.exit(1);
|
|
@@ -131,7 +156,7 @@ export async function bootUiService(
|
|
|
131
156
|
blocks: [],
|
|
132
157
|
routes: [],
|
|
133
158
|
port,
|
|
134
|
-
app:
|
|
159
|
+
app: shell,
|
|
135
160
|
};
|
|
136
161
|
}
|
|
137
162
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @status generated — ts-api-smoke template (scala-test-suites
|
|
3
|
-
* templates/ts_api_smoke.pkl @1.
|
|
4
|
-
* ./ws-boot-self-test declares 1 runtime export(s) and
|
|
3
|
+
* templates/ts_api_smoke.pkl @1.5.0) over the exports map of @teamscala/service-boot.
|
|
4
|
+
* ./ws-boot-self-test declares 1 runtime export(s) and 4 declared behaviour vector(s);
|
|
5
5
|
* this suite asserts the import resolves, the runtime surface matches
|
|
6
6
|
* exactly, and every export carries its declared kind. Regenerate from
|
|
7
|
-
* inputs/server-service-boot
|
|
7
|
+
* inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
|
|
8
8
|
* overwritten.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -24,20 +24,27 @@ describe("ws-boot-self-test exports-map smoke", () => {
|
|
|
24
24
|
expect(typeof mod["runWsBootSelfTest"]).toBe("function");
|
|
25
25
|
});
|
|
26
26
|
it("no declared websocket routes resolves without connecting", async () => {
|
|
27
|
-
const got = await runWsBootSelfTest({ websocket_routes: [] } as never, "svc-ws", 59999)
|
|
27
|
+
const got = await runWsBootSelfTest({ websocket_routes: [] } as never, "svc-ws", 59999)
|
|
28
28
|
expect(got).toBeUndefined();
|
|
29
29
|
});
|
|
30
30
|
it("a route whose server upgrades resolves 101", async () => {
|
|
31
31
|
const server = Bun.serve({ port: 0, fetch(req, srv) { if (!srv.upgrade(req)) return new Response("no upgrade", { status: 401 }); }, websocket: { open() {}, message() {} } });
|
|
32
32
|
const port = server.port as number;
|
|
33
|
-
const got = await runWsBootSelfTest({ websocket_routes: [{ path: "/ws-boot-self-test" }] } as never, "svc-ws", port)
|
|
33
|
+
const got = await runWsBootSelfTest({ websocket_routes: [{ path: "/ws-boot-self-test" }] } as never, "svc-ws", port)
|
|
34
|
+
server.stop(true);
|
|
34
35
|
expect(got).toBeUndefined();
|
|
35
|
-
server.stop(true);
|
|
36
36
|
});
|
|
37
|
-
it("a route nobody listens on fails boot loudly, naming the path", async () => {
|
|
37
|
+
it("a route nobody listens on fails boot loudly, naming the path [toBeNull]", async () => {
|
|
38
38
|
let thrown: unknown = null;
|
|
39
39
|
try { await runWsBootSelfTest({ websocket_routes: [{ path: "/ws-boot-self-test-dead" }] } as never, "svc-ws", 59987); } catch (err) { thrown = err; }
|
|
40
40
|
expect(thrown).not.toBeNull();
|
|
41
|
-
expect(
|
|
41
|
+
expect(thrown).not.toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
it("a route nobody listens on fails boot loudly, naming the path [toContain]", async () => {
|
|
44
|
+
let thrown: unknown = null;
|
|
45
|
+
try { await runWsBootSelfTest({ websocket_routes: [{ path: "/ws-boot-self-test-dead" }] } as never, "svc-ws", 59987); } catch (err) { thrown = err; }
|
|
46
|
+
const got = (String(thrown));
|
|
47
|
+
const __t0_0 = got;
|
|
48
|
+
expect(typeof __t0_0 === "string" ? __t0_0 : JSON.stringify(__t0_0)).toContain("/ws-boot-self-test-dead");
|
|
42
49
|
});
|
|
43
50
|
});
|