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/cli/templates/lib/sig.ts
CHANGED
|
@@ -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 {
|
|
3
|
-
import
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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.#
|
|
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
|
|
349
|
-
|
|
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))
|
|
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
|
-
|
|
49
|
+
get status() {
|
|
50
|
+
return this.#status;
|
|
51
|
+
}
|
|
52
|
+
start(options: IncrementalBuilderStartOptions = {}) {
|
|
35
53
|
if (this.#proc) this.stop();
|
|
36
|
-
this.#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
|
|
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
|
|
58
|
-
|
|
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 = [
|
package/document/dataLoader.ts
CHANGED
|
@@ -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) =>
|
|
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) =>
|
|
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>) =>
|
|
217
|
+
return queries.map((query: QueryOf<unknown>) => listByKey[getQueryKey(query)] ?? null);
|
|
84
218
|
});
|
|
85
219
|
return data as unknown as Promise<Value[]>;
|
|
86
220
|
},
|
package/document/database.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Dayjs, MergedValues, PromiseOrObject } from "akanjs/base";
|
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
3
|
import type { DocumentModel, QueryOf } from "akanjs/constant";
|
|
4
4
|
import type { CacheAdaptor } from "akanjs/service";
|
|
5
|
-
import type DataLoader from "
|
|
5
|
+
import type { DataLoader } from "./dataLoader";
|
|
6
6
|
import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta";
|
|
7
7
|
import type { CRUDEventType, Mdl, SaveEventType } from "./into";
|
|
8
8
|
import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akanjs",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.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": {
|