akanjs 2.0.5 → 2.0.7

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 (81) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/cli/application/application.command.ts +4 -1
  4. package/cli/application/application.runner.ts +6 -8
  5. package/cli/build.ts +3 -1
  6. package/cli/cloud/cloud.runner.ts +7 -8
  7. package/cli/index.js +288 -115
  8. package/cli/library/library.runner.ts +2 -2
  9. package/cli/module/module.runner.ts +2 -2
  10. package/cli/npmRegistry.ts +13 -0
  11. package/cli/openBrowser.ts +15 -0
  12. package/cli/pluralizeName.ts +5 -0
  13. package/cli/scalar/scalar.prompt.ts +2 -2
  14. package/cli/scalar/scalar.runner.ts +2 -2
  15. package/cli/semver.ts +18 -0
  16. package/cli/templates/lib/sig.ts +2 -2
  17. package/cli/workspace/workspace.runner.ts +3 -3
  18. package/client/cookie.ts +10 -15
  19. package/common/index.ts +1 -0
  20. package/common/jwtDecode.ts +17 -0
  21. package/constant/serialize.ts +1 -1
  22. package/devkit/akanApp/akanApp.host.ts +46 -9
  23. package/devkit/akanConfig/akanConfig.ts +2 -1
  24. package/devkit/capacitor.base.config.ts +18 -4
  25. package/devkit/capacitorApp.ts +118 -64
  26. package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
  27. package/devkit/mobile/mobileTarget.ts +2 -1
  28. package/devkit/scanInfo.ts +1 -0
  29. package/document/dataLoader.ts +140 -6
  30. package/document/database.ts +1 -1
  31. package/package.json +7 -13
  32. package/server/akanApp.ts +250 -44
  33. package/server/di/diLifecycle.ts +1 -1
  34. package/server/processMetricsCollector.ts +79 -1
  35. package/server/proxy/localeWebProxy.ts +29 -12
  36. package/server/resolver/database.resolver.ts +82 -31
  37. package/server/resolver/signal.resolver.ts +67 -28
  38. package/service/ipcTypes.ts +5 -0
  39. package/service/predefinedAdaptor/database.adaptor.ts +95 -27
  40. package/service/predefinedAdaptor/solidSqlite.ts +7 -7
  41. package/service/predefinedAdaptor/storage.adaptor.ts +35 -9
  42. package/service/serviceModule.ts +1 -6
  43. package/signal/base.signal.ts +1 -1
  44. package/signal/index.ts +1 -0
  45. package/signal/middleware.ts +5 -1
  46. package/signal/signalContext.ts +85 -31
  47. package/signal/signalRegistry.ts +35 -10
  48. package/signal/trace.ts +279 -0
  49. package/types/cli/npmRegistry.d.ts +1 -0
  50. package/types/cli/openBrowser.d.ts +1 -0
  51. package/types/cli/pluralizeName.d.ts +1 -0
  52. package/types/cli/semver.d.ts +1 -0
  53. package/types/client/cookie.d.ts +6 -1
  54. package/types/common/index.d.ts +1 -0
  55. package/types/common/jwtDecode.d.ts +2 -0
  56. package/types/devkit/capacitorApp.d.ts +14 -5
  57. package/types/devkit/incrementalBuilder/incrementalBuilder.host.d.ts +9 -5
  58. package/types/document/dataLoader.d.ts +21 -2
  59. package/types/document/database.d.ts +1 -1
  60. package/types/server/processMetricsCollector.d.ts +2 -0
  61. package/types/service/ipcTypes.d.ts +5 -0
  62. package/types/service/predefinedAdaptor/database.adaptor.d.ts +26 -32
  63. package/types/service/predefinedAdaptor/solidSqlite.d.ts +3 -3
  64. package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -2
  65. package/types/service/serviceModule.d.ts +1 -1
  66. package/types/signal/index.d.ts +1 -0
  67. package/types/signal/signalContext.d.ts +4 -1
  68. package/types/signal/signalRegistry.d.ts +25 -4
  69. package/types/signal/trace.d.ts +97 -0
  70. package/types/ui/Signal/style.d.ts +15 -0
  71. package/ui/Signal/Arg.tsx +22 -15
  72. package/ui/Signal/Doc.tsx +30 -24
  73. package/ui/Signal/Listener.tsx +15 -39
  74. package/ui/Signal/Message.tsx +32 -50
  75. package/ui/Signal/Object.tsx +16 -13
  76. package/ui/Signal/PubSub.tsx +29 -47
  77. package/ui/Signal/Response.tsx +7 -17
  78. package/ui/Signal/RestApi.tsx +41 -57
  79. package/ui/Signal/WebSocket.tsx +1 -1
  80. package/ui/Signal/style.ts +36 -0
  81. package/webkit/useCsrValues.ts +147 -37
@@ -2,7 +2,7 @@ import type { Dayjs, MergedValues, PromiseOrObject } from "akanjs/base";
2
2
  import { Logger } from "akanjs/common";
3
3
  import type { DocumentModel, QueryOf } from "akanjs/constant";
4
4
  import type { CacheAdaptor } from "akanjs/service";
5
- import type DataLoader from "dataloader";
5
+ import type { DataLoader } from "./dataLoader";
6
6
  import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta";
7
7
  import type { CRUDEventType, Mdl, SaveEventType } from "./into";
8
8
  import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "bin": {
@@ -162,7 +162,6 @@
162
162
  }
163
163
  },
164
164
  "dependencies": {
165
- "@formatjs/intl-localematcher": "^0.8.8",
166
165
  "@inquirer/prompts": "^8.4.3",
167
166
  "@langchain/core": "^1.1.47",
168
167
  "@langchain/deepseek": "^1.0.26",
@@ -171,29 +170,19 @@
171
170
  "chalk": "^5.6.2",
172
171
  "clsx": "^2.1.1",
173
172
  "commander": "^14.0.3",
174
- "compare-versions": "^6.1.1",
175
- "dataloader": "^2.2.3",
176
173
  "dayjs": "^1.11.20",
177
174
  "fontaine": "^0.8.0",
178
175
  "fonteditor-core": "^2.6.3",
179
176
  "ignore": "^7.0.5",
180
177
  "immer": "^11.1.8",
181
178
  "ink": "^6.8.0",
182
- "js-cookie": "^3.0.7",
183
179
  "js-yaml": "^4.1.1",
184
- "jwt-decode": "^4.0.0",
185
- "latest-version": "^9.0.0",
186
- "lodash": "^4.18.1",
187
- "negotiator": "^1.0.0",
188
- "open": "^11.0.0",
189
180
  "ora": "^9.4.0",
190
- "pluralize": "^8.0.0",
191
181
  "qrcode": "^1.5.4",
192
182
  "sharp": "^0.34.5",
193
183
  "ssh2": "^1.17.0",
194
184
  "subset-font": "^2.5.0",
195
- "tailwindcss": "^4.3.0",
196
- "uuid": "^13.0.2"
185
+ "tailwindcss": "^4.3.0"
197
186
  },
198
187
  "peerDependencies": {
199
188
  "@capacitor-community/contacts": "^7.2.0",
@@ -222,6 +211,8 @@
222
211
  "croner": "^10.0.1",
223
212
  "daisyui": "^5.5.20",
224
213
  "file-saver": "^2.0.5",
214
+ "fontaine": "^0.8.0",
215
+ "fonteditor-core": "^2.6.3",
225
216
  "ioredis": "^5.10.1",
226
217
  "mermaid": "^11.15.0",
227
218
  "postgres": "^3.4.9",
@@ -237,7 +228,10 @@
237
228
  "react-simple-pull-to-refresh": "^1.3.4",
238
229
  "react-spring": "^9.7.5",
239
230
  "scheduler": "^0.27.0",
231
+ "sharp": "^0.34.5",
232
+ "subset-font": "^2.5.0",
240
233
  "tailwind-scrollbar": "^4.0.2",
234
+ "tailwindcss": "^4.3.0",
241
235
  "typescript": "^6.0.3"
242
236
  },
243
237
  "peerDependenciesMeta": {
package/server/akanApp.ts CHANGED
@@ -2,6 +2,7 @@ import { mkdir, rm } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { Logger } from "akanjs/common";
4
4
  import type { AkanChildRole, AkanChildStatus, AkanIpcMessage, AkanMetricsReport, AkanUpstream } from "akanjs/service";
5
+ import { isTraceEnabled } from "akanjs/signal";
5
6
  import type { BuilderMessage, BuilderReq, BuilderRes } from "./artifact";
6
7
  import { RotatingLogWriter } from "./logging/rotatingLogWriter";
7
8
  import { ProcessMetricsCollector } from "./processMetricsCollector";
@@ -17,6 +18,13 @@ interface ChildState {
17
18
  healthPath?: string;
18
19
  metrics: AkanMetricsReport;
19
20
  lastPongAt?: number;
21
+ restartAttempts: number;
22
+ restartCount: number;
23
+ restartTimer: Timer | null;
24
+ restartPending: boolean;
25
+ lastExitCode?: number | null;
26
+ lastRestartAt?: number;
27
+ lastRestartReason?: string;
20
28
  }
21
29
 
22
30
  interface GatewayWsData {
@@ -49,6 +57,9 @@ interface AkanReplicaConfig {
49
57
 
50
58
  /** Gateway/orchestrator that starts Akan child servers and proxies HTTP/WebSocket traffic. */
51
59
  export class AkanApp {
60
+ static readonly #childRestartBaseDelayMs = 1_000;
61
+ static readonly #childRestartMaxDelayMs = 30_000;
62
+ static readonly #childRestartGraceMs = 5_000;
52
63
  static readonly #hopByHopHeaders = new Set([
53
64
  "connection",
54
65
  "keep-alive",
@@ -75,6 +86,7 @@ export class AkanApp {
75
86
  readonly #builderReqMap = new Map<number, { childIdx: number; childLocalId: number }>();
76
87
  #server: Bun.Server<GatewayWsData> | null = null;
77
88
  #rrIdx = 0;
89
+ #federationChildCache: ChildState[] | null = null;
78
90
  #snapshotTimer: Timer | null = null;
79
91
  #healthTimer: Timer | null = null;
80
92
  #metricsTimer: Timer | null = null;
@@ -83,6 +95,9 @@ export class AkanApp {
83
95
  readonly #childOutputBuffers = new Map<string, string>();
84
96
  static readonly #ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
85
97
  #gatewayMetrics: AkanMetricsReport = {};
98
+ #proxyHopCount = 0;
99
+ #proxyHopSumMs = 0;
100
+ #proxyHopMaxMs = 0;
86
101
  #resolveStopped: (() => void) | null = null;
87
102
  #exitAfterStop = false;
88
103
  #stopping = false;
@@ -171,7 +186,11 @@ export class AkanApp {
171
186
  this.#server?.stop(true);
172
187
  this.#server = null;
173
188
  for (const child of this.#children.values()) {
174
- if (!child.proc.killed) child.proc.send({ type: "shutdown", signal } satisfies AkanIpcMessage);
189
+ if (child.restartTimer) {
190
+ clearTimeout(child.restartTimer);
191
+ child.restartTimer = null;
192
+ }
193
+ this.#sendToChild(child, { type: "shutdown", signal } satisfies AkanIpcMessage);
175
194
  }
176
195
  await Promise.race([
177
196
  Promise.all([...this.#children.values()].map((child) => child.proc.exited.catch(() => undefined))),
@@ -200,7 +219,8 @@ export class AkanApp {
200
219
  const role = this.#getRole(idx);
201
220
  const upstream = this.#getChildUpstream(idx, role);
202
221
  const childCode = `import(${JSON.stringify(path.resolve(this.#serverPath))}).then((mod)=>{ const server = mod.server ?? mod.app; if (!server?.start) throw new Error("server.ts must export server or app with start()"); return server.start({ listen: process.env.SERVER_MODE !== "batch" }); }).catch((error)=>{ process.send?.({ type: "error", message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, pid: process.pid }); process.exit(1); });`;
203
- const proc = Bun.spawn(["bun", "-e", childCode], {
222
+ let proc!: Bun.Subprocess<"ignore", "pipe", "pipe">;
223
+ proc = Bun.spawn(["bun", "-e", childCode], {
204
224
  cwd: process.cwd(),
205
225
  env: {
206
226
  ...process.env,
@@ -212,32 +232,143 @@ export class AkanApp {
212
232
  AKAN_CHILD_SOCKET: upstream.http.socketPath,
213
233
  AKAN_CHILD_WS_PORT: upstream.ws ? String(upstream.ws.port) : "",
214
234
  },
215
- ipc: (message) => this.#handleMessage(idx, message as AkanIpcMessage),
235
+ ipc: (message) => this.#handleMessage(idx, message as AkanIpcMessage, proc),
216
236
  stdout: "pipe",
217
237
  stderr: "pipe",
218
238
  stdin: "ignore",
219
239
  });
220
- this.#children.set(idx, { idx, role, proc, ready: false, status: "starting", metrics: {} });
240
+ const previous = this.#children.get(idx);
241
+ this.#children.set(idx, {
242
+ idx,
243
+ role,
244
+ proc,
245
+ ready: false,
246
+ status: "starting",
247
+ metrics: {},
248
+ restartAttempts: previous?.restartAttempts ?? 0,
249
+ restartCount: previous?.restartCount ?? 0,
250
+ restartTimer: null,
251
+ restartPending: false,
252
+ lastExitCode: previous?.lastExitCode,
253
+ lastRestartAt: previous?.lastRestartAt,
254
+ lastRestartReason: previous?.lastRestartReason,
255
+ });
256
+ this.#invalidateFederationChildCache();
221
257
  this.#pipeOutput(idx, role, proc.stdout, "stdout");
222
258
  this.#pipeOutput(idx, role, proc.stderr, "stderr");
223
- proc.exited.then((code) => {
224
- const child = this.#children.get(idx);
225
- if (child) child.status = "exited";
226
- this.#removeChildRooms(idx);
227
- if (this.#stopping) return;
228
- this.logger.error(`Child ${idx}/${role} exited with code ${code}; shutting down all children`);
229
- void this.stop("child-exit");
259
+ proc.exited.then((code) => this.#handleChildExit(idx, proc, code));
260
+ }
261
+
262
+ #handleChildExit(idx: number, proc: Bun.Subprocess<"ignore", "pipe", "pipe">, code: number | null) {
263
+ const child = this.#children.get(idx);
264
+ if (!child || child.proc !== proc) return;
265
+ child.status = "exited";
266
+ child.lastExitCode = code;
267
+ this.#invalidateFederationChildCache();
268
+ this.#removeChildRooms(idx);
269
+ if (this.#stopping) return;
270
+ void this.#scheduleChildRestart(child, proc, `exit:${code ?? "unknown"}`);
271
+ }
272
+
273
+ #scheduleChildRestart(child: ChildState, proc: Bun.Subprocess<"ignore", "pipe", "pipe">, reason: string): void {
274
+ if (this.#stopping) return;
275
+ if (child.proc !== proc) return;
276
+ if (child.restartPending || child.restartTimer) {
277
+ child.lastRestartReason = reason;
278
+ return;
279
+ }
280
+
281
+ child.restartPending = true;
282
+ child.ready = false;
283
+ child.status = reason === "health-timeout" ? "unhealthy" : "exited";
284
+ child.upstream = undefined;
285
+ child.healthPath = undefined;
286
+ this.#invalidateFederationChildCache();
287
+ child.lastRestartReason = reason;
288
+ child.lastRestartAt = Date.now();
289
+ this.#removeChildRooms(child.idx);
290
+
291
+ const attempt = child.restartAttempts;
292
+ const delay = Math.min(AkanApp.#childRestartBaseDelayMs * 2 ** attempt, AkanApp.#childRestartMaxDelayMs);
293
+ child.restartAttempts = attempt + 1;
294
+ child.restartCount += 1;
295
+ this.logger.error(
296
+ `Child ${child.idx}/${child.role} failed (${reason}); restarting in ${delay}ms (attempt ${child.restartAttempts})`,
297
+ );
298
+
299
+ void this.#restartChildAfterDelay(child.idx, proc, reason, delay).catch((error) => {
300
+ const current = this.#children.get(child.idx);
301
+ if (!current || current.proc !== proc || this.#stopping) return;
302
+ current.restartPending = false;
303
+ this.logger.error(
304
+ `Failed to restart child ${child.idx}/${child.role}: ${error instanceof Error ? error.message : String(error)}`,
305
+ );
306
+ this.#scheduleChildRestart(current, proc, "restart-failed");
230
307
  });
231
308
  }
232
309
 
310
+ async #restartChildAfterDelay(
311
+ idx: number,
312
+ proc: Bun.Subprocess<"ignore", "pipe", "pipe">,
313
+ reason: string,
314
+ delay: number,
315
+ ) {
316
+ const child = this.#children.get(idx);
317
+ if (!child || child.proc !== proc || this.#stopping) return;
318
+ await this.#stopChildForRestart(child, proc, reason);
319
+ if (this.#stopping) return;
320
+ const current = this.#children.get(idx);
321
+ if (!current || current.proc !== proc) return;
322
+ current.restartTimer = setTimeout(() => {
323
+ current.restartTimer = null;
324
+ void this.#respawnChild(idx, proc).catch((error) => {
325
+ const latest = this.#children.get(idx);
326
+ if (!latest || latest.proc !== proc || this.#stopping) return;
327
+ latest.restartPending = false;
328
+ this.logger.error(
329
+ `Failed to respawn child ${idx}/${latest.role}: ${error instanceof Error ? error.message : String(error)}`,
330
+ );
331
+ this.#scheduleChildRestart(latest, proc, "respawn-failed");
332
+ });
333
+ }, delay);
334
+ }
335
+
336
+ async #respawnChild(idx: number, proc: Bun.Subprocess<"ignore", "pipe", "pipe">) {
337
+ if (this.#stopping) return;
338
+ const current = this.#children.get(idx);
339
+ if (!current || current.proc !== proc) return;
340
+ await this.#removeChildSocket(idx, current.role);
341
+ current.restartPending = false;
342
+ this.#spawn(idx);
343
+ }
344
+
345
+ async #stopChildForRestart(child: ChildState, proc: Bun.Subprocess<"ignore", "pipe", "pipe">, reason: string) {
346
+ if (!proc.killed) {
347
+ this.#sendToChild(child, { type: "shutdown", signal: reason } satisfies AkanIpcMessage);
348
+ }
349
+ const result = await Promise.race([
350
+ proc.exited.then(() => "exited" as const).catch(() => "exited" as const),
351
+ new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), AkanApp.#childRestartGraceMs)),
352
+ ]);
353
+ if (result === "timeout" && !proc.killed) {
354
+ this.logger.warn(`Child ${child.idx}/${child.role} did not stop in ${AkanApp.#childRestartGraceMs}ms; killing`);
355
+ proc.kill();
356
+ await proc.exited.catch(() => undefined);
357
+ }
358
+ }
359
+
233
360
  async #prepareRuntimeDir() {
234
361
  await mkdir(this.#runtimeDir, { recursive: true });
235
362
  for (let idx = 0; idx < this.#replica.total; idx++) {
236
- const socketPath = this.#getChildUpstream(idx, this.#getRole(idx)).http.socketPath;
237
- try {
238
- await rm(socketPath, { force: true });
239
- } catch {
240
- }
363
+ await this.#removeChildSocket(idx, this.#getRole(idx));
364
+ }
365
+ }
366
+
367
+ async #removeChildSocket(idx: number, role: AkanChildRole) {
368
+ const socketPath = this.#getChildUpstream(idx, role).http.socketPath;
369
+ try {
370
+ await rm(socketPath, { force: true });
371
+ } catch {
241
372
  }
242
373
  }
243
374
 
@@ -280,6 +411,7 @@ export class AkanApp {
280
411
  const url = new URL(req.url);
281
412
  if (url.pathname === "/_akan/app/health") return Response.json(this.#getHealthStatus());
282
413
  if (url.pathname === "/_akan/app/metrics") return Response.json(this.#getMetricsStatus());
414
+ if (url.pathname === "/_akan/bench/ping") return new Response("ok");
283
415
  if (this.#isWebSocketPath(url.pathname)) return this.#upgradeWebSocket(req, server);
284
416
  const assetResponse = await this.#serveImmutableArtifact(req, url);
285
417
  if (assetResponse) return assetResponse;
@@ -388,6 +520,12 @@ export class AkanApp {
388
520
  ready: child.ready,
389
521
  pid: child.pid,
390
522
  upstream: child.upstream,
523
+ restartAttempts: child.restartAttempts,
524
+ restartCount: child.restartCount,
525
+ restartPending: child.restartPending,
526
+ lastExitCode: child.lastExitCode,
527
+ lastRestartAt: child.lastRestartAt,
528
+ lastRestartReason: child.lastRestartReason,
391
529
  })),
392
530
  };
393
531
  }
@@ -397,11 +535,24 @@ export class AkanApp {
397
535
  rooms: this.#roomChildren.size,
398
536
  sockets: this.#socketRooms.size,
399
537
  gateway: this.#gatewayMetrics,
538
+ proxyHop: this.#proxyHopCount
539
+ ? {
540
+ count: this.#proxyHopCount,
541
+ meanMs: Math.round((this.#proxyHopSumMs / this.#proxyHopCount) * 1000) / 1000,
542
+ maxMs: Math.round(this.#proxyHopMaxMs * 1000) / 1000,
543
+ }
544
+ : null,
400
545
  children: [...this.#children.values()].map((child) => ({
401
546
  idx: child.idx,
402
547
  role: child.role,
403
548
  metrics: child.metrics,
404
549
  rooms: this.#childRooms.get(child.idx)?.size ?? 0,
550
+ restartAttempts: child.restartAttempts,
551
+ restartCount: child.restartCount,
552
+ restartPending: child.restartPending,
553
+ lastExitCode: child.lastExitCode,
554
+ lastRestartAt: child.lastRestartAt,
555
+ lastRestartReason: child.lastRestartReason,
405
556
  })),
406
557
  };
407
558
  }
@@ -416,6 +567,8 @@ export class AkanApp {
416
567
  const headers = this.#makeProxyHeaders(req, child.idx);
417
568
  child.metrics.activeRequests = (child.metrics.activeRequests ?? 0) + 1;
418
569
  child.metrics.totalRequests = (child.metrics.totalRequests ?? 0) + 1;
570
+ const traced = isTraceEnabled();
571
+ const hopStart = traced ? performance.now() : 0;
419
572
  try {
420
573
  const upstreamRes = await fetch(upstreamUrl, {
421
574
  unix: child.upstream.socketPath,
@@ -428,9 +581,20 @@ export class AkanApp {
428
581
  return this.#proxyResponse(upstreamRes);
429
582
  } finally {
430
583
  child.metrics.activeRequests = Math.max(0, (child.metrics.activeRequests ?? 1) - 1);
584
+ if (traced) this.#recordProxyHop(performance.now() - hopStart);
431
585
  }
432
586
  }
433
587
 
588
+ /**
589
+ * Gateway-observed upstream round-trip time. The pure proxy overhead is this value
590
+ * minus the child handler time captured in the per-request trace.
591
+ */
592
+ #recordProxyHop(durationMs: number) {
593
+ this.#proxyHopCount += 1;
594
+ this.#proxyHopSumMs += durationMs;
595
+ this.#proxyHopMaxMs = Math.max(this.#proxyHopMaxMs, durationMs);
596
+ }
597
+
434
598
  #proxyResponse(upstreamRes: Response): Response {
435
599
  const headers = new Headers(upstreamRes.headers);
436
600
 
@@ -517,27 +681,36 @@ export class AkanApp {
517
681
  #makeProxyHeaders(req: Request, childIdx: number) {
518
682
  const headers = new Headers(req.headers);
519
683
  for (const key of AkanApp.#hopByHopHeaders) headers.delete(key);
520
- const url = new URL(req.url);
521
684
  const forwardedFor = headers.get("x-forwarded-for");
522
685
  const clientAddress = headers.get("x-real-ip") ?? "127.0.0.1";
686
+ const host = headers.get("host");
523
687
  headers.set("x-forwarded-for", forwardedFor ? `${forwardedFor}, ${clientAddress}` : clientAddress);
524
- headers.set("x-forwarded-host", headers.get("host") ?? url.host);
525
- headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
688
+ headers.set("x-forwarded-host", host ?? new URL(req.url).host);
689
+ headers.set("x-forwarded-proto", req.url.startsWith("https:") ? "https" : "http");
526
690
  headers.set("x-akan-child-idx", String(childIdx));
527
- headers.set("x-request-id", headers.get("x-request-id") ?? crypto.randomUUID());
691
+ if (!headers.has("x-request-id") && process.env.AKAN_BENCH_SKIP_REQUEST_ID !== "1") {
692
+ headers.set("x-request-id", crypto.randomUUID());
693
+ }
528
694
  headers.set("host", "akan-child");
529
695
  return headers;
530
696
  }
531
697
 
698
+ #invalidateFederationChildCache() {
699
+ this.#federationChildCache = null;
700
+ }
701
+
532
702
  #pickFederationChild() {
533
- const candidates = [...this.#children.values()].filter(
534
- (child) =>
535
- (child.role === "federation" || child.role === "all") &&
536
- child.ready &&
537
- child.status !== "unhealthy" &&
538
- child.status !== "exited" &&
539
- !child.proc.killed,
540
- );
703
+ const candidates =
704
+ this.#federationChildCache ??
705
+ [...this.#children.values()].filter(
706
+ (child) =>
707
+ (child.role === "federation" || child.role === "all") &&
708
+ child.ready &&
709
+ child.status !== "unhealthy" &&
710
+ child.status !== "exited" &&
711
+ !child.proc.killed,
712
+ );
713
+ this.#federationChildCache = candidates;
541
714
  if (candidates.length === 0) return null;
542
715
  const child = candidates[this.#rrIdx % candidates.length];
543
716
  this.#rrIdx++;
@@ -557,7 +730,13 @@ export class AkanApp {
557
730
  };
558
731
  }
559
732
 
560
- #handleMessage(idx: number, message: AkanIpcMessage | BuilderMessage) {
733
+ #handleMessage(
734
+ idx: number,
735
+ message: AkanIpcMessage | BuilderMessage,
736
+ proc?: Bun.Subprocess<"ignore", "pipe", "pipe">,
737
+ ) {
738
+ const child = this.#children.get(idx);
739
+ if (proc && (!child || child.proc !== proc)) return;
561
740
  if (!message || typeof message !== "object") return;
562
741
  switch (message.type) {
563
742
  case "ready":
@@ -586,7 +765,7 @@ export class AkanApp {
586
765
  return;
587
766
  case "error":
588
767
  this.logger.error(message.message);
589
- void this.stop("child-error");
768
+ if (child && proc) void this.#scheduleChildRestart(child, proc, "child-error");
590
769
  return;
591
770
  case "build-route":
592
771
  this.#forwardBuildRoute(idx, message);
@@ -618,6 +797,9 @@ export class AkanApp {
618
797
  child.upstream = message.upstream;
619
798
  child.healthPath = message.healthPath;
620
799
  child.lastPongAt = Date.now();
800
+ child.restartAttempts = 0;
801
+ child.restartPending = false;
802
+ this.#invalidateFederationChildCache();
621
803
  if ([...this.#children.values()].every((item) => item.ready)) {
622
804
  process.send?.({ type: "backend-ready", pid: process.pid } satisfies AkanIpcMessage);
623
805
  this.logger.verbose(`All ${this.#children.size} child process(es) are ready`);
@@ -629,6 +811,7 @@ export class AkanApp {
629
811
  if (!child) return;
630
812
  child.status = "healthy";
631
813
  child.lastPongAt = Date.now();
814
+ this.#invalidateFederationChildCache();
632
815
  }
633
816
 
634
817
  #deliverPubsub(originIdx: number, message: Extract<AkanIpcMessage, { type: "pubsub.publish" }>) {
@@ -643,12 +826,15 @@ export class AkanApp {
643
826
  if (childIdx === originIdx) continue;
644
827
  const child = this.#children.get(childIdx);
645
828
  if (!child || child.proc.killed || child.status === "exited") continue;
646
- child.proc.send({
647
- type: "pubsub.deliver",
648
- roomId: message.roomId,
649
- data: message.data,
650
- origin: message.origin,
651
- } satisfies AkanIpcMessage);
829
+ if (
830
+ !this.#sendToChild(child, {
831
+ type: "pubsub.deliver",
832
+ roomId: message.roomId,
833
+ data: message.data,
834
+ origin: message.origin,
835
+ } satisfies AkanIpcMessage)
836
+ )
837
+ continue;
652
838
  child.metrics.pubsubDeliverCount = (child.metrics.pubsubDeliverCount ?? 0) + 1;
653
839
  }
654
840
  }
@@ -747,7 +933,7 @@ export class AkanApp {
747
933
  #requestRoomSnapshots() {
748
934
  for (const child of this.#children.values()) {
749
935
  if (child.proc.killed || child.status === "exited") continue;
750
- child.proc.send({ type: "pubsub.snapshot.request" } satisfies AkanIpcMessage);
936
+ this.#sendToChild(child, { type: "pubsub.snapshot.request" } satisfies AkanIpcMessage);
751
937
  }
752
938
  }
753
939
 
@@ -757,11 +943,15 @@ export class AkanApp {
757
943
  if (child.proc.killed || child.status === "exited") continue;
758
944
  if (child.lastPongAt && now - child.lastPongAt > 5_000) {
759
945
  child.status = "unhealthy";
760
- this.logger.error(`Child ${child.idx}/${child.role} health check timed out; shutting down all children`);
761
- void this.stop("health-timeout");
946
+ this.#invalidateFederationChildCache();
947
+ void this.#scheduleChildRestart(child, child.proc, "health-timeout");
762
948
  return;
763
949
  }
764
- child.proc.send({ type: "health.ping", nonce: crypto.randomUUID(), sentAt: now } satisfies AkanIpcMessage);
950
+ this.#sendToChild(child, {
951
+ type: "health.ping",
952
+ nonce: crypto.randomUUID(),
953
+ sentAt: now,
954
+ } satisfies AkanIpcMessage);
765
955
  }
766
956
  }
767
957
 
@@ -780,25 +970,41 @@ export class AkanApp {
780
970
  this.#builderReqMap.delete(message.id);
781
971
  const child = this.#children.get(request.childIdx);
782
972
  if (!child || child.proc.killed) return;
783
- child.proc.send({ ...message, id: request.childLocalId } satisfies BuilderRes);
973
+ this.#sendToChild(child, { ...message, id: request.childLocalId } satisfies BuilderRes);
784
974
  }
785
975
 
786
976
  #fanoutToFederation(message: AkanIpcMessage | BuilderMessage, exceptIdx?: number) {
787
977
  for (const child of this.#children.values()) {
788
978
  if (child.idx === exceptIdx) continue;
789
- if (child.role === "federation" || child.role === "all") child.proc.send(message);
979
+ if (child.role === "federation" || child.role === "all") this.#sendToChild(child, message);
790
980
  }
791
981
  }
792
982
 
793
983
  #fanoutToBatch(message: AkanIpcMessage) {
794
984
  for (const child of this.#children.values()) {
795
985
  if (child.role === "batch" || child.role === "all") {
796
- child.proc.send(message);
797
- if (message.type === "queue.wake") child.metrics.queueWakeCount = (child.metrics.queueWakeCount ?? 0) + 1;
986
+ if (this.#sendToChild(child, message) && message.type === "queue.wake") {
987
+ child.metrics.queueWakeCount = (child.metrics.queueWakeCount ?? 0) + 1;
988
+ }
798
989
  }
799
990
  }
800
991
  }
801
992
 
993
+ #sendToChild(child: ChildState, message: AkanIpcMessage | BuilderMessage): boolean {
994
+ if (child.proc.killed || child.status === "exited") return false;
995
+ try {
996
+ child.proc.send(message);
997
+ return true;
998
+ } catch (error) {
999
+ this.logger.warn(
1000
+ `Failed to send ${message.type} to child ${child.idx}/${child.role}: ${
1001
+ error instanceof Error ? error.message : String(error)
1002
+ }`,
1003
+ );
1004
+ return false;
1005
+ }
1006
+ }
1007
+
802
1008
  async #pipeOutput(
803
1009
  idx: number,
804
1010
  role: AkanChildRole,
@@ -80,7 +80,7 @@ export class DiLifecycle {
80
80
  this.#libs = libs;
81
81
  this.#service.set("base", {
82
82
  service: srv.base,
83
- signal: SignalRegistry.registerService(BaseInternal, BaseEndpoint, Base),
83
+ signal: SignalRegistry.registerService("base" as const, BaseInternal, BaseEndpoint, Base),
84
84
  });
85
85
  this.#middleware.set(Logging.refName, Logging);
86
86
  const defaultOption = createDefaultAkanOption();