@teamscala/service-boot 0.23.539 → 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 +5 -1
- package/src/apply-logger-config.test.ts +13 -8
- package/src/attach-portal-members.test.ts +4 -0
- package/src/boot-shell.test.ts +57 -0
- package/src/boot-shell.ts +20 -0
- package/src/build-client-assets/sourcemaps-upload.test.ts +51 -6
- package/src/error-page-logo.test.ts +35 -15
- package/src/mount-orpc-router.test.ts +8 -0
- package/src/orpc-contract-rows.test.ts +10 -9
- package/src/page-server-loader-graph.test.ts +10 -6
- package/src/read-html-template.test.ts +9 -1
- package/src/readiness-gate.ts +4 -0
- package/src/register-registry-page-route/apply-page-branding.test.ts +13 -1
- package/src/register-registry-page-route/apply-page-branding.ts +1 -1
- package/src/register-registry-page-route/live-loader-config.test.ts +13 -4
- package/src/register-registry-page-route/meta-title.test.ts +8 -8
- package/src/register-registry-page-route/page-data-shared.test.ts +12 -5
- package/src/register-registry-page-route/resolve-entity-title.test.ts +9 -1
- package/src/register-registry-page-route/resolve-page-data.test.ts +16 -1
- package/src/register-registry-page-route/resolve-request-graph.test.ts +12 -26
- 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/resource-not-found.test.ts +19 -1
- 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 +5 -1
- package/src/theme-fonts-link.test.ts +2 -0
- package/src/two-phase-full-app.test.ts +69 -2
- package/src/two-phase-full-app.ts +29 -6
- 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 +55 -23
- package/src/ws-boot-self-test.test.ts +15 -8
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),
|
|
@@ -101,18 +120,31 @@ export async function bootUiService(
|
|
|
101
120
|
|
|
102
121
|
swapToFullApp(fullAppResult.app, serviceSlug);
|
|
103
122
|
|
|
123
|
+
if (fullAppResult.clientBuildFailed !== undefined) {
|
|
124
|
+
log.error(
|
|
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.`,
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
104
130
|
log.info(
|
|
105
131
|
`[ui-service-boot:${serviceSlug}] Full app swapped in after ${fullDuration}ms (${fullDuration - minimalDuration}ms after minimal app)`,
|
|
106
132
|
);
|
|
107
133
|
} catch (err) {
|
|
108
134
|
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
144
|
await recordEventDurable({
|
|
113
145
|
id: crypto.randomUUID(),
|
|
114
146
|
kind: "service.boot.failed",
|
|
115
|
-
payload: { service_slug: serviceSlug, phase
|
|
147
|
+
payload: { service_slug: serviceSlug, phase, message: message.slice(0, 2000) },
|
|
116
148
|
occurredAt: new Date(),
|
|
117
149
|
});
|
|
118
150
|
process.exit(1);
|
|
@@ -124,7 +156,7 @@ export async function bootUiService(
|
|
|
124
156
|
blocks: [],
|
|
125
157
|
routes: [],
|
|
126
158
|
port,
|
|
127
|
-
app:
|
|
159
|
+
app: shell,
|
|
128
160
|
};
|
|
129
161
|
}
|
|
130
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
|
});
|