@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.
Files changed (40) hide show
  1. package/package.json +5 -1
  2. package/src/apply-logger-config.test.ts +13 -8
  3. package/src/attach-portal-members.test.ts +4 -0
  4. package/src/boot-shell.test.ts +57 -0
  5. package/src/boot-shell.ts +20 -0
  6. package/src/build-client-assets/sourcemaps-upload.test.ts +51 -6
  7. package/src/error-page-logo.test.ts +35 -15
  8. package/src/mount-orpc-router.test.ts +8 -0
  9. package/src/orpc-contract-rows.test.ts +10 -9
  10. package/src/page-server-loader-graph.test.ts +10 -6
  11. package/src/read-html-template.test.ts +9 -1
  12. package/src/readiness-gate.ts +4 -0
  13. package/src/register-registry-page-route/apply-page-branding.test.ts +13 -1
  14. package/src/register-registry-page-route/apply-page-branding.ts +1 -1
  15. package/src/register-registry-page-route/live-loader-config.test.ts +13 -4
  16. package/src/register-registry-page-route/meta-title.test.ts +8 -8
  17. package/src/register-registry-page-route/page-data-shared.test.ts +12 -5
  18. package/src/register-registry-page-route/resolve-entity-title.test.ts +9 -1
  19. package/src/register-registry-page-route/resolve-page-data.test.ts +16 -1
  20. package/src/register-registry-page-route/resolve-request-graph.test.ts +12 -26
  21. package/src/resolve-brand-favicon.test.ts +13 -11
  22. package/src/resolve-theme-mode.test.ts +52 -27
  23. package/src/resolvers/fetch-org-brand.test.ts +23 -12
  24. package/src/resolvers/website-by-host-map.test.ts +32 -16
  25. package/src/resource-not-found.test.ts +19 -1
  26. package/src/run-boot-hook-list.test.ts +10 -8
  27. package/src/run-boot-hooks.test.ts +11 -25
  28. package/src/scala-tools-currency-watchdog.test.ts +14 -6
  29. package/src/scala-tools-currency-watchdog.ts +5 -1
  30. package/src/theme-fonts-link.test.ts +2 -0
  31. package/src/two-phase-full-app.test.ts +69 -2
  32. package/src/two-phase-full-app.ts +29 -6
  33. package/src/two-phase-health-gate.test.ts +24 -0
  34. package/src/two-phase-minimal-app.test.ts +2 -2
  35. package/src/two-phase-readiness.test.ts +84 -34
  36. package/src/two-phase-readiness.ts +33 -7
  37. package/src/types.test.ts +3 -4
  38. package/src/ui-service.test.ts +2 -2
  39. package/src/ui-service.ts +55 -23
  40. 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 minimalReadyTime = Date.now();
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}] Ready gate flipped - MCP tools can now connect (full app building in background)`,
72
+ `[ui-service-boot:${serviceSlug}] Bind-first boot shell composed — the port answers before the mount chain runs`,
85
73
  );
86
74
 
87
- ;(async () => {
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
- log.error(
110
- `[ui-service-boot:${serviceSlug}] Phase-2 boot failed — exiting so the supervisor restarts a service that serves a fresh client build: ${message}`,
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: "phase-2", message: message.slice(0, 2000) },
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: minimalAppResult.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.3.0) over the exports map of @teamscala/service-boot.
4
- * ./ws-boot-self-test declares 1 runtime export(s) and 3 declared behaviour vector(s);
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-ts/api_smoke.json (scala-test-suites) — hand edits are
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(String(thrown)).toContain("/ws-boot-self-test-dead");
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
  });