akanjs 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/cli/application/application.command.ts +4 -1
- package/cli/application/application.runner.ts +6 -8
- package/cli/build.ts +3 -1
- package/cli/cloud/cloud.runner.ts +7 -8
- package/cli/index.js +288 -115
- package/cli/library/library.runner.ts +2 -2
- package/cli/module/module.runner.ts +2 -2
- package/cli/npmRegistry.ts +13 -0
- package/cli/openBrowser.ts +15 -0
- package/cli/pluralizeName.ts +5 -0
- package/cli/scalar/scalar.prompt.ts +2 -2
- package/cli/scalar/scalar.runner.ts +2 -2
- package/cli/semver.ts +18 -0
- package/cli/templates/lib/sig.ts +2 -2
- package/cli/workspace/workspace.runner.ts +3 -3
- package/client/cookie.ts +10 -15
- package/common/index.ts +1 -0
- package/common/jwtDecode.ts +17 -0
- package/constant/serialize.ts +1 -1
- package/devkit/akanApp/akanApp.host.ts +46 -9
- package/devkit/akanConfig/akanConfig.ts +2 -1
- package/devkit/capacitor.base.config.ts +18 -4
- package/devkit/capacitorApp.ts +118 -64
- package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
- package/devkit/mobile/mobileTarget.ts +2 -1
- package/devkit/scanInfo.ts +1 -0
- package/document/dataLoader.ts +140 -6
- package/document/database.ts +1 -1
- package/package.json +7 -13
- package/server/akanApp.ts +250 -44
- package/server/di/diLifecycle.ts +1 -1
- package/server/processMetricsCollector.ts +79 -1
- package/server/proxy/localeWebProxy.ts +29 -12
- package/server/resolver/database.resolver.ts +82 -31
- package/server/resolver/signal.resolver.ts +67 -28
- package/service/ipcTypes.ts +5 -0
- package/service/predefinedAdaptor/database.adaptor.ts +95 -27
- package/service/predefinedAdaptor/solidSqlite.ts +7 -7
- package/service/predefinedAdaptor/storage.adaptor.ts +35 -9
- package/service/serviceModule.ts +1 -6
- package/signal/base.signal.ts +1 -1
- package/signal/index.ts +1 -0
- package/signal/middleware.ts +5 -1
- package/signal/signalContext.ts +85 -31
- package/signal/signalRegistry.ts +35 -10
- package/signal/trace.ts +279 -0
- package/types/cli/npmRegistry.d.ts +1 -0
- package/types/cli/openBrowser.d.ts +1 -0
- package/types/cli/pluralizeName.d.ts +1 -0
- package/types/cli/semver.d.ts +1 -0
- package/types/client/cookie.d.ts +6 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/jwtDecode.d.ts +2 -0
- package/types/devkit/capacitorApp.d.ts +14 -5
- package/types/devkit/incrementalBuilder/incrementalBuilder.host.d.ts +9 -5
- package/types/document/dataLoader.d.ts +21 -2
- package/types/document/database.d.ts +1 -1
- package/types/server/processMetricsCollector.d.ts +2 -0
- package/types/service/ipcTypes.d.ts +5 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +26 -32
- package/types/service/predefinedAdaptor/solidSqlite.d.ts +3 -3
- package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -2
- package/types/service/serviceModule.d.ts +1 -1
- package/types/signal/index.d.ts +1 -0
- package/types/signal/signalContext.d.ts +4 -1
- package/types/signal/signalRegistry.d.ts +25 -4
- package/types/signal/trace.d.ts +97 -0
- package/types/ui/Signal/style.d.ts +15 -0
- package/ui/Signal/Arg.tsx +22 -15
- package/ui/Signal/Doc.tsx +30 -24
- package/ui/Signal/Listener.tsx +15 -39
- package/ui/Signal/Message.tsx +32 -50
- package/ui/Signal/Object.tsx +16 -13
- package/ui/Signal/PubSub.tsx +29 -47
- package/ui/Signal/Response.tsx +7 -17
- package/ui/Signal/RestApi.tsx +41 -57
- package/ui/Signal/WebSocket.tsx +1 -1
- package/ui/Signal/style.ts +36 -0
- package/webkit/useCsrValues.ts +147 -37
package/devkit/capacitorApp.ts
CHANGED
|
@@ -15,37 +15,53 @@ interface RunConfig {
|
|
|
15
15
|
regenerate?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
interface PrepareConfig extends RunConfig {}
|
|
19
|
+
|
|
18
20
|
export class CapacitorApp {
|
|
19
21
|
project: MobileProject & { ios: IosProject; android: AndroidProject };
|
|
20
22
|
iosTargetName = "App";
|
|
21
23
|
readonly targetRoot: string;
|
|
24
|
+
readonly targetRootPath: string;
|
|
25
|
+
readonly targetWebRoot: string;
|
|
26
|
+
readonly targetAssetRoot: string;
|
|
27
|
+
readonly iosRootPath = "ios";
|
|
28
|
+
readonly iosProjectPath = "ios/App";
|
|
29
|
+
readonly androidRootPath = "android";
|
|
22
30
|
constructor(
|
|
23
31
|
private readonly app: AppExecutor,
|
|
24
32
|
readonly target: AkanMobileTargetConfig,
|
|
25
33
|
) {
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
this.targetRootPath = path.posix.join("mobile", this.target.name);
|
|
35
|
+
this.targetRoot = path.join(this.app.cwdPath, this.targetRootPath);
|
|
36
|
+
this.targetWebRoot = path.join(this.targetRoot, "www");
|
|
37
|
+
this.targetAssetRoot = path.join(this.targetRoot, "assets");
|
|
38
|
+
this.project = new MobileProject(this.app.cwdPath, {
|
|
39
|
+
android: { path: this.androidRootPath },
|
|
40
|
+
ios: { path: this.iosProjectPath },
|
|
30
41
|
}) as MobileProject & { ios: IosProject; android: AndroidProject };
|
|
31
42
|
}
|
|
32
|
-
async init({
|
|
43
|
+
async init({
|
|
44
|
+
platform,
|
|
45
|
+
operation = "release",
|
|
46
|
+
env = "debug",
|
|
47
|
+
regenerate = false,
|
|
48
|
+
}: { platform?: "ios" | "android" } & Partial<PrepareConfig> = {}) {
|
|
33
49
|
await mkdir(this.targetRoot, { recursive: true });
|
|
34
50
|
await this.#writeCapacitorConfig();
|
|
35
51
|
if (regenerate) {
|
|
36
52
|
if (!platform || platform === "ios")
|
|
37
|
-
await rm(path.join(this.
|
|
53
|
+
await rm(path.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
|
|
38
54
|
if (!platform || platform === "android")
|
|
39
|
-
await rm(path.join(this.
|
|
55
|
+
await rm(path.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
|
|
40
56
|
}
|
|
41
57
|
const project = this.project as MobileProject;
|
|
42
58
|
await this.project.load();
|
|
43
59
|
if ((!platform || platform === "android") && !project.android) {
|
|
44
|
-
await this.#
|
|
60
|
+
await this.#spawnMobile("npx", ["cap", "add", "android"], { operation, env });
|
|
45
61
|
await this.project.load();
|
|
46
62
|
}
|
|
47
63
|
if ((!platform || platform === "ios") && !project.ios) {
|
|
48
|
-
await this.#
|
|
64
|
+
await this.#spawnMobile("npx", ["cap", "add", "ios"], { operation, env });
|
|
49
65
|
await this.project.load();
|
|
50
66
|
}
|
|
51
67
|
return this;
|
|
@@ -53,58 +69,54 @@ export class CapacitorApp {
|
|
|
53
69
|
async save() {
|
|
54
70
|
await this.project.commit();
|
|
55
71
|
}
|
|
56
|
-
async #prepareIos({ regenerate = false }:
|
|
57
|
-
await this.init({ platform: "ios", regenerate });
|
|
72
|
+
async #prepareIos({ operation, env, regenerate = false }: PrepareConfig) {
|
|
73
|
+
await this.init({ platform: "ios", operation, env, regenerate });
|
|
58
74
|
await this.#prepareTargetAssets();
|
|
59
75
|
await this.#prepareExternalFiles("ios");
|
|
60
76
|
await this.#applyIosMetadata();
|
|
61
77
|
await this.#applyPermissions();
|
|
62
78
|
await this.#applyLinks();
|
|
63
79
|
await this.project.commit();
|
|
64
|
-
await this.#generateAssets();
|
|
80
|
+
await this.#generateAssets({ operation, env });
|
|
65
81
|
this.app.verbose(`syncing iOS`);
|
|
66
|
-
await this.#
|
|
82
|
+
await this.#spawnMobile("npx", ["cap", "sync", "ios"], { operation, env });
|
|
67
83
|
this.app.verbose(`sync completed.`);
|
|
68
84
|
}
|
|
69
|
-
async buildIos(
|
|
85
|
+
async buildIos({ env = "debug", regenerate = false }: { env?: RunConfig["env"]; regenerate?: boolean } = {}) {
|
|
70
86
|
await this.prepareWww();
|
|
71
|
-
await this.#prepareIos(
|
|
72
|
-
await this.#
|
|
87
|
+
await this.#prepareIos({ operation: "release", env, regenerate });
|
|
88
|
+
await this.#spawnMobile("npx", ["cap", "build", "ios"], { operation: "release", env }, { stdio: "inherit" });
|
|
73
89
|
this.app.verbose(`build completed iOS.`);
|
|
74
90
|
return;
|
|
75
91
|
}
|
|
76
92
|
async syncIos() {
|
|
77
|
-
await this.#
|
|
93
|
+
await this.#spawnMobile("npx", ["cap", "sync", "ios"], { operation: "local", env: "local" });
|
|
78
94
|
}
|
|
79
95
|
async openIos() {
|
|
80
|
-
await this.#
|
|
96
|
+
await this.#spawnMobile("npx", ["cap", "open", "ios"], { operation: "local", env: "local" });
|
|
81
97
|
}
|
|
82
98
|
async runIos({ operation, env, regenerate = false }: RunConfig) {
|
|
83
99
|
if (operation === "release") await this.prepareWww();
|
|
84
|
-
await this.#prepareIos({ regenerate });
|
|
100
|
+
await this.#prepareIos({ operation, env, regenerate });
|
|
85
101
|
const args = ["cap", "run", "ios"];
|
|
86
|
-
|
|
87
|
-
await this.#spawn("npx", args, {
|
|
88
|
-
env: this.#commandEnv(operation, env),
|
|
89
|
-
stdio: "inherit",
|
|
90
|
-
});
|
|
102
|
+
await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
async #prepareAndroid({ regenerate = false }:
|
|
94
|
-
await this.init({ platform: "android", regenerate });
|
|
105
|
+
async #prepareAndroid({ operation, env, regenerate = false }: PrepareConfig) {
|
|
106
|
+
await this.init({ platform: "android", operation, env, regenerate });
|
|
95
107
|
await this.#prepareTargetAssets();
|
|
96
108
|
await this.#prepareExternalFiles("android");
|
|
97
109
|
await this.#applyAndroidMetadata();
|
|
98
110
|
await this.#applyPermissions();
|
|
99
111
|
await this.#applyLinks();
|
|
100
112
|
await this.project.commit();
|
|
101
|
-
await this.#generateAssets();
|
|
102
|
-
await this.#
|
|
113
|
+
await this.#generateAssets({ operation, env });
|
|
114
|
+
await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
async #updateAndroidBuildTypes() {
|
|
106
118
|
|
|
107
|
-
const appGradle = await FileEditor.create(
|
|
119
|
+
const appGradle = await FileEditor.create(path.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
|
|
108
120
|
const buildTypesBlock = `
|
|
109
121
|
debug {
|
|
110
122
|
applicationIdSuffix ".debug"
|
|
@@ -139,9 +151,12 @@ export class CapacitorApp {
|
|
|
139
151
|
}
|
|
140
152
|
await appGradle.save();
|
|
141
153
|
}
|
|
142
|
-
async buildAndroid(
|
|
154
|
+
async buildAndroid(
|
|
155
|
+
assembleType: "apk" | "aab",
|
|
156
|
+
{ env = "debug", regenerate = false }: { env?: RunConfig["env"]; regenerate?: boolean } = {},
|
|
157
|
+
) {
|
|
143
158
|
await this.prepareWww();
|
|
144
|
-
await this.#prepareAndroid(
|
|
159
|
+
await this.#prepareAndroid({ operation: "release", env, regenerate });
|
|
145
160
|
await this.#updateAndroidBuildTypes();
|
|
146
161
|
|
|
147
162
|
const isWindows = process.platform === "win32";
|
|
@@ -149,45 +164,44 @@ export class CapacitorApp {
|
|
|
149
164
|
|
|
150
165
|
await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
|
|
151
166
|
stdio: "inherit",
|
|
152
|
-
cwd:
|
|
167
|
+
cwd: path.join(this.app.cwdPath, this.androidRootPath),
|
|
168
|
+
env: this.#commandEnv("release", env),
|
|
153
169
|
});
|
|
154
170
|
}
|
|
155
171
|
async openAndroid() {
|
|
156
|
-
await this.#
|
|
172
|
+
await this.#spawnMobile("npx", ["cap", "open", "android"], { operation: "local", env: "local" });
|
|
157
173
|
}
|
|
158
174
|
async syncAndroid(options: { regenerate?: boolean } = {}) {
|
|
159
175
|
await this.prepareWww();
|
|
160
|
-
await this.#prepareAndroid(options);
|
|
176
|
+
await this.#prepareAndroid({ operation: "release", env: "debug", ...options });
|
|
161
177
|
this.app.log(`Sync Android Completed.`);
|
|
162
178
|
}
|
|
163
179
|
async runAndroid({ operation, env, regenerate = false }: RunConfig) {
|
|
164
180
|
if (operation === "release") await this.prepareWww();
|
|
165
|
-
await this.#prepareAndroid({ regenerate });
|
|
181
|
+
await this.#prepareAndroid({ operation, env, regenerate });
|
|
166
182
|
this.app.logger.info(`Running Android in ${operation} mode on ${env} env`);
|
|
167
183
|
const args = ["cap", "run", "android"];
|
|
168
|
-
|
|
169
|
-
await this.#spawn("npx", args, {
|
|
170
|
-
env: this.#commandEnv(operation, env),
|
|
171
|
-
stdio: "inherit",
|
|
172
|
-
});
|
|
184
|
+
await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
async releaseIos() {
|
|
176
188
|
await this.prepareWww();
|
|
177
|
-
await this.#prepareIos();
|
|
189
|
+
await this.#prepareIos({ operation: "release", env: "main" });
|
|
178
190
|
}
|
|
179
191
|
async releaseAndroid() {
|
|
180
192
|
await this.prepareWww();
|
|
181
|
-
await this.#prepareAndroid();
|
|
193
|
+
await this.#prepareAndroid({ operation: "release", env: "main" });
|
|
182
194
|
}
|
|
183
195
|
async prepareWww() {
|
|
184
196
|
const htmlSource = path.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
|
|
185
197
|
if (!(await Bun.file(htmlSource).exists()))
|
|
186
198
|
throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
|
|
187
|
-
|
|
188
|
-
await
|
|
189
|
-
await
|
|
190
|
-
|
|
199
|
+
await rm(this.targetWebRoot, { recursive: true, force: true });
|
|
200
|
+
await mkdir(this.targetWebRoot, { recursive: true });
|
|
201
|
+
await Bun.write(
|
|
202
|
+
path.join(this.targetWebRoot, "index.html"),
|
|
203
|
+
this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()),
|
|
204
|
+
);
|
|
191
205
|
}
|
|
192
206
|
#injectMobileTargetMeta(html: string) {
|
|
193
207
|
const basePath = this.target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
@@ -198,45 +212,75 @@ export class CapacitorApp {
|
|
|
198
212
|
async #writeCapacitorConfig() {
|
|
199
213
|
await mkdir(this.targetRoot, { recursive: true });
|
|
200
214
|
const appInfoPath = path
|
|
201
|
-
.relative(this.
|
|
215
|
+
.relative(this.app.cwdPath, path.join(this.app.cwdPath, "akan.app.json"))
|
|
202
216
|
.split(path.sep)
|
|
203
217
|
.join("/");
|
|
204
218
|
const baseConfigPath = path
|
|
205
|
-
.relative(this.
|
|
219
|
+
.relative(this.app.cwdPath, path.join(this.app.workspace.cwdPath, "pkgs/akanjs/devkit/capacitor.base.config"))
|
|
206
220
|
.split(path.sep)
|
|
207
221
|
.join("/");
|
|
208
|
-
const content = `import {
|
|
209
|
-
import
|
|
222
|
+
const content = `import type { AppScanResult } from "akanjs/devkit";
|
|
223
|
+
import { withBase } from "${baseConfigPath.startsWith(".") ? baseConfigPath : `./${baseConfigPath}`}";
|
|
224
|
+
import appInfo from "${appInfoPath.startsWith(".") ? appInfoPath : `./${appInfoPath}`}";
|
|
210
225
|
|
|
211
|
-
export default withBase(
|
|
226
|
+
export default withBase(
|
|
227
|
+
(config, target) => ({
|
|
228
|
+
...config,
|
|
229
|
+
webDir: \`mobile/\${target.name}/www\`,
|
|
230
|
+
android: {
|
|
231
|
+
...config.android,
|
|
232
|
+
path: "android",
|
|
233
|
+
},
|
|
234
|
+
ios: {
|
|
235
|
+
...config.ios,
|
|
236
|
+
path: "ios",
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
appInfo as AppScanResult,
|
|
240
|
+
);
|
|
212
241
|
`;
|
|
213
|
-
await Bun.write(path.join(this.
|
|
242
|
+
await Bun.write(path.join(this.app.cwdPath, "capacitor.config.ts"), content);
|
|
214
243
|
}
|
|
215
244
|
async #prepareTargetAssets() {
|
|
216
245
|
if (!this.target.assets) return;
|
|
217
|
-
|
|
218
|
-
await mkdir(assetsDir, { recursive: true });
|
|
246
|
+
await mkdir(this.targetAssetRoot, { recursive: true });
|
|
219
247
|
if (this.target.assets.icon)
|
|
220
|
-
await cp(path.join(this.app.cwdPath, this.target.assets.icon), path.join(
|
|
248
|
+
await cp(path.join(this.app.cwdPath, this.target.assets.icon), path.join(this.targetAssetRoot, "icon.png"), {
|
|
249
|
+
force: true,
|
|
250
|
+
});
|
|
221
251
|
if (this.target.assets.splash)
|
|
222
|
-
await cp(path.join(this.app.cwdPath, this.target.assets.splash), path.join(
|
|
252
|
+
await cp(path.join(this.app.cwdPath, this.target.assets.splash), path.join(this.targetAssetRoot, "splash.png"), {
|
|
223
253
|
force: true,
|
|
224
254
|
});
|
|
225
255
|
}
|
|
226
256
|
async #prepareExternalFiles(platform: "ios" | "android") {
|
|
227
257
|
const files = this.target.files?.[platform];
|
|
228
258
|
if (!files) return;
|
|
259
|
+
const platformRoot = path.join(this.app.cwdPath, platform === "ios" ? this.iosRootPath : this.androidRootPath);
|
|
229
260
|
await Promise.all(
|
|
230
261
|
Object.entries(files).map(async ([to, from]) => {
|
|
231
|
-
const targetPath = path.join(
|
|
262
|
+
const targetPath = path.join(platformRoot, to);
|
|
232
263
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
233
264
|
await cp(path.join(this.app.cwdPath, from), targetPath, { force: true });
|
|
234
265
|
}),
|
|
235
266
|
);
|
|
236
267
|
}
|
|
237
|
-
async #generateAssets() {
|
|
268
|
+
async #generateAssets({ operation, env }: Pick<RunConfig, "operation" | "env">) {
|
|
238
269
|
if (!this.target.assets) return;
|
|
239
|
-
await this.#
|
|
270
|
+
await this.#spawnMobile(
|
|
271
|
+
"npx",
|
|
272
|
+
[
|
|
273
|
+
"@capacitor/assets",
|
|
274
|
+
"generate",
|
|
275
|
+
"--assetPath",
|
|
276
|
+
path.posix.join(this.targetRootPath, "assets"),
|
|
277
|
+
"--iosProject",
|
|
278
|
+
this.iosProjectPath,
|
|
279
|
+
"--androidProject",
|
|
280
|
+
this.androidRootPath,
|
|
281
|
+
],
|
|
282
|
+
{ operation, env },
|
|
283
|
+
);
|
|
240
284
|
}
|
|
241
285
|
async #applyIosMetadata() {
|
|
242
286
|
this.project.ios.setBundleId("App", "Debug", this.target.appId);
|
|
@@ -291,16 +335,26 @@ export default withBase((config) => config, appInfo, "${this.target.name}");
|
|
|
291
335
|
}
|
|
292
336
|
}
|
|
293
337
|
#commandEnv(operation: "local" | "release", env: "local" | "debug" | "develop" | "main") {
|
|
294
|
-
return {
|
|
295
|
-
...process.env,
|
|
338
|
+
return this.app.getCommandEnv({
|
|
296
339
|
APP_OPERATION_MODE: operation,
|
|
297
|
-
AKAN_PUBLIC_OPERATION_MODE:
|
|
340
|
+
AKAN_PUBLIC_OPERATION_MODE: env === "local" ? "local" : "cloud",
|
|
298
341
|
AKAN_PUBLIC_ENV: env,
|
|
299
342
|
AKAN_MOBILE_TARGET: this.target.name,
|
|
300
|
-
};
|
|
343
|
+
});
|
|
301
344
|
}
|
|
302
345
|
async #spawn(command: string, args: string[] = [], options: Parameters<AppExecutor["spawn"]>[2] = {}) {
|
|
303
|
-
return await this.app.spawn(command, args, { cwd: this.
|
|
346
|
+
return await this.app.spawn(command, args, { cwd: this.app.cwdPath, ...options });
|
|
347
|
+
}
|
|
348
|
+
async #spawnMobile(
|
|
349
|
+
command: string,
|
|
350
|
+
args: string[] = [],
|
|
351
|
+
{ operation, env }: Pick<RunConfig, "operation" | "env">,
|
|
352
|
+
options: Parameters<AppExecutor["spawn"]>[2] = {},
|
|
353
|
+
) {
|
|
354
|
+
return await this.#spawn(command, args, {
|
|
355
|
+
...options,
|
|
356
|
+
env: { ...this.#commandEnv(operation, env), ...options.env },
|
|
357
|
+
});
|
|
304
358
|
}
|
|
305
359
|
async addCamera() {
|
|
306
360
|
await this.#setPermissionInIos({
|
|
@@ -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 = [
|
|
@@ -36,9 +36,10 @@ const resolveMobileTargetByBasePath = (
|
|
|
36
36
|
const [template] = targets;
|
|
37
37
|
if (!template) return undefined;
|
|
38
38
|
return {
|
|
39
|
-
name:
|
|
39
|
+
name: normalizedBasePath,
|
|
40
40
|
config: {
|
|
41
41
|
...template.config,
|
|
42
|
+
name: normalizedBasePath,
|
|
42
43
|
basePath: normalizedBasePath,
|
|
43
44
|
},
|
|
44
45
|
};
|
package/devkit/scanInfo.ts
CHANGED
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
|
},
|