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.
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/cli/application/application.command.ts +4 -1
- package/cli/application/application.runner.ts +6 -8
- package/cli/build.ts +3 -1
- package/cli/cloud/cloud.runner.ts +7 -8
- package/cli/index.js +288 -115
- package/cli/library/library.runner.ts +2 -2
- package/cli/module/module.runner.ts +2 -2
- package/cli/npmRegistry.ts +13 -0
- package/cli/openBrowser.ts +15 -0
- package/cli/pluralizeName.ts +5 -0
- package/cli/scalar/scalar.prompt.ts +2 -2
- package/cli/scalar/scalar.runner.ts +2 -2
- package/cli/semver.ts +18 -0
- package/cli/templates/lib/sig.ts +2 -2
- package/cli/workspace/workspace.runner.ts +3 -3
- package/client/cookie.ts +10 -15
- package/common/index.ts +1 -0
- package/common/jwtDecode.ts +17 -0
- package/constant/serialize.ts +1 -1
- package/devkit/akanApp/akanApp.host.ts +46 -9
- package/devkit/akanConfig/akanConfig.ts +2 -1
- package/devkit/capacitor.base.config.ts +18 -4
- package/devkit/capacitorApp.ts +118 -64
- package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
- package/devkit/mobile/mobileTarget.ts +2 -1
- package/devkit/scanInfo.ts +1 -0
- package/document/dataLoader.ts +140 -6
- package/document/database.ts +1 -1
- package/package.json +7 -13
- package/server/akanApp.ts +250 -44
- package/server/di/diLifecycle.ts +1 -1
- package/server/processMetricsCollector.ts +79 -1
- package/server/proxy/localeWebProxy.ts +29 -12
- package/server/resolver/database.resolver.ts +82 -31
- package/server/resolver/signal.resolver.ts +67 -28
- package/service/ipcTypes.ts +5 -0
- package/service/predefinedAdaptor/database.adaptor.ts +95 -27
- package/service/predefinedAdaptor/solidSqlite.ts +7 -7
- package/service/predefinedAdaptor/storage.adaptor.ts +35 -9
- package/service/serviceModule.ts +1 -6
- package/signal/base.signal.ts +1 -1
- package/signal/index.ts +1 -0
- package/signal/middleware.ts +5 -1
- package/signal/signalContext.ts +85 -31
- package/signal/signalRegistry.ts +35 -10
- package/signal/trace.ts +279 -0
- package/types/cli/npmRegistry.d.ts +1 -0
- package/types/cli/openBrowser.d.ts +1 -0
- package/types/cli/pluralizeName.d.ts +1 -0
- package/types/cli/semver.d.ts +1 -0
- package/types/client/cookie.d.ts +6 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/jwtDecode.d.ts +2 -0
- package/types/devkit/capacitorApp.d.ts +14 -5
- package/types/devkit/incrementalBuilder/incrementalBuilder.host.d.ts +9 -5
- package/types/document/dataLoader.d.ts +21 -2
- package/types/document/database.d.ts +1 -1
- package/types/server/processMetricsCollector.d.ts +2 -0
- package/types/service/ipcTypes.d.ts +5 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +26 -32
- package/types/service/predefinedAdaptor/solidSqlite.d.ts +3 -3
- package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -2
- package/types/service/serviceModule.d.ts +1 -1
- package/types/signal/index.d.ts +1 -0
- package/types/signal/signalContext.d.ts +4 -1
- package/types/signal/signalRegistry.d.ts +25 -4
- package/types/signal/trace.d.ts +97 -0
- package/types/ui/Signal/style.d.ts +15 -0
- package/ui/Signal/Arg.tsx +22 -15
- package/ui/Signal/Doc.tsx +30 -24
- package/ui/Signal/Listener.tsx +15 -39
- package/ui/Signal/Message.tsx +32 -50
- package/ui/Signal/Object.tsx +16 -13
- package/ui/Signal/PubSub.tsx +29 -47
- package/ui/Signal/Response.tsx +7 -17
- package/ui/Signal/RestApi.tsx +41 -57
- package/ui/Signal/WebSocket.tsx +1 -1
- package/ui/Signal/style.ts +36 -0
- package/webkit/useCsrValues.ts +147 -37
package/document/database.ts
CHANGED
|
@@ -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 "
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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",
|
|
525
|
-
headers.set("x-forwarded-proto", url.
|
|
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.
|
|
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 =
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
child
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
|
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
|
|
761
|
-
void this.
|
|
946
|
+
this.#invalidateFederationChildCache();
|
|
947
|
+
void this.#scheduleChildRestart(child, child.proc, "health-timeout");
|
|
762
948
|
return;
|
|
763
949
|
}
|
|
764
|
-
child
|
|
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
|
|
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
|
|
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.
|
|
797
|
-
|
|
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,
|
package/server/di/diLifecycle.ts
CHANGED
|
@@ -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();
|