akanjs 2.0.5 → 2.0.6
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/cli/application/application.runner.ts +1 -1
- package/cli/build.ts +2 -1
- package/cli/cloud/cloud.runner.ts +7 -8
- package/cli/index.js +176 -43
- 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/devkit/akanApp/akanApp.host.ts +46 -9
- package/devkit/akanConfig/akanConfig.ts +2 -1
- package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
- package/document/dataLoader.ts +140 -6
- package/document/database.ts +1 -1
- package/package.json +7 -13
- package/server/akanApp.ts +197 -32
- package/server/di/diLifecycle.ts +1 -1
- package/server/proxy/localeWebProxy.ts +29 -12
- package/service/serviceModule.ts +1 -6
- package/signal/base.signal.ts +1 -1
- package/signal/signalRegistry.ts +35 -10
- 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/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/service/serviceModule.d.ts +1 -1
- package/types/signal/signalRegistry.d.ts +25 -4
- package/ui/Signal/Doc.tsx +2 -3
package/server/akanApp.ts
CHANGED
|
@@ -17,6 +17,13 @@ interface ChildState {
|
|
|
17
17
|
healthPath?: string;
|
|
18
18
|
metrics: AkanMetricsReport;
|
|
19
19
|
lastPongAt?: number;
|
|
20
|
+
restartAttempts: number;
|
|
21
|
+
restartCount: number;
|
|
22
|
+
restartTimer: Timer | null;
|
|
23
|
+
restartPending: boolean;
|
|
24
|
+
lastExitCode?: number | null;
|
|
25
|
+
lastRestartAt?: number;
|
|
26
|
+
lastRestartReason?: string;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
interface GatewayWsData {
|
|
@@ -49,6 +56,9 @@ interface AkanReplicaConfig {
|
|
|
49
56
|
|
|
50
57
|
/** Gateway/orchestrator that starts Akan child servers and proxies HTTP/WebSocket traffic. */
|
|
51
58
|
export class AkanApp {
|
|
59
|
+
static readonly #childRestartBaseDelayMs = 1_000;
|
|
60
|
+
static readonly #childRestartMaxDelayMs = 30_000;
|
|
61
|
+
static readonly #childRestartGraceMs = 5_000;
|
|
52
62
|
static readonly #hopByHopHeaders = new Set([
|
|
53
63
|
"connection",
|
|
54
64
|
"keep-alive",
|
|
@@ -171,7 +181,11 @@ export class AkanApp {
|
|
|
171
181
|
this.#server?.stop(true);
|
|
172
182
|
this.#server = null;
|
|
173
183
|
for (const child of this.#children.values()) {
|
|
174
|
-
if (
|
|
184
|
+
if (child.restartTimer) {
|
|
185
|
+
clearTimeout(child.restartTimer);
|
|
186
|
+
child.restartTimer = null;
|
|
187
|
+
}
|
|
188
|
+
this.#sendToChild(child, { type: "shutdown", signal } satisfies AkanIpcMessage);
|
|
175
189
|
}
|
|
176
190
|
await Promise.race([
|
|
177
191
|
Promise.all([...this.#children.values()].map((child) => child.proc.exited.catch(() => undefined))),
|
|
@@ -200,7 +214,8 @@ export class AkanApp {
|
|
|
200
214
|
const role = this.#getRole(idx);
|
|
201
215
|
const upstream = this.#getChildUpstream(idx, role);
|
|
202
216
|
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
|
-
|
|
217
|
+
let proc!: Bun.Subprocess<"ignore", "pipe", "pipe">;
|
|
218
|
+
proc = Bun.spawn(["bun", "-e", childCode], {
|
|
204
219
|
cwd: process.cwd(),
|
|
205
220
|
env: {
|
|
206
221
|
...process.env,
|
|
@@ -212,32 +227,140 @@ export class AkanApp {
|
|
|
212
227
|
AKAN_CHILD_SOCKET: upstream.http.socketPath,
|
|
213
228
|
AKAN_CHILD_WS_PORT: upstream.ws ? String(upstream.ws.port) : "",
|
|
214
229
|
},
|
|
215
|
-
ipc: (message) => this.#handleMessage(idx, message as AkanIpcMessage),
|
|
230
|
+
ipc: (message) => this.#handleMessage(idx, message as AkanIpcMessage, proc),
|
|
216
231
|
stdout: "pipe",
|
|
217
232
|
stderr: "pipe",
|
|
218
233
|
stdin: "ignore",
|
|
219
234
|
});
|
|
220
|
-
this.#children.
|
|
235
|
+
const previous = this.#children.get(idx);
|
|
236
|
+
this.#children.set(idx, {
|
|
237
|
+
idx,
|
|
238
|
+
role,
|
|
239
|
+
proc,
|
|
240
|
+
ready: false,
|
|
241
|
+
status: "starting",
|
|
242
|
+
metrics: {},
|
|
243
|
+
restartAttempts: previous?.restartAttempts ?? 0,
|
|
244
|
+
restartCount: previous?.restartCount ?? 0,
|
|
245
|
+
restartTimer: null,
|
|
246
|
+
restartPending: false,
|
|
247
|
+
lastExitCode: previous?.lastExitCode,
|
|
248
|
+
lastRestartAt: previous?.lastRestartAt,
|
|
249
|
+
lastRestartReason: previous?.lastRestartReason,
|
|
250
|
+
});
|
|
221
251
|
this.#pipeOutput(idx, role, proc.stdout, "stdout");
|
|
222
252
|
this.#pipeOutput(idx, role, proc.stderr, "stderr");
|
|
223
|
-
proc.exited.then((code) =>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
253
|
+
proc.exited.then((code) => this.#handleChildExit(idx, proc, code));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#handleChildExit(idx: number, proc: Bun.Subprocess<"ignore", "pipe", "pipe">, code: number | null) {
|
|
257
|
+
const child = this.#children.get(idx);
|
|
258
|
+
if (!child || child.proc !== proc) return;
|
|
259
|
+
child.status = "exited";
|
|
260
|
+
child.lastExitCode = code;
|
|
261
|
+
this.#removeChildRooms(idx);
|
|
262
|
+
if (this.#stopping) return;
|
|
263
|
+
void this.#scheduleChildRestart(child, proc, `exit:${code ?? "unknown"}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#scheduleChildRestart(child: ChildState, proc: Bun.Subprocess<"ignore", "pipe", "pipe">, reason: string): void {
|
|
267
|
+
if (this.#stopping) return;
|
|
268
|
+
if (child.proc !== proc) return;
|
|
269
|
+
if (child.restartPending || child.restartTimer) {
|
|
270
|
+
child.lastRestartReason = reason;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
child.restartPending = true;
|
|
275
|
+
child.ready = false;
|
|
276
|
+
child.status = reason === "health-timeout" ? "unhealthy" : "exited";
|
|
277
|
+
child.upstream = undefined;
|
|
278
|
+
child.healthPath = undefined;
|
|
279
|
+
child.lastRestartReason = reason;
|
|
280
|
+
child.lastRestartAt = Date.now();
|
|
281
|
+
this.#removeChildRooms(child.idx);
|
|
282
|
+
|
|
283
|
+
const attempt = child.restartAttempts;
|
|
284
|
+
const delay = Math.min(AkanApp.#childRestartBaseDelayMs * 2 ** attempt, AkanApp.#childRestartMaxDelayMs);
|
|
285
|
+
child.restartAttempts = attempt + 1;
|
|
286
|
+
child.restartCount += 1;
|
|
287
|
+
this.logger.error(
|
|
288
|
+
`Child ${child.idx}/${child.role} failed (${reason}); restarting in ${delay}ms (attempt ${child.restartAttempts})`,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
void this.#restartChildAfterDelay(child.idx, proc, reason, delay).catch((error) => {
|
|
292
|
+
const current = this.#children.get(child.idx);
|
|
293
|
+
if (!current || current.proc !== proc || this.#stopping) return;
|
|
294
|
+
current.restartPending = false;
|
|
295
|
+
this.logger.error(
|
|
296
|
+
`Failed to restart child ${child.idx}/${child.role}: ${error instanceof Error ? error.message : String(error)}`,
|
|
297
|
+
);
|
|
298
|
+
this.#scheduleChildRestart(current, proc, "restart-failed");
|
|
230
299
|
});
|
|
231
300
|
}
|
|
232
301
|
|
|
302
|
+
async #restartChildAfterDelay(
|
|
303
|
+
idx: number,
|
|
304
|
+
proc: Bun.Subprocess<"ignore", "pipe", "pipe">,
|
|
305
|
+
reason: string,
|
|
306
|
+
delay: number,
|
|
307
|
+
) {
|
|
308
|
+
const child = this.#children.get(idx);
|
|
309
|
+
if (!child || child.proc !== proc || this.#stopping) return;
|
|
310
|
+
await this.#stopChildForRestart(child, proc, reason);
|
|
311
|
+
if (this.#stopping) return;
|
|
312
|
+
const current = this.#children.get(idx);
|
|
313
|
+
if (!current || current.proc !== proc) return;
|
|
314
|
+
current.restartTimer = setTimeout(() => {
|
|
315
|
+
current.restartTimer = null;
|
|
316
|
+
void this.#respawnChild(idx, proc).catch((error) => {
|
|
317
|
+
const latest = this.#children.get(idx);
|
|
318
|
+
if (!latest || latest.proc !== proc || this.#stopping) return;
|
|
319
|
+
latest.restartPending = false;
|
|
320
|
+
this.logger.error(
|
|
321
|
+
`Failed to respawn child ${idx}/${latest.role}: ${error instanceof Error ? error.message : String(error)}`,
|
|
322
|
+
);
|
|
323
|
+
this.#scheduleChildRestart(latest, proc, "respawn-failed");
|
|
324
|
+
});
|
|
325
|
+
}, delay);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async #respawnChild(idx: number, proc: Bun.Subprocess<"ignore", "pipe", "pipe">) {
|
|
329
|
+
if (this.#stopping) return;
|
|
330
|
+
const current = this.#children.get(idx);
|
|
331
|
+
if (!current || current.proc !== proc) return;
|
|
332
|
+
await this.#removeChildSocket(idx, current.role);
|
|
333
|
+
current.restartPending = false;
|
|
334
|
+
this.#spawn(idx);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async #stopChildForRestart(child: ChildState, proc: Bun.Subprocess<"ignore", "pipe", "pipe">, reason: string) {
|
|
338
|
+
if (!proc.killed) {
|
|
339
|
+
this.#sendToChild(child, { type: "shutdown", signal: reason } satisfies AkanIpcMessage);
|
|
340
|
+
}
|
|
341
|
+
const result = await Promise.race([
|
|
342
|
+
proc.exited.then(() => "exited" as const).catch(() => "exited" as const),
|
|
343
|
+
new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), AkanApp.#childRestartGraceMs)),
|
|
344
|
+
]);
|
|
345
|
+
if (result === "timeout" && !proc.killed) {
|
|
346
|
+
this.logger.warn(`Child ${child.idx}/${child.role} did not stop in ${AkanApp.#childRestartGraceMs}ms; killing`);
|
|
347
|
+
proc.kill();
|
|
348
|
+
await proc.exited.catch(() => undefined);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
233
352
|
async #prepareRuntimeDir() {
|
|
234
353
|
await mkdir(this.#runtimeDir, { recursive: true });
|
|
235
354
|
for (let idx = 0; idx < this.#replica.total; idx++) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
355
|
+
await this.#removeChildSocket(idx, this.#getRole(idx));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async #removeChildSocket(idx: number, role: AkanChildRole) {
|
|
360
|
+
const socketPath = this.#getChildUpstream(idx, role).http.socketPath;
|
|
361
|
+
try {
|
|
362
|
+
await rm(socketPath, { force: true });
|
|
363
|
+
} catch {
|
|
241
364
|
}
|
|
242
365
|
}
|
|
243
366
|
|
|
@@ -388,6 +511,12 @@ export class AkanApp {
|
|
|
388
511
|
ready: child.ready,
|
|
389
512
|
pid: child.pid,
|
|
390
513
|
upstream: child.upstream,
|
|
514
|
+
restartAttempts: child.restartAttempts,
|
|
515
|
+
restartCount: child.restartCount,
|
|
516
|
+
restartPending: child.restartPending,
|
|
517
|
+
lastExitCode: child.lastExitCode,
|
|
518
|
+
lastRestartAt: child.lastRestartAt,
|
|
519
|
+
lastRestartReason: child.lastRestartReason,
|
|
391
520
|
})),
|
|
392
521
|
};
|
|
393
522
|
}
|
|
@@ -402,6 +531,12 @@ export class AkanApp {
|
|
|
402
531
|
role: child.role,
|
|
403
532
|
metrics: child.metrics,
|
|
404
533
|
rooms: this.#childRooms.get(child.idx)?.size ?? 0,
|
|
534
|
+
restartAttempts: child.restartAttempts,
|
|
535
|
+
restartCount: child.restartCount,
|
|
536
|
+
restartPending: child.restartPending,
|
|
537
|
+
lastExitCode: child.lastExitCode,
|
|
538
|
+
lastRestartAt: child.lastRestartAt,
|
|
539
|
+
lastRestartReason: child.lastRestartReason,
|
|
405
540
|
})),
|
|
406
541
|
};
|
|
407
542
|
}
|
|
@@ -557,7 +692,13 @@ export class AkanApp {
|
|
|
557
692
|
};
|
|
558
693
|
}
|
|
559
694
|
|
|
560
|
-
#handleMessage(
|
|
695
|
+
#handleMessage(
|
|
696
|
+
idx: number,
|
|
697
|
+
message: AkanIpcMessage | BuilderMessage,
|
|
698
|
+
proc?: Bun.Subprocess<"ignore", "pipe", "pipe">,
|
|
699
|
+
) {
|
|
700
|
+
const child = this.#children.get(idx);
|
|
701
|
+
if (proc && (!child || child.proc !== proc)) return;
|
|
561
702
|
if (!message || typeof message !== "object") return;
|
|
562
703
|
switch (message.type) {
|
|
563
704
|
case "ready":
|
|
@@ -586,7 +727,7 @@ export class AkanApp {
|
|
|
586
727
|
return;
|
|
587
728
|
case "error":
|
|
588
729
|
this.logger.error(message.message);
|
|
589
|
-
void this
|
|
730
|
+
if (child && proc) void this.#scheduleChildRestart(child, proc, "child-error");
|
|
590
731
|
return;
|
|
591
732
|
case "build-route":
|
|
592
733
|
this.#forwardBuildRoute(idx, message);
|
|
@@ -618,6 +759,8 @@ export class AkanApp {
|
|
|
618
759
|
child.upstream = message.upstream;
|
|
619
760
|
child.healthPath = message.healthPath;
|
|
620
761
|
child.lastPongAt = Date.now();
|
|
762
|
+
child.restartAttempts = 0;
|
|
763
|
+
child.restartPending = false;
|
|
621
764
|
if ([...this.#children.values()].every((item) => item.ready)) {
|
|
622
765
|
process.send?.({ type: "backend-ready", pid: process.pid } satisfies AkanIpcMessage);
|
|
623
766
|
this.logger.verbose(`All ${this.#children.size} child process(es) are ready`);
|
|
@@ -643,12 +786,15 @@ export class AkanApp {
|
|
|
643
786
|
if (childIdx === originIdx) continue;
|
|
644
787
|
const child = this.#children.get(childIdx);
|
|
645
788
|
if (!child || child.proc.killed || child.status === "exited") continue;
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
789
|
+
if (
|
|
790
|
+
!this.#sendToChild(child, {
|
|
791
|
+
type: "pubsub.deliver",
|
|
792
|
+
roomId: message.roomId,
|
|
793
|
+
data: message.data,
|
|
794
|
+
origin: message.origin,
|
|
795
|
+
} satisfies AkanIpcMessage)
|
|
796
|
+
)
|
|
797
|
+
continue;
|
|
652
798
|
child.metrics.pubsubDeliverCount = (child.metrics.pubsubDeliverCount ?? 0) + 1;
|
|
653
799
|
}
|
|
654
800
|
}
|
|
@@ -747,7 +893,7 @@ export class AkanApp {
|
|
|
747
893
|
#requestRoomSnapshots() {
|
|
748
894
|
for (const child of this.#children.values()) {
|
|
749
895
|
if (child.proc.killed || child.status === "exited") continue;
|
|
750
|
-
child
|
|
896
|
+
this.#sendToChild(child, { type: "pubsub.snapshot.request" } satisfies AkanIpcMessage);
|
|
751
897
|
}
|
|
752
898
|
}
|
|
753
899
|
|
|
@@ -757,11 +903,14 @@ export class AkanApp {
|
|
|
757
903
|
if (child.proc.killed || child.status === "exited") continue;
|
|
758
904
|
if (child.lastPongAt && now - child.lastPongAt > 5_000) {
|
|
759
905
|
child.status = "unhealthy";
|
|
760
|
-
this
|
|
761
|
-
void this.stop("health-timeout");
|
|
906
|
+
void this.#scheduleChildRestart(child, child.proc, "health-timeout");
|
|
762
907
|
return;
|
|
763
908
|
}
|
|
764
|
-
child
|
|
909
|
+
this.#sendToChild(child, {
|
|
910
|
+
type: "health.ping",
|
|
911
|
+
nonce: crypto.randomUUID(),
|
|
912
|
+
sentAt: now,
|
|
913
|
+
} satisfies AkanIpcMessage);
|
|
765
914
|
}
|
|
766
915
|
}
|
|
767
916
|
|
|
@@ -780,25 +929,41 @@ export class AkanApp {
|
|
|
780
929
|
this.#builderReqMap.delete(message.id);
|
|
781
930
|
const child = this.#children.get(request.childIdx);
|
|
782
931
|
if (!child || child.proc.killed) return;
|
|
783
|
-
child
|
|
932
|
+
this.#sendToChild(child, { ...message, id: request.childLocalId } satisfies BuilderRes);
|
|
784
933
|
}
|
|
785
934
|
|
|
786
935
|
#fanoutToFederation(message: AkanIpcMessage | BuilderMessage, exceptIdx?: number) {
|
|
787
936
|
for (const child of this.#children.values()) {
|
|
788
937
|
if (child.idx === exceptIdx) continue;
|
|
789
|
-
if (child.role === "federation" || child.role === "all") child
|
|
938
|
+
if (child.role === "federation" || child.role === "all") this.#sendToChild(child, message);
|
|
790
939
|
}
|
|
791
940
|
}
|
|
792
941
|
|
|
793
942
|
#fanoutToBatch(message: AkanIpcMessage) {
|
|
794
943
|
for (const child of this.#children.values()) {
|
|
795
944
|
if (child.role === "batch" || child.role === "all") {
|
|
796
|
-
child.
|
|
797
|
-
|
|
945
|
+
if (this.#sendToChild(child, message) && message.type === "queue.wake") {
|
|
946
|
+
child.metrics.queueWakeCount = (child.metrics.queueWakeCount ?? 0) + 1;
|
|
947
|
+
}
|
|
798
948
|
}
|
|
799
949
|
}
|
|
800
950
|
}
|
|
801
951
|
|
|
952
|
+
#sendToChild(child: ChildState, message: AkanIpcMessage | BuilderMessage): boolean {
|
|
953
|
+
if (child.proc.killed || child.status === "exited") return false;
|
|
954
|
+
try {
|
|
955
|
+
child.proc.send(message);
|
|
956
|
+
return true;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
this.logger.warn(
|
|
959
|
+
`Failed to send ${message.type} to child ${child.idx}/${child.role}: ${
|
|
960
|
+
error instanceof Error ? error.message : String(error)
|
|
961
|
+
}`,
|
|
962
|
+
);
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
802
967
|
async #pipeOutput(
|
|
803
968
|
idx: number,
|
|
804
969
|
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();
|
|
@@ -1,22 +1,39 @@
|
|
|
1
|
-
import { match as matchLocale } from "@formatjs/intl-localematcher";
|
|
2
1
|
import { parseAkanI18nEnv } from "akanjs/common";
|
|
3
|
-
import Negotiator from "negotiator";
|
|
4
2
|
import { AkanResponse } from "./akanResponse";
|
|
5
3
|
import type { WebProxy } from "./types";
|
|
6
4
|
|
|
7
5
|
function getLocale(request: Bun.BunRequest): string {
|
|
8
6
|
const i18n = parseAkanI18nEnv();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
const acceptLanguage = request.headers.get("accept-language");
|
|
8
|
+
if (!acceptLanguage) return i18n.defaultLocale;
|
|
9
|
+
return matchAcceptedLocale(acceptLanguage, i18n.locales, i18n.defaultLocale);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function matchAcceptedLocale(acceptLanguage: string, locales: string[], defaultLocale: string): string {
|
|
13
|
+
const localeByLower = new Map(locales.map((locale) => [locale.toLowerCase(), locale]));
|
|
14
|
+
const acceptedLanguages = acceptLanguage
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((part, index) => {
|
|
17
|
+
const [tag = "", ...params] = part.trim().split(";");
|
|
18
|
+
const quality = params
|
|
19
|
+
.map((param) => param.trim())
|
|
20
|
+
.find((param) => param.startsWith("q="))
|
|
21
|
+
?.slice(2);
|
|
22
|
+
const q = quality === undefined ? 1 : Number(quality);
|
|
23
|
+
return { tag: tag.trim().toLowerCase(), q: Number.isFinite(q) ? q : 0, index };
|
|
24
|
+
})
|
|
25
|
+
.filter(({ tag, q }) => tag.length > 0 && q > 0)
|
|
26
|
+
.sort((a, b) => b.q - a.q || a.index - b.index);
|
|
27
|
+
|
|
28
|
+
for (const { tag } of acceptedLanguages) {
|
|
29
|
+
if (tag === "*") return defaultLocale;
|
|
30
|
+
const exact = localeByLower.get(tag);
|
|
31
|
+
if (exact) return exact;
|
|
32
|
+
const [language] = tag.split("-");
|
|
33
|
+
const baseMatch = locales.find((locale) => locale.toLowerCase().split("-")[0] === language);
|
|
34
|
+
if (baseMatch) return baseMatch;
|
|
19
35
|
}
|
|
36
|
+
return defaultLocale;
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
export class LocaleWebProxy implements WebProxy {
|
package/service/serviceModule.ts
CHANGED
|
@@ -45,12 +45,7 @@ export class ServiceModel<
|
|
|
45
45
|
this.cnst,
|
|
46
46
|
this.db,
|
|
47
47
|
Object.assign({}, this.srvMap, ...srvs.map((srv) => srv.srvMap)),
|
|
48
|
-
) as unknown as ServiceModel<
|
|
49
|
-
Srv & MergeAllKeyOfObjects<SrvModules, "srv">,
|
|
50
|
-
CnstModel,
|
|
51
|
-
DbModel,
|
|
52
|
-
SrvMap & MergeAllKeyOfObjects<SrvModules, "srvMap">
|
|
53
|
-
>;
|
|
48
|
+
) as unknown as ServiceModel<Srv, CnstModel, DbModel, SrvMap & MergeAllKeyOfObjects<SrvModules, "srvMap">>;
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
static getDefaultDbServiceMethods(className: string) {
|
package/signal/base.signal.ts
CHANGED
|
@@ -31,7 +31,7 @@ export class BaseEndpoint extends endpoint(srv.base, ({ query, mutation, message
|
|
|
31
31
|
})) {}
|
|
32
32
|
|
|
33
33
|
export class Base extends serverSignal(BaseEndpoint, BaseInternal) {}
|
|
34
|
-
export const base = SignalRegistry.registerService(BaseInternal, BaseEndpoint, Base);
|
|
34
|
+
export const base = SignalRegistry.registerService("base" as const, BaseInternal, BaseEndpoint, Base);
|
|
35
35
|
|
|
36
36
|
const createBaseFetch = () => FetchClient.from(base);
|
|
37
37
|
type BaseFetch = ReturnType<typeof createBaseFetch>;
|
package/signal/signalRegistry.ts
CHANGED
|
@@ -5,6 +5,18 @@ import type { ServerSignalCls } from "./serverSignal";
|
|
|
5
5
|
import type { SliceCls } from "./slice";
|
|
6
6
|
import type { SerializedSignal } from "./types";
|
|
7
7
|
|
|
8
|
+
type SignalBaseRef<SigCls> = SigCls extends { srv: { srv: { refName: infer RefName } } } ? RefName : never;
|
|
9
|
+
type SignalWithBase<RefName extends string, SigCls> = SignalBaseRef<SigCls> extends RefName ? SigCls : never;
|
|
10
|
+
|
|
11
|
+
function assertSignalBase(refName: string, signalKind: string, signalCls: { srv?: { srv?: { refName?: unknown } } }) {
|
|
12
|
+
const signalRefName = signalCls.srv?.srv?.refName;
|
|
13
|
+
if (signalRefName !== refName) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Signal base mismatch: ${signalKind} uses "${String(signalRefName)}", but registry expected "${refName}". Use srv.${refName}.with(...) when the signal needs another service.`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
export class DatabaseSignal<
|
|
9
21
|
IntlCls extends InternalCls = InternalCls,
|
|
10
22
|
EndpCls extends EndpointCls = EndpointCls,
|
|
@@ -46,34 +58,47 @@ export class ServiceSignal<
|
|
|
46
58
|
|
|
47
59
|
/** Registry for database and service signals used by routing and fetch serialization. */
|
|
48
60
|
export class SignalRegistry {
|
|
49
|
-
static readonly #database = new Map<string, DatabaseSignal<
|
|
50
|
-
static readonly #service = new Map<string, ServiceSignal<
|
|
61
|
+
static readonly #database = new Map<string, DatabaseSignal<InternalCls, EndpointCls, SliceCls, ServerSignalCls>>();
|
|
62
|
+
static readonly #service = new Map<string, ServiceSignal<InternalCls, EndpointCls, ServerSignalCls>>();
|
|
51
63
|
|
|
52
64
|
static registerDatabase<
|
|
65
|
+
RefName extends string,
|
|
53
66
|
IntlCls extends InternalCls,
|
|
54
67
|
EndpCls extends EndpointCls,
|
|
55
68
|
SlceCls extends SliceCls,
|
|
56
69
|
SrvrCls extends ServerSignalCls,
|
|
57
70
|
>(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
refName: RefName,
|
|
72
|
+
internal: SignalWithBase<RefName, IntlCls>,
|
|
73
|
+
endpoint: SignalWithBase<RefName, EndpCls>,
|
|
74
|
+
slice: SignalWithBase<RefName, SlceCls>,
|
|
61
75
|
server: SrvrCls,
|
|
62
76
|
): DatabaseSignal<IntlCls, EndpCls, SlceCls, SrvrCls> {
|
|
77
|
+
assertSignalBase(refName, "internal", internal);
|
|
78
|
+
assertSignalBase(refName, "endpoint", endpoint);
|
|
79
|
+
assertSignalBase(refName, "slice", slice);
|
|
63
80
|
const databaseSignal = new DatabaseSignal(internal, endpoint, slice, server);
|
|
64
|
-
SignalRegistry.#database.set(
|
|
81
|
+
SignalRegistry.#database.set(refName, databaseSignal);
|
|
65
82
|
return databaseSignal;
|
|
66
83
|
}
|
|
67
84
|
static getDatabase(refName: string) {
|
|
68
85
|
return SignalRegistry.#database.get(refName);
|
|
69
86
|
}
|
|
70
|
-
static registerService<
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
static registerService<
|
|
88
|
+
RefName extends string,
|
|
89
|
+
IntlCls extends InternalCls,
|
|
90
|
+
EndpCls extends EndpointCls,
|
|
91
|
+
SrvrCls extends ServerSignalCls,
|
|
92
|
+
>(
|
|
93
|
+
refName: RefName,
|
|
94
|
+
internal: SignalWithBase<RefName, IntlCls>,
|
|
95
|
+
endpoint: SignalWithBase<RefName, EndpCls>,
|
|
73
96
|
server: SrvrCls,
|
|
74
97
|
): ServiceSignal<IntlCls, EndpCls, SrvrCls> {
|
|
98
|
+
assertSignalBase(refName, "internal", internal);
|
|
99
|
+
assertSignalBase(refName, "endpoint", endpoint);
|
|
75
100
|
const serviceSignal = new ServiceSignal(internal, endpoint, server);
|
|
76
|
-
SignalRegistry.#service.set(
|
|
101
|
+
SignalRegistry.#service.set(refName, serviceSignal);
|
|
77
102
|
return serviceSignal;
|
|
78
103
|
}
|
|
79
104
|
static getService(refName: string) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getLatestPackageVersion(packageName: string, tag?: string): Promise<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function openBrowser(url: string): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function pluralizeName(name: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function compareSemver(a: string, b: string): number;
|
package/types/client/cookie.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { Account } from "akanjs/fetch";
|
|
2
|
+
interface CookieOptions {
|
|
3
|
+
path?: string;
|
|
4
|
+
sameSite?: "strict" | "lax" | "none";
|
|
5
|
+
secure?: boolean;
|
|
6
|
+
}
|
|
2
7
|
export declare const cookies: () => Map<string, {
|
|
3
8
|
name: string;
|
|
4
9
|
value: string;
|
|
5
10
|
}>;
|
|
6
|
-
export declare const setCookie: (key: string, value: string, options?:
|
|
11
|
+
export declare const setCookie: (key: string, value: string, options?: CookieOptions) => void;
|
|
7
12
|
export declare const getCookie: (key: string) => string | undefined;
|
|
8
13
|
export declare const removeCookie: (key: string, options?: {
|
|
9
14
|
path: string;
|
package/types/common/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { isEmail } from "./isEmail.d.ts";
|
|
|
11
11
|
export { isPhoneNumber } from "./isPhoneNumber.d.ts";
|
|
12
12
|
export { isQueryEqual } from "./isQueryEqual.d.ts";
|
|
13
13
|
export { isValidDate } from "./isValidDate.d.ts";
|
|
14
|
+
export { decodeJwtPayload } from "./jwtDecode.d.ts";
|
|
14
15
|
export { Logger, type LoggerSink, type LoggerSinkEntry, type LogLevel } from "./Logger.d.ts";
|
|
15
16
|
export { type AkanI18nConfig, type AkanI18nConfigInput, DEFAULT_AKAN_I18N, parseAkanI18nEnv, resolveAkanI18nConfig, } from "./localeConfig.d.ts";
|
|
16
17
|
export { lowerlize } from "./lowerlize.d.ts";
|
|
@@ -7,6 +7,12 @@ interface IncrementalBuilderHostOptions {
|
|
|
7
7
|
env: Record<string, string>;
|
|
8
8
|
onMessage: (message: BuilderMessage) => void;
|
|
9
9
|
}
|
|
10
|
+
type IncrementalBuilderStatus = "starting" | "ready" | "restarting" | "stopped";
|
|
11
|
+
interface IncrementalBuilderStartOptions {
|
|
12
|
+
onExit?: () => void;
|
|
13
|
+
onReady?: () => void;
|
|
14
|
+
onRestartReady?: () => void;
|
|
15
|
+
}
|
|
10
16
|
export declare class IncrementalBuilderHost {
|
|
11
17
|
#private;
|
|
12
18
|
logger: Logger;
|
|
@@ -15,11 +21,9 @@ export declare class IncrementalBuilderHost {
|
|
|
15
21
|
app: App;
|
|
16
22
|
ready: boolean;
|
|
17
23
|
constructor({ app, entry, env, onMessage }: IncrementalBuilderHostOptions);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}): this;
|
|
22
|
-
send(message: BuilderMessage): void;
|
|
24
|
+
get status(): IncrementalBuilderStatus;
|
|
25
|
+
start(options?: IncrementalBuilderStartOptions): this;
|
|
26
|
+
send(message: BuilderMessage): boolean;
|
|
23
27
|
stop(): void;
|
|
24
28
|
static create(app: App, env: Record<string, string>, onMessage: (message: BuilderMessage) => void): Promise<IncrementalBuilderHost>;
|
|
25
29
|
}
|
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
import type { QueryOf } from "akanjs/constant";
|
|
2
|
-
import DataLoader from "dataloader";
|
|
3
2
|
export declare const Id: StringConstructor;
|
|
4
3
|
export declare const ObjectId: StringConstructor;
|
|
5
4
|
export declare const Mixed: ObjectConstructor;
|
|
6
|
-
export { DataLoader };
|
|
7
5
|
type LoaderItem = Record<string, unknown>;
|
|
8
6
|
type LoaderModel = {
|
|
9
7
|
find: (query: QueryOf<unknown>) => Promise<LoaderItem[]> | {
|
|
10
8
|
then: Promise<LoaderItem[]>["then"];
|
|
11
9
|
};
|
|
12
10
|
};
|
|
11
|
+
type BatchLoadFn<Key, Value> = (keys: readonly Key[]) => PromiseLike<ReadonlyArray<Value | Error>> | ReadonlyArray<Value | Error>;
|
|
12
|
+
interface DataLoaderOptions<Key, CacheKey> {
|
|
13
|
+
cache?: boolean;
|
|
14
|
+
cacheKeyFn?: (key: Key) => CacheKey;
|
|
15
|
+
batch?: boolean;
|
|
16
|
+
batchScheduleFn?: (callback: () => void) => void;
|
|
17
|
+
maxBatchSize?: number;
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Minimal DataLoader-compatible batch loader used by Akan document resolvers. */
|
|
21
|
+
export declare class DataLoader<Key, Value, CacheKey = Key> {
|
|
22
|
+
#private;
|
|
23
|
+
readonly name?: string;
|
|
24
|
+
constructor(batchLoadFn: BatchLoadFn<Key, Value>, options?: DataLoaderOptions<Key, CacheKey>);
|
|
25
|
+
load(key: Key): Promise<Value>;
|
|
26
|
+
loadMany(keys: readonly Key[]): Promise<Array<Value | Error>>;
|
|
27
|
+
clear(key: Key): this;
|
|
28
|
+
clearAll(): this;
|
|
29
|
+
prime(key: Key, value: Value | Error): this;
|
|
30
|
+
}
|
|
13
31
|
export declare const createLoader: <Key, Value>(model: LoaderModel, fieldName?: string, defaultQuery?: QueryOf<unknown>) => DataLoader<Key, Value, Key>;
|
|
14
32
|
export declare const createArrayLoader: <K, V>(model: LoaderModel, fieldName?: string, defaultQuery?: QueryOf<unknown>) => DataLoader<K, V, K>;
|
|
15
33
|
export declare const createArrayElementLoader: <K, V>(model: LoaderModel, fieldName?: string, defaultQuery?: QueryOf<unknown>) => DataLoader<K, V, K>;
|
|
16
34
|
export declare const createQueryLoader: <Key, Value>(model: LoaderModel, queryKeys: string[], defaultQuery?: QueryOf<unknown>) => DataLoader<Key, Value, Key>;
|
|
17
35
|
export type Loader<Field, Value> = DataLoader<Field, Value | null>;
|
|
36
|
+
export {};
|
|
@@ -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.d.ts";
|
|
6
6
|
import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta.d.ts";
|
|
7
7
|
import type { CRUDEventType, Mdl, SaveEventType } from "./into.d.ts";
|
|
8
8
|
import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types.d.ts";
|
|
@@ -17,7 +17,7 @@ export declare class ServiceModel<Srv extends ServiceCls = ServiceCls, CnstModel
|
|
|
17
17
|
});
|
|
18
18
|
static fromModel<Srv extends ServiceCls, CnstModel extends ConstantModel, DbModel extends DatabaseModel>(srv: Srv, cnst: CnstModel, db: DbModel): ServiceModel<Srv, CnstModel, DbModel>;
|
|
19
19
|
static from<Srv extends ServiceCls>(srv: Srv): ServiceModel<Srv, never, never, { [K in `${Uncapitalize<Srv["refName"]>}Service`]: UnCls<Srv>; }>;
|
|
20
|
-
with<SrvModules extends ServiceModel[]>(...srvs: SrvModules): ServiceModel<Srv
|
|
20
|
+
with<SrvModules extends ServiceModel[]>(...srvs: SrvModules): ServiceModel<Srv, CnstModel, DbModel, SrvMap & MergeAllKeyOfObjects<SrvModules, "srvMap">>;
|
|
21
21
|
static getDefaultDbServiceMethods(className: string): {
|
|
22
22
|
[x: string]: ((this: DatabaseService, query: QueryOf<any>, queryOption?: ListQueryOption) => Promise<any>) | ((this: DatabaseService, query: QueryOf<any>, queryOption?: FindQueryOption) => Promise<any>) | ((this: DatabaseService, type: SaveEventType, listener: (doc: Doc, type: CRUDEventType) => PromiseOrObject<void>) => any) | ((this: DatabaseService, id: string, data: DataInputOf) => Promise<any>);
|
|
23
23
|
__get(this: DatabaseService, id: string): Promise<any>;
|