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.
Files changed (42) hide show
  1. package/cli/application/application.runner.ts +1 -1
  2. package/cli/build.ts +2 -1
  3. package/cli/cloud/cloud.runner.ts +7 -8
  4. package/cli/index.js +176 -43
  5. package/cli/library/library.runner.ts +2 -2
  6. package/cli/module/module.runner.ts +2 -2
  7. package/cli/npmRegistry.ts +13 -0
  8. package/cli/openBrowser.ts +15 -0
  9. package/cli/pluralizeName.ts +5 -0
  10. package/cli/scalar/scalar.prompt.ts +2 -2
  11. package/cli/scalar/scalar.runner.ts +2 -2
  12. package/cli/semver.ts +18 -0
  13. package/cli/templates/lib/sig.ts +2 -2
  14. package/cli/workspace/workspace.runner.ts +3 -3
  15. package/client/cookie.ts +10 -15
  16. package/common/index.ts +1 -0
  17. package/common/jwtDecode.ts +17 -0
  18. package/devkit/akanApp/akanApp.host.ts +46 -9
  19. package/devkit/akanConfig/akanConfig.ts +2 -1
  20. package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
  21. package/document/dataLoader.ts +140 -6
  22. package/document/database.ts +1 -1
  23. package/package.json +7 -13
  24. package/server/akanApp.ts +197 -32
  25. package/server/di/diLifecycle.ts +1 -1
  26. package/server/proxy/localeWebProxy.ts +29 -12
  27. package/service/serviceModule.ts +1 -6
  28. package/signal/base.signal.ts +1 -1
  29. package/signal/signalRegistry.ts +35 -10
  30. package/types/cli/npmRegistry.d.ts +1 -0
  31. package/types/cli/openBrowser.d.ts +1 -0
  32. package/types/cli/pluralizeName.d.ts +1 -0
  33. package/types/cli/semver.d.ts +1 -0
  34. package/types/client/cookie.d.ts +6 -1
  35. package/types/common/index.d.ts +1 -0
  36. package/types/common/jwtDecode.d.ts +2 -0
  37. package/types/devkit/incrementalBuilder/incrementalBuilder.host.d.ts +9 -5
  38. package/types/document/dataLoader.d.ts +21 -2
  39. package/types/document/database.d.ts +1 -1
  40. package/types/service/serviceModule.d.ts +1 -1
  41. package/types/signal/signalRegistry.d.ts +25 -4
  42. 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 (!child.proc.killed) child.proc.send({ type: "shutdown", signal } satisfies AkanIpcMessage);
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
- const proc = Bun.spawn(["bun", "-e", childCode], {
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.set(idx, { idx, role, proc, ready: false, status: "starting", metrics: {} });
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
- 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");
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
- const socketPath = this.#getChildUpstream(idx, this.#getRole(idx)).http.socketPath;
237
- try {
238
- await rm(socketPath, { force: true });
239
- } catch {
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(idx: number, message: AkanIpcMessage | BuilderMessage) {
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.stop("child-error");
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
- child.proc.send({
647
- type: "pubsub.deliver",
648
- roomId: message.roomId,
649
- data: message.data,
650
- origin: message.origin,
651
- } satisfies AkanIpcMessage);
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.proc.send({ type: "pubsub.snapshot.request" } satisfies AkanIpcMessage);
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.logger.error(`Child ${child.idx}/${child.role} health check timed out; shutting down all children`);
761
- void this.stop("health-timeout");
906
+ void this.#scheduleChildRestart(child, child.proc, "health-timeout");
762
907
  return;
763
908
  }
764
- child.proc.send({ type: "health.ping", nonce: crypto.randomUUID(), sentAt: now } satisfies AkanIpcMessage);
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.proc.send({ ...message, id: request.childLocalId } satisfies BuilderRes);
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.proc.send(message);
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.proc.send(message);
797
- if (message.type === "queue.wake") child.metrics.queueWakeCount = (child.metrics.queueWakeCount ?? 0) + 1;
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,
@@ -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
- if (!request.headers.get("accept-language")) return i18n.defaultLocale;
10
- const negotiatorHeaders: Record<string, string> = {};
11
- request.headers.forEach((value, key) => {
12
- negotiatorHeaders[key] = value;
13
- });
14
- try {
15
- const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
16
- return matchLocale(languages, i18n.locales, i18n.defaultLocale);
17
- } catch {
18
- return i18n.defaultLocale;
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 {
@@ -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) {
@@ -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>;
@@ -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<any, any, any, any>>();
50
- static readonly #service = new Map<string, ServiceSignal<any, any, any>>();
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
- internal: IntlCls,
59
- endpoint: EndpCls,
60
- slice: SlceCls,
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(internal.refName, databaseSignal);
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<IntlCls extends InternalCls, EndpCls extends EndpointCls, SrvrCls extends ServerSignalCls>(
71
- internal: IntlCls,
72
- endpoint: EndpCls,
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(internal.refName, serviceSignal);
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;
@@ -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?: Cookies.CookieAttributes) => void;
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;
@@ -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";
@@ -0,0 +1,2 @@
1
+ /** Decodes a JWT payload without validating its signature. */
2
+ export declare function decodeJwtPayload<T = unknown>(jwt: string): T;
@@ -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
- start({ onExit, onReady }: {
19
- onExit?: () => void;
20
- onReady?: () => void;
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 "dataloader";
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 & MergeAllKeyOfObjects<SrvModules, "srv">, CnstModel, DbModel, SrvMap & MergeAllKeyOfObjects<SrvModules, "srvMap">>;
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>;