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
@@ -50,8 +50,8 @@ ${[...scanInfo.service.entries()]
50
50
  )
51
51
  .join("\n")}
52
52
 
53
- ${databaseModules.map((module) => `export const ${module} = SignalRegistry.registerDatabase(${module}Sig.${capitalize(module)}Internal, ${module}Sig.${capitalize(module)}Endpoint, ${module}Sig.${capitalize(module)}Slice, ${capitalize(module)});`).join("\n")}
54
- ${serviceModules.map((module) => `export const ${module} = SignalRegistry.registerService(${module}Sig.${capitalize(module)}Internal, ${module}Sig.${capitalize(module)}Endpoint, ${capitalize(module)});`).join("\n")}
53
+ ${databaseModules.map((module) => `export const ${module} = SignalRegistry.registerDatabase("${module}" as const, ${module}Sig.${capitalize(module)}Internal, ${module}Sig.${capitalize(module)}Endpoint, ${module}Sig.${capitalize(module)}Slice, ${capitalize(module)});`).join("\n")}
54
+ ${serviceModules.map((module) => `export const ${module} = SignalRegistry.registerService("${module}" as const, ${module}Sig.${capitalize(module)}Internal, ${module}Sig.${capitalize(module)}Endpoint, ${capitalize(module)});`).join("\n")}
55
55
 
56
56
  export const fetch = FetchClient.from(${[...(libs.length === 0 ? ["base"] : libs), ...databaseModules, ...serviceModules].join(", ")});
57
57
 
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
- import { FileSys, type Exec, type PackageJson, runner, type Workspace, WorkspaceExecutor } from "akanjs/devkit";
3
- import latestVersion from "latest-version";
2
+ import { type Exec, FileSys, type PackageJson, runner, type Workspace, WorkspaceExecutor } from "akanjs/devkit";
3
+ import { getLatestPackageVersion } from "../npmRegistry";
4
4
 
5
5
  export class WorkspaceRunner extends runner("workspace") {
6
6
  async createWorkspace(
@@ -13,7 +13,7 @@ export class WorkspaceRunner extends runner("workspace") {
13
13
 
14
14
  const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot, repoName });
15
15
  const templateSpinner = workspace.spinning(`Creating workspace template files in ${dirname}/${repoName}...`);
16
- const latestTypesBunVersion = await latestVersion("@types/bun");
16
+ const latestTypesBunVersion = await getLatestPackageVersion("@types/bun");
17
17
  await workspace.applyTemplate({
18
18
  basePath: ".",
19
19
  template: "workspaceRoot",
package/client/cookie.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  import { getEnv } from "akanjs/base";
2
- import { Logger } from "akanjs/common";
2
+ import { decodeJwtPayload, Logger } from "akanjs/common";
3
3
  import type { Account } from "akanjs/fetch";
4
4
  import { requestStorage } from "akanjs/fetch";
5
- import Cookies from "js-cookie";
6
- import { jwtDecode } from "jwt-decode";
7
5
  import { storage } from "./storage";
8
6
  import { fetch } from "./useClient";
9
7
 
8
+ interface CookieOptions {
9
+ path?: string;
10
+ sameSite?: "strict" | "lax" | "none";
11
+ secure?: boolean;
12
+ }
13
+
10
14
  function parseCookieHeader(cookieHeader: string): Map<string, { name: string; value: string }> {
11
15
  const entries = cookieHeader
12
16
  .split(";")
@@ -30,22 +34,13 @@ export const cookies = (): Map<string, { name: string; value: string }> => {
30
34
  if (!req) return new Map();
31
35
  return parseCookieHeader(req.headers.get("cookie") ?? "");
32
36
  }
33
- const cookie = Cookies.get();
34
- return new Map(
35
- Object.entries(cookie).map(([key, value]) => [
36
- key,
37
- {
38
- name: key,
39
- value: typeof value === "string" && value.startsWith("j:") ? (JSON.parse(value.slice(2)) as string) : value,
40
- },
41
- ]),
42
- );
37
+ return parseCookieHeader(document.cookie);
43
38
  };
44
39
 
45
40
  export const setCookie = (
46
41
  key: string,
47
42
  value: string,
48
- options: Cookies.CookieAttributes = { path: "/", sameSite: "none", secure: true },
43
+ options: CookieOptions = { path: "/", sameSite: "none", secure: true },
49
44
  ) => {
50
45
  if (getEnv().side === "server") return;
51
46
  else
@@ -88,7 +83,7 @@ export const getAccount = <AddData = unknown>(): Account<AddData> => {
88
83
  const jwt = getCookie("jwt") ?? getHeader("jwt");
89
84
  const defaultAccount = { appName: getEnv().appName, environment: getEnv().environment } as Account<AddData>;
90
85
  if (!jwt) return defaultAccount;
91
- const account: Account<AddData> = jwtDecode<Account<AddData>>(jwt);
86
+ const account = decodeJwtPayload<Account<AddData>>(jwt);
92
87
  if (account.appName !== getEnv().appName || account.environment !== getEnv().environment) return defaultAccount;
93
88
  return account;
94
89
  };
package/common/index.ts CHANGED
@@ -11,6 +11,7 @@ export { isEmail } from "./isEmail";
11
11
  export { isPhoneNumber } from "./isPhoneNumber";
12
12
  export { isQueryEqual } from "./isQueryEqual";
13
13
  export { isValidDate } from "./isValidDate";
14
+ export { decodeJwtPayload } from "./jwtDecode";
14
15
  export { Logger, type LoggerSink, type LoggerSinkEntry, type LogLevel } from "./Logger";
15
16
  export {
16
17
  type AkanI18nConfig,
@@ -0,0 +1,17 @@
1
+ function decodeBase64Url(input: string): string {
2
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
3
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
4
+ if (typeof atob === "function") return atob(padded);
5
+ return Buffer.from(padded, "base64").toString("binary");
6
+ }
7
+
8
+ /** Decodes a JWT payload without validating its signature. */
9
+ export function decodeJwtPayload<T = unknown>(jwt: string): T {
10
+ const [, payload] = jwt.split(".");
11
+ if (!payload) throw new Error("Invalid JWT payload");
12
+ const binary = decodeBase64Url(payload);
13
+ const json = decodeURIComponent(
14
+ [...binary].map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`).join(""),
15
+ );
16
+ return JSON.parse(json) as T;
17
+ }
@@ -9,6 +9,8 @@ import { IncrementalBuilderHost } from "../incrementalBuilder";
9
9
  const backendMsgTypeSet = new Set<BuilderMessage["type"]>(["build-route"]);
10
10
  const BACKEND_RESTART_DEBOUNCE_MS = 120;
11
11
  const BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
12
+ const BACKEND_RECOVERY_BASE_DELAY_MS = 1_000;
13
+ const BACKEND_RECOVERY_MAX_DELAY_MS = 30_000;
12
14
  const BUILDER_READY_TIMEOUT_MS = 15000;
13
15
  const BUILDER_START_MAX_ATTEMPTS = 3;
14
16
  const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -132,6 +134,8 @@ export class AkanAppHost {
132
134
  #backendReady = false;
133
135
  #plannedBackendStops = new WeakSet<Bun.Subprocess<"ignore", "inherit", "inherit">>();
134
136
  #restartTimer: ReturnType<typeof setTimeout> | null = null;
137
+ #backendRecoveryTimer: ReturnType<typeof setTimeout> | null = null;
138
+ #backendRecoveryAttempts = 0;
135
139
  #restartFiles = new Set<string>();
136
140
  #latestPagesUpdated: Extract<BuilderMessage, { type: "pages-updated" }> | null = null;
137
141
  #latestCssUpdated: Extract<BuilderMessage, { type: "css-updated" }> | null = null;
@@ -162,6 +166,10 @@ export class AkanAppHost {
162
166
  clearTimeout(this.#restartTimer);
163
167
  this.#restartTimer = null;
164
168
  }
169
+ if (this.#backendRecoveryTimer) {
170
+ clearTimeout(this.#backendRecoveryTimer);
171
+ this.#backendRecoveryTimer = null;
172
+ }
165
173
  await this.#stopBackend();
166
174
  this.#stopBuilder();
167
175
  return this;
@@ -185,6 +193,7 @@ export class AkanAppHost {
185
193
  if (!msg || typeof msg !== "object") return;
186
194
  if (msg.type === "backend-ready") {
187
195
  this.#backendReady = true;
196
+ this.#backendRecoveryAttempts = 0;
188
197
  this.logger.verbose(`backend ready pid=${msg.pid}`);
189
198
  this.#replayBuilderState();
190
199
  return;
@@ -199,7 +208,7 @@ export class AkanAppHost {
199
208
  this.#plannedBackendStops.delete(backend);
200
209
  return;
201
210
  }
202
- this.#stopBuilder();
211
+ this.#scheduleBackendRecovery("backend-exit");
203
212
  },
204
213
  });
205
214
  this.#backend = backend;
@@ -245,6 +254,10 @@ export class AkanAppHost {
245
254
  }
246
255
  #scheduleBackendRestart(files: string[]) {
247
256
  for (const file of files) this.#restartFiles.add(file);
257
+ if (this.#backendRecoveryTimer) {
258
+ clearTimeout(this.#backendRecoveryTimer);
259
+ this.#backendRecoveryTimer = null;
260
+ }
248
261
  if (this.#restartTimer) clearTimeout(this.#restartTimer);
249
262
  this.#restartTimer = setTimeout(() => {
250
263
  this.#restartTimer = null;
@@ -255,9 +268,26 @@ export class AkanAppHost {
255
268
  }
256
269
  async #restartBackend(files: string[]) {
257
270
  this.logger.verbose(`[backend-reload] restarting backend for ${files.length} file(s)`);
271
+ this.#backendRecoveryAttempts = 0;
258
272
  await Promise.all([this.#stopBackend(), this.#backendGraph.refresh()]);
259
273
  this.#startBackend();
260
274
  }
275
+ #scheduleBackendRecovery(reason: string) {
276
+ if (this.#backendRecoveryTimer || this.#backend) return;
277
+ const attempt = this.#backendRecoveryAttempts;
278
+ const delay = Math.min(BACKEND_RECOVERY_BASE_DELAY_MS * 2 ** attempt, BACKEND_RECOVERY_MAX_DELAY_MS);
279
+ this.#backendRecoveryAttempts = attempt + 1;
280
+ this.logger.warn(
281
+ `[backend-recovery] backend exited unexpectedly (${reason}); restarting in ${delay}ms (attempt ${this.#backendRecoveryAttempts})`,
282
+ );
283
+ this.#backendRecoveryTimer = setTimeout(() => {
284
+ this.#backendRecoveryTimer = null;
285
+ if (this.#backend) return;
286
+ void this.#backendGraph.refresh().finally(() => {
287
+ if (!this.#backend) this.#startBackend();
288
+ });
289
+ }, delay);
290
+ }
261
291
  #enqueueBuilderMessage(message: BuilderMessage) {
262
292
  this.#builderMessageQueue = this.#builderMessageQueue
263
293
  .then(() => this.#handleBuilderMessage(message))
@@ -319,7 +349,6 @@ export class AkanAppHost {
319
349
  return new Promise<void>((resolve, reject) => {
320
350
  if (!this.#builder) throw new Error("Builder Not Found");
321
351
  let settled = false;
322
- let ready = false;
323
352
  const settle = (fn: () => void) => {
324
353
  if (settled) return;
325
354
  settled = true;
@@ -331,22 +360,30 @@ export class AkanAppHost {
331
360
  }, BUILDER_READY_TIMEOUT_MS);
332
361
  this.#builder.start({
333
362
  onExit: () => {
334
- if (settled && ready) {
335
- void this.#stopBackend();
336
- return;
337
- }
338
363
  settle(() => reject(new Error(`[cli] builder exited before emitting builder-ready (attempt ${attempt})`)));
339
364
  },
340
365
  onReady: () => {
341
- ready = true;
342
366
  settle(resolve);
343
367
  },
368
+ onRestartReady: () => {
369
+ this.logger.verbose("[builder-recovery] builder ready after restart; replaying latest state");
370
+ this.#replayBuilderState();
371
+ },
344
372
  });
345
373
  });
346
374
  }
347
375
  #sendToBuilder(message: BuilderMessage) {
348
- if (this.#builder) this.#builder.send(message);
349
- else this.logger.warn("akanAppHost is not running");
376
+ if (this.#builder?.send(message)) return;
377
+ if (message.type === "build-route") {
378
+ this.#sendToBackend({
379
+ type: "build-route-res",
380
+ id: message.id,
381
+ ok: false,
382
+ error: `builder is ${this.#builder?.status ?? "stopped"}; reload after the builder is ready`,
383
+ });
384
+ return;
385
+ }
386
+ this.logger.warn("akanAppHost builder is not running");
350
387
  }
351
388
  #stopBuilder() {
352
389
  if (!this.#builder) return;
@@ -269,7 +269,8 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
269
269
  const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
270
270
  if (rootVersion) return rootVersion;
271
271
  const akanPackageJson = getAkanPackageJson();
272
- if (AKAN_RUNTIME_PACKAGES.has(lib)) return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
272
+ if (AKAN_RUNTIME_PACKAGES.has(lib))
273
+ return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
273
274
  }
274
275
  getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
275
276
  return {
@@ -17,7 +17,17 @@ interface IncrementalBuilderHostOptions {
17
17
  onMessage: (message: BuilderMessage) => void;
18
18
  }
19
19
 
20
+ type IncrementalBuilderStatus = "starting" | "ready" | "restarting" | "stopped";
21
+
22
+ interface IncrementalBuilderStartOptions {
23
+ onExit?: () => void;
24
+ onReady?: () => void;
25
+ onRestartReady?: () => void;
26
+ }
27
+
20
28
  export class IncrementalBuilderHost {
29
+ static readonly #restartBaseDelayMs = 1_000;
30
+ static readonly #restartMaxDelayMs = 30_000;
21
31
  logger = new Logger("IncrementalBuilderHost");
22
32
  entry: string;
23
33
  env: Record<string, string>;
@@ -25,42 +35,106 @@ export class IncrementalBuilderHost {
25
35
  ready = false;
26
36
  readonly #onMessage: (message: BuilderMessage) => void;
27
37
  #proc: Bun.Subprocess<"ignore", "inherit", "inherit"> | null = null;
38
+ #status: IncrementalBuilderStatus = "stopped";
39
+ #restartAttempts = 0;
40
+ #restartTimer: ReturnType<typeof setTimeout> | null = null;
41
+ #manualStop = false;
42
+ #startOptions: IncrementalBuilderStartOptions = {};
28
43
  constructor({ app, entry, env, onMessage }: IncrementalBuilderHostOptions) {
29
44
  this.app = app;
30
45
  this.entry = entry;
31
46
  this.env = env;
32
47
  this.#onMessage = onMessage;
33
48
  }
34
- start({ onExit, onReady }: { onExit?: () => void; onReady?: () => void }) {
49
+ get status() {
50
+ return this.#status;
51
+ }
52
+ start(options: IncrementalBuilderStartOptions = {}) {
35
53
  if (this.#proc) this.stop();
36
- this.#proc = Bun.spawn(["bun", this.entry], {
54
+ this.#manualStop = false;
55
+ this.#startOptions = options;
56
+ this.#spawn(false);
57
+ return this;
58
+ }
59
+ #spawn(isRestart: boolean) {
60
+ this.#status = isRestart ? "restarting" : "starting";
61
+ this.ready = false;
62
+ let proc!: Bun.Subprocess<"ignore", "inherit", "inherit">;
63
+ proc = Bun.spawn(["bun", this.entry], {
37
64
  cwd: this.app.cwdPath,
38
65
  env: { ...this.env, AKAN_WATCH: "1" },
39
66
  stdio: ["ignore", "inherit", "inherit"],
40
67
  ipc: (msg: BuilderMessage) => {
68
+ if (this.#proc !== proc) return;
41
69
  if (!msg || typeof msg !== "object") return;
42
70
  if (builderMsgTypeSet.has(msg.type)) this.#onMessage(msg);
43
71
  if (msg.type === "builder-ready" && !this.ready) {
44
72
  this.ready = true;
45
- onReady?.();
73
+ this.#status = "ready";
74
+ this.#restartAttempts = 0;
75
+ if (isRestart) this.#startOptions.onRestartReady?.();
76
+ else this.#startOptions.onReady?.();
46
77
  }
47
78
  },
48
79
  serialization: "advanced",
49
80
  onExit: () => {
50
- onExit?.();
81
+ if (this.#proc !== proc) return;
82
+ this.#proc = null;
83
+ const wasReady = this.ready;
84
+ this.ready = false;
85
+ if (this.#manualStop || this.#status === "stopped") return;
86
+ if (!wasReady) {
87
+ this.#status = "stopped";
88
+ this.#startOptions.onExit?.();
89
+ return;
90
+ }
91
+ this.#scheduleRestart();
51
92
  },
52
93
  });
53
- this.logger.verbose(`builder spawned pid=${this.#proc.pid} entry=${this.entry}`);
54
- return this;
94
+ this.#proc = proc;
95
+ this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
96
+ }
97
+ #scheduleRestart() {
98
+ if (this.#manualStop || this.#restartTimer) return;
99
+ this.#status = "restarting";
100
+ const attempt = this.#restartAttempts;
101
+ const delay = Math.min(
102
+ IncrementalBuilderHost.#restartBaseDelayMs * 2 ** attempt,
103
+ IncrementalBuilderHost.#restartMaxDelayMs,
104
+ );
105
+ this.#restartAttempts = attempt + 1;
106
+ this.logger.warn(`builder exited after ready; restarting in ${delay}ms (attempt ${this.#restartAttempts})`);
107
+ this.#restartTimer = setTimeout(() => {
108
+ this.#restartTimer = null;
109
+ if (this.#manualStop) return;
110
+ this.#spawn(true);
111
+ }, delay);
55
112
  }
56
- send(message: BuilderMessage) {
57
- if (this.#proc) this.#proc.send(message);
58
- else this.logger.warn("incrementalBuilderHost is not running");
113
+ send(message: BuilderMessage): boolean {
114
+ if (!this.#proc || this.#status !== "ready") {
115
+ this.logger.warn(`incrementalBuilderHost is ${this.#status}; cannot send ${message.type}`);
116
+ return false;
117
+ }
118
+ try {
119
+ this.#proc.send(message);
120
+ return true;
121
+ } catch (error) {
122
+ this.logger.warn(
123
+ `failed to send ${message.type} to builder: ${error instanceof Error ? error.message : String(error)}`,
124
+ );
125
+ return false;
126
+ }
59
127
  }
60
128
  stop() {
129
+ this.#manualStop = true;
130
+ if (this.#restartTimer) {
131
+ clearTimeout(this.#restartTimer);
132
+ this.#restartTimer = null;
133
+ }
61
134
  if (this.#proc) this.#proc.kill();
62
135
  this.#proc = null;
63
136
  this.ready = false;
137
+ this.#status = "stopped";
64
138
  }
65
139
  static async create(app: App, env: Record<string, string>, onMessage: (message: BuilderMessage) => void) {
66
140
  const candidates = [
@@ -1,11 +1,8 @@
1
1
  import type { QueryOf } from "akanjs/constant";
2
- import DataLoader from "dataloader";
3
- import { get, groupBy, keyBy } from "lodash";
4
2
 
5
3
  export const Id = String;
6
4
  export const ObjectId = String;
7
5
  export const Mixed = Object;
8
- export { DataLoader };
9
6
 
10
7
  type LoaderItem = Record<string, unknown>;
11
8
  type LoaderModel = {
@@ -13,6 +10,143 @@ type LoaderModel = {
13
10
  };
14
11
  type ArrayElementLoaderItem = LoaderItem & { key: unknown };
15
12
  type QueryRecord = Record<string, unknown>;
13
+ type BatchLoadFn<Key, Value> = (
14
+ keys: readonly Key[],
15
+ ) => PromiseLike<ReadonlyArray<Value | Error>> | ReadonlyArray<Value | Error>;
16
+
17
+ interface DataLoaderOptions<Key, CacheKey> {
18
+ cache?: boolean;
19
+ cacheKeyFn?: (key: Key) => CacheKey;
20
+ batch?: boolean;
21
+ batchScheduleFn?: (callback: () => void) => void;
22
+ maxBatchSize?: number;
23
+ name?: string;
24
+ }
25
+
26
+ interface BatchItem<Key, Value> {
27
+ key: Key;
28
+ resolve: (value: Value) => void;
29
+ reject: (reason: unknown) => void;
30
+ }
31
+
32
+ /** Minimal DataLoader-compatible batch loader used by Akan document resolvers. */
33
+ export class DataLoader<Key, Value, CacheKey = Key> {
34
+ readonly name?: string;
35
+ readonly #batchLoadFn: BatchLoadFn<Key, Value>;
36
+ readonly #cache: boolean;
37
+ readonly #cacheKeyFn: (key: Key) => CacheKey;
38
+ readonly #batch: boolean;
39
+ readonly #batchScheduleFn: (callback: () => void) => void;
40
+ readonly #maxBatchSize: number;
41
+ readonly #promiseCache = new Map<CacheKey, Promise<Value>>();
42
+ #queue: BatchItem<Key, Value>[] = [];
43
+ #scheduled = false;
44
+
45
+ constructor(batchLoadFn: BatchLoadFn<Key, Value>, options: DataLoaderOptions<Key, CacheKey> = {}) {
46
+ this.#batchLoadFn = batchLoadFn;
47
+ this.#cache = options.cache !== false;
48
+ this.#cacheKeyFn = options.cacheKeyFn ?? ((key) => key as unknown as CacheKey);
49
+ this.#batch = options.batch !== false;
50
+ this.#batchScheduleFn = options.batchScheduleFn ?? ((callback) => queueMicrotask(callback));
51
+ this.#maxBatchSize = options.maxBatchSize ?? Number.POSITIVE_INFINITY;
52
+ this.name = options.name;
53
+ }
54
+
55
+ load(key: Key): Promise<Value> {
56
+ const cacheKey = this.#cacheKeyFn(key);
57
+ if (this.#cache) {
58
+ const cached = this.#promiseCache.get(cacheKey);
59
+ if (cached) return cached;
60
+ }
61
+
62
+ const promise = new Promise<Value>((resolve, reject) => {
63
+ this.#queue.push({ key, resolve, reject });
64
+ if (this.#batch) this.#schedule();
65
+ else this.#dispatch();
66
+ });
67
+ if (this.#cache) this.#promiseCache.set(cacheKey, promise);
68
+ return promise;
69
+ }
70
+
71
+ async loadMany(keys: readonly Key[]): Promise<Array<Value | Error>> {
72
+ const results = await Promise.allSettled(keys.map((key) => this.load(key)));
73
+ return results.map((result) => (result.status === "fulfilled" ? result.value : toError(result.reason)));
74
+ }
75
+
76
+ clear(key: Key): this {
77
+ this.#promiseCache.delete(this.#cacheKeyFn(key));
78
+ return this;
79
+ }
80
+
81
+ clearAll(): this {
82
+ this.#promiseCache.clear();
83
+ return this;
84
+ }
85
+
86
+ prime(key: Key, value: Value | Error): this {
87
+ if (!this.#cache) return this;
88
+ const cacheKey = this.#cacheKeyFn(key);
89
+ if (this.#promiseCache.has(cacheKey)) return this;
90
+ this.#promiseCache.set(cacheKey, value instanceof Error ? Promise.reject(value) : Promise.resolve(value));
91
+ return this;
92
+ }
93
+
94
+ #schedule() {
95
+ if (this.#scheduled) return;
96
+ this.#scheduled = true;
97
+ this.#batchScheduleFn(() => this.#dispatch());
98
+ }
99
+
100
+ #dispatch() {
101
+ this.#scheduled = false;
102
+ const batch = this.#queue.splice(0, this.#maxBatchSize);
103
+ if (this.#queue.length > 0) this.#schedule();
104
+ if (batch.length === 0) return;
105
+ const keys = batch.map(({ key }) => key);
106
+ Promise.resolve(this.#batchLoadFn(keys)).then(
107
+ (values) => {
108
+ if (values.length !== batch.length) {
109
+ const error = new Error(`DataLoader expected ${batch.length} values, received ${values.length}`);
110
+ batch.forEach(({ reject }) => {
111
+ reject(error);
112
+ });
113
+ return;
114
+ }
115
+ values.forEach((value, index) => {
116
+ if (value instanceof Error) batch[index]?.reject(value);
117
+ else batch[index]?.resolve(value as Value);
118
+ });
119
+ },
120
+ (error) => {
121
+ batch.forEach(({ reject }) => {
122
+ reject(error);
123
+ });
124
+ },
125
+ );
126
+ }
127
+ }
128
+
129
+ function toError(reason: unknown): Error {
130
+ return reason instanceof Error ? reason : new Error(String(reason));
131
+ }
132
+
133
+ function keyBy<T>(items: T[], keyOrGetter: keyof T | ((item: T) => unknown)): Record<string, T> {
134
+ const entries = items.map((item) => {
135
+ const key = typeof keyOrGetter === "function" ? keyOrGetter(item) : item[keyOrGetter];
136
+ return [String(key), item] as const;
137
+ });
138
+ return Object.fromEntries(entries);
139
+ }
140
+
141
+ function groupBy<T>(items: T[], getKey: (item: T) => unknown): Record<string, T[]> {
142
+ const groups: Record<string, T[]> = {};
143
+ for (const item of items) {
144
+ const key = String(getKey(item));
145
+ groups[key] ??= [];
146
+ groups[key].push(item);
147
+ }
148
+ return groups;
149
+ }
16
150
 
17
151
  const setQueryOperator = (query: QueryOf<unknown>, fieldName: string, op: "oneOf" | "has", value: unknown) => {
18
152
  (query as QueryRecord)[fieldName] = { kind: "op", op, value };
@@ -25,7 +159,7 @@ export const createLoader = <Key, Value>(model: LoaderModel, fieldName = "id", d
25
159
  setQueryOperator(query, fieldName, "oneOf", fields);
26
160
  const data = Promise.resolve(model.find(query)).then((list) => {
27
161
  const listByKey = keyBy(list, fieldName);
28
- return fields.map((id: unknown) => get(listByKey, String(id), null));
162
+ return fields.map((id: unknown) => listByKey[String(id)] ?? null);
29
163
  });
30
164
  return data as unknown as Promise<Value[]>;
31
165
  },
@@ -60,7 +194,7 @@ export const createArrayElementLoader = <K, V>(
60
194
  }));
61
195
  });
62
196
  const listByKey = groupBy(flat, (dat) => dat.key);
63
- return fields.map((id) => get(listByKey, String(id), null));
197
+ return fields.map((id) => listByKey[String(id)] ?? null);
64
198
  });
65
199
  return data as unknown as Promise<V[]>;
66
200
  },
@@ -80,7 +214,7 @@ export const createQueryLoader = <Key, Value>(
80
214
  queryKeys.map((key) => String((query as QueryRecord)[key])).join("");
81
215
  const data = Promise.resolve(model.find(query)).then((list) => {
82
216
  const listByKey = keyBy(list, getQueryKey);
83
- return queries.map((query: QueryOf<unknown>) => get(listByKey, getQueryKey(query), null));
217
+ return queries.map((query: QueryOf<unknown>) => listByKey[getQueryKey(query)] ?? null);
84
218
  });
85
219
  return data as unknown as Promise<Value[]>;
86
220
  },
@@ -2,7 +2,7 @@ import type { Dayjs, MergedValues, PromiseOrObject } from "akanjs/base";
2
2
  import { Logger } from "akanjs/common";
3
3
  import type { DocumentModel, QueryOf } from "akanjs/constant";
4
4
  import type { CacheAdaptor } from "akanjs/service";
5
- import type DataLoader from "dataloader";
5
+ import type { DataLoader } from "./dataLoader";
6
6
  import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta";
7
7
  import type { CRUDEventType, Mdl, SaveEventType } from "./into";
8
8
  import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
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": {