@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.
@@ -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.
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-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
 
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),
@@ -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 stay unready. buildFullApp recorded boot.client-build-failed with the reason.`,
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
- log.error(
117
- `[ui-service-boot:${serviceSlug}] Phase-2 boot failed — exiting so the supervisor restarts a service that serves a fresh client build: ${message}`,
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: "phase-2", message: message.slice(0, 2000) },
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: minimalAppResult.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.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
  });