akanjs 2.4.0-rc.1 → 2.4.0-rc.2

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/base/baseEnv.ts CHANGED
@@ -104,7 +104,11 @@ export const getEnv = (): ClientEnv => {
104
104
  const clientHost =
105
105
  process.env.AKAN_PUBLIC_CLIENT_HOST ??
106
106
  (operationMode === "local" || side === "server" ? "localhost" : window.location.hostname);
107
- const clientPort = parseInt(process.env.AKAN_PUBLIC_CLIENT_PORT ?? (operationMode === "local" ? "8282" : "443"));
107
+ const clientPort =
108
+ side === "server"
109
+ ? parseInt(process.env.AKAN_PUBLIC_CLIENT_PORT ?? (operationMode === "local" ? "8282" : "443"))
110
+ : parseInt(window.location.port || (window.location.protocol === "https:" ? "443" : "80"));
111
+
108
112
  const clientHttpProtocol =
109
113
  side === "client"
110
114
  ? (window.location.protocol as "http:" | "https:")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.4.0-rc.1",
3
+ "version": "2.4.0-rc.2",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/server/akanApp.ts CHANGED
@@ -112,10 +112,13 @@ export class AkanApp {
112
112
  this.#serverPath = AkanApp.#resolveServerPath(resolvedOptions.serverPath ?? serverPath);
113
113
  this.#artifactDir = path.resolve(path.dirname(this.#serverPath), ".akan", "artifact");
114
114
  this.#replica = AkanApp.#parseReplicaConfig(resolvedOptions.replica);
115
- this.#runtimeDir =
116
- (resolvedOptions.runtimeDir ?? process.env.AKAN_RUNTIME_DIR ?? process.env.NODE_ENV === "production")
117
- ? path.resolve(process.cwd(), "runtime")
118
- : path.resolve(process.cwd(), "local", "apps", process.env.AKAN_PUBLIC_APP_NAME ?? "unknown", "runtime");
115
+ this.#runtimeDir = path.resolve(
116
+ resolvedOptions.runtimeDir ??
117
+ process.env.AKAN_RUNTIME_DIR ??
118
+ (process.env.NODE_ENV === "production"
119
+ ? path.resolve(process.cwd(), "runtime")
120
+ : path.resolve(process.cwd(), "local", "apps", process.env.AKAN_PUBLIC_APP_NAME ?? "unknown", "runtime")),
121
+ );
119
122
  this.#port = Number(resolvedOptions.port ?? process.env.PORT ?? 8282);
120
123
  this.#wsBasePort = Number(resolvedOptions.wsBasePort ?? process.env.AKAN_WS_BASE_PORT ?? this.#port + 10_000);
121
124
  this.#openapi = resolvedOptions.openapi;
@@ -234,9 +237,10 @@ export class AkanApp {
234
237
  Promise.all([...this.#children.values()].map((child) => child.proc.exited.catch(() => undefined))),
235
238
  new Promise((resolve) => setTimeout(resolve, AkanApp.#childShutdownWaitMs())),
236
239
  ]);
237
- for (const child of this.#children.values()) {
238
- if (!child.proc.killed) child.proc.kill();
239
- }
240
+
241
+ const stragglers = [...this.#children.values()].filter((child) => !child.proc.killed);
242
+ for (const child of stragglers) child.proc.kill("SIGKILL");
243
+ await Promise.all(stragglers.map((child) => child.proc.exited.catch(() => undefined)));
240
244
  this.#children.clear();
241
245
  await this.#stopFileLogging();
242
246
  this.#resolveStopped?.();
@@ -657,7 +661,7 @@ export class AkanApp {
657
661
  async #proxyHttp(req: Request): Promise<Response> {
658
662
  const child = this.#pickFederationChild();
659
663
  if (!child?.upstream || child.upstream.type !== "unix") {
660
- return new Response("No healthy federation child is ready", { status: 503 });
664
+ return this.#respondWithCrashPage(req) ?? new Response("No healthy federation child is ready", { status: 503 });
661
665
  }
662
666
  const url = new URL(req.url);
663
667
  const upstreamUrl = `http://akan-child${url.pathname}${url.search}`;
@@ -697,6 +701,70 @@ export class AkanApp {
697
701
  return candidate.code === "FailedToOpenSocket" || String(candidate.message ?? "").includes("FailedToOpenSocket");
698
702
  }
699
703
 
704
+ /**
705
+ * Dev-only: every traffic replica is in the crashed terminal state, so a bare 503 would hide the
706
+ * boot error from the browser. Surface it, and reload once a fixed gateway takes over the port.
707
+ */
708
+ #respondWithCrashPage(req: Request): Response | null {
709
+ if (!this.#devHosted) return null;
710
+ const trafficChildren = [...this.#children.values()].filter((child) => child.role !== "batch");
711
+ if (trafficChildren.length === 0) return null;
712
+ if (!trafficChildren.every((child) => child.status === "crashed")) return null;
713
+ const detail =
714
+ trafficChildren.map((child) => child.lastErrorMessage ?? child.lastRestartReason).find(Boolean) ??
715
+ "unknown boot error";
716
+ const message = `Backend failed to start after ${AkanApp.#devMaxChildBootFailures} boot attempts: ${detail}`;
717
+ if (!req.headers.get("accept")?.includes("text/html")) {
718
+ return new Response(message, { status: 503, headers: { "cache-control": "no-store" } });
719
+ }
720
+ const html = `<!doctype html>
721
+ <html>
722
+ <head>
723
+ <meta charset="utf-8" />
724
+ <title>Backend failed to start</title>
725
+ <style>
726
+ body { margin: 0; padding: 48px 24px; background: #111827; color: #e5e7eb; font-family: ui-sans-serif, system-ui, sans-serif; }
727
+ main { max-width: 720px; margin: 0 auto; }
728
+ h1 { font-size: 20px; color: #f87171; }
729
+ pre { padding: 16px; border-radius: 8px; background: #1f2937; color: #fca5a5; white-space: pre-wrap; word-break: break-word; }
730
+ p { color: #9ca3af; font-size: 14px; }
731
+ </style>
732
+ </head>
733
+ <body>
734
+ <main>
735
+ <h1>Backend failed to start</h1>
736
+ <pre>${AkanApp.#escapeHtml(detail)}</pre>
737
+ <p>The dev server stopped retrying after ${AkanApp.#devMaxChildBootFailures} failed boots. Fix the error and save &mdash; this page reloads automatically.</p>
738
+ </main>
739
+ <script>
740
+ const poll = async () => {
741
+ try {
742
+ const res = await fetch(location.href, { cache: "no-store" });
743
+ if (res.ok) { location.reload(); return; }
744
+ } catch {}
745
+ setTimeout(poll, 1000);
746
+ };
747
+ setTimeout(poll, 1000);
748
+ </script>
749
+ </body>
750
+ </html>`;
751
+ return new Response(html, {
752
+ status: 503,
753
+ headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
754
+ });
755
+ }
756
+
757
+ static #escapeHtml(text: string) {
758
+ const replacements: Record<string, string> = {
759
+ "&": "&amp;",
760
+ "<": "&lt;",
761
+ ">": "&gt;",
762
+ '"': "&quot;",
763
+ "'": "&#39;",
764
+ };
765
+ return text.replace(/[&<>"']/g, (ch) => replacements[ch] ?? ch);
766
+ }
767
+
700
768
  /**
701
769
  * Gateway-observed upstream round-trip time. The pure proxy overhead is this value
702
770
  * minus the child handler time captured in the per-request trace.
@@ -1,4 +0,0 @@
1
- [AkanApp] 39364 - 07/21/2026, 04:19:57 AM INFO AkanApp gateway is running on port http://localhost:32283 +499ms
2
- [AkanApp] 39364 - 07/21/2026, 04:19:57 AM VERBOSE All 1 child process(es) are ready +512ms
3
- [AkanApp] 39364 - 07/21/2026, 04:19:57 AM ERROR Child 0/federation failed (exit:1); restarting in 1000ms (attempt 1) +565ms
4
- [AkanApp] 39364 - 07/21/2026, 04:19:58 AM VERBOSE All 1 child process(es) are ready +1582ms
@@ -1,5 +0,0 @@
1
- [AkanApp] 39364 - 07/21/2026, 04:19:58 AM INFO AkanApp gateway is running on port http://localhost:30883 +1587ms
2
- [AkanApp] 39364 - 07/21/2026, 04:19:58 AM VERBOSE All 1 child process(es) are ready +1599ms
3
- [AkanApp] 39364 - 07/21/2026, 04:19:58 AM ERROR Child 0/federation upstream is unreachable (/Users/kangminseon/github3/leading-flight-guidance/pkgs/akanjs/runtime/akan-child-39364-mru5abmz-0.sock); restarting +1740ms
4
- [AkanApp] 39364 - 07/21/2026, 04:19:58 AM ERROR Child 0/federation failed (upstream-open-failed); restarting in 1000ms (attempt 1) +1740ms
5
- [AkanApp] 39364 - 07/21/2026, 04:19:59 AM VERBOSE All 1 child process(es) are ready +2760ms
@@ -1,7 +0,0 @@
1
- [AkanApp] 39364 - 07/21/2026, 04:19:59 AM INFO AkanApp gateway is running on port http://localhost:24583 +2773ms
2
- [AkanApp] 39364 - 07/21/2026, 04:19:59 AM ERROR intentional-boot-failure +2783ms
3
- [AkanApp] 39364 - 07/21/2026, 04:19:59 AM ERROR Child 0/federation failed (child-error); restarting in 1000ms (attempt 1) +2783ms
4
- [AkanApp] 39364 - 07/21/2026, 04:20:00 AM ERROR intentional-boot-failure +3799ms
5
- [AkanApp] 39364 - 07/21/2026, 04:20:00 AM ERROR Child 0/federation failed (child-error); restarting in 2000ms (attempt 2) +3799ms
6
- [AkanApp] 39364 - 07/21/2026, 04:20:02 AM ERROR intentional-boot-failure +5817ms
7
- [AkanApp] 39364 - 07/21/2026, 04:20:02 AM ERROR [child-crash-loop] Backend replica 0/federation failed 3 consecutive boots; waiting for a code change to retry: intentional-boot-failure +5817ms
@@ -1,2 +0,0 @@
1
- [AkanApp] 39364 - 07/21/2026, 04:20:03 AM INFO AkanApp gateway is running on port http://localhost:24132 +7360ms
2
- [AkanApp] 39364 - 07/21/2026, 04:20:03 AM VERBOSE All 1 child process(es) are ready +7373ms