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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AkanMetricsReport } from "akanjs/service";
|
|
2
|
+
import { getTraceSnapshot, isTraceEnabled } from "akanjs/signal";
|
|
2
3
|
|
|
3
4
|
type BunJscHeapStats = {
|
|
4
5
|
heapSize?: number;
|
|
@@ -8,19 +9,83 @@ type BunJscHeapStats = {
|
|
|
8
9
|
protectedObjectCount?: number;
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Samples event-loop scheduling delay by measuring how late a fixed-interval timer
|
|
14
|
+
* actually fires. The sample window is summarized and reset on each metrics report,
|
|
15
|
+
* so values reflect recent load rather than process lifetime.
|
|
16
|
+
*/
|
|
17
|
+
class EventLoopLagMonitor {
|
|
18
|
+
static readonly #maxSamples = 600;
|
|
19
|
+
#timer: ReturnType<typeof setInterval> | null = null;
|
|
20
|
+
#intervalMs = 500;
|
|
21
|
+
#lastTickAt = 0;
|
|
22
|
+
#samples: number[] = [];
|
|
23
|
+
#maxMs = 0;
|
|
24
|
+
|
|
25
|
+
start(intervalMs = 500): void {
|
|
26
|
+
if (this.#timer) return;
|
|
27
|
+
this.#intervalMs = intervalMs;
|
|
28
|
+
this.#lastTickAt = performance.now();
|
|
29
|
+
this.#timer = setInterval(() => {
|
|
30
|
+
const now = performance.now();
|
|
31
|
+
const lag = Math.max(0, now - this.#lastTickAt - this.#intervalMs);
|
|
32
|
+
this.#lastTickAt = now;
|
|
33
|
+
this.#maxMs = Math.max(this.#maxMs, lag);
|
|
34
|
+
if (this.#samples.length < EventLoopLagMonitor.#maxSamples) this.#samples.push(lag);
|
|
35
|
+
else this.#samples[Math.floor(Math.random() * EventLoopLagMonitor.#maxSamples)] = lag;
|
|
36
|
+
}, intervalMs);
|
|
37
|
+
|
|
38
|
+
(this.#timer as { unref?: () => void }).unref?.();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Summarize the current window and reset it. */
|
|
42
|
+
snapshotAndReset(): { meanMs: number; p99Ms: number; maxMs: number } | null {
|
|
43
|
+
if (this.#samples.length === 0) return null;
|
|
44
|
+
const sorted = [...this.#samples].sort((a, b) => a - b);
|
|
45
|
+
const sum = sorted.reduce((acc, v) => acc + v, 0);
|
|
46
|
+
const p99Index = Math.min(sorted.length - 1, Math.floor(0.99 * sorted.length));
|
|
47
|
+
const result = {
|
|
48
|
+
meanMs: round(sum / sorted.length),
|
|
49
|
+
p99Ms: round(sorted[p99Index] ?? 0),
|
|
50
|
+
maxMs: round(this.#maxMs),
|
|
51
|
+
};
|
|
52
|
+
this.#samples = [];
|
|
53
|
+
this.#maxMs = 0;
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const round = (value: number, digits = 3): number => {
|
|
59
|
+
const factor = 10 ** digits;
|
|
60
|
+
return Math.round(value * factor) / factor;
|
|
61
|
+
};
|
|
62
|
+
|
|
11
63
|
export class ProcessMetricsCollector {
|
|
12
64
|
static readonly #defaultMemoryLogIntervalMs = 60_000;
|
|
65
|
+
static readonly #lagMonitor = new EventLoopLagMonitor();
|
|
13
66
|
|
|
14
67
|
static parseMemoryLogIntervalMs(value = process.env.AKAN_MEMORY_LOG_INTERVAL_MS) {
|
|
15
68
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
16
69
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : ProcessMetricsCollector.#defaultMemoryLogIntervalMs;
|
|
17
70
|
}
|
|
18
71
|
|
|
72
|
+
/** Begin sampling event-loop lag. Idempotent; safe to call from each server role. */
|
|
73
|
+
static startEventLoopLagMonitor(intervalMs = 500): void {
|
|
74
|
+
ProcessMetricsCollector.#lagMonitor.start(intervalMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
19
77
|
static async collect(extra: AkanMetricsReport = {}): Promise<AkanMetricsReport> {
|
|
20
|
-
|
|
78
|
+
ProcessMetricsCollector.#lagMonitor.start();
|
|
79
|
+
let gcDurationMs: number | undefined;
|
|
80
|
+
if (process.env.AKAN_MEMORY_GC_ON_REPORT === "1") {
|
|
81
|
+
const gcStart = performance.now();
|
|
82
|
+
Bun.gc(true);
|
|
83
|
+
gcDurationMs = round(performance.now() - gcStart);
|
|
84
|
+
}
|
|
21
85
|
const memory = process.memoryUsage();
|
|
22
86
|
const resourceUsage = process.resourceUsage?.();
|
|
23
87
|
const jsc = await ProcessMetricsCollector.#collectJscHeapStats();
|
|
88
|
+
const lag = ProcessMetricsCollector.#lagMonitor.snapshotAndReset();
|
|
24
89
|
return {
|
|
25
90
|
pid: process.pid,
|
|
26
91
|
reportedAt: Date.now(),
|
|
@@ -45,6 +110,15 @@ export class ProcessMetricsCollector {
|
|
|
45
110
|
jscProtectedObjectCount: jsc.protectedObjectCount,
|
|
46
111
|
}
|
|
47
112
|
: {}),
|
|
113
|
+
...(lag
|
|
114
|
+
? {
|
|
115
|
+
eventLoopLagMeanMs: lag.meanMs,
|
|
116
|
+
eventLoopLagP99Ms: lag.p99Ms,
|
|
117
|
+
eventLoopLagMaxMs: lag.maxMs,
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
120
|
+
...(gcDurationMs !== undefined ? { gcDurationMs } : {}),
|
|
121
|
+
...(isTraceEnabled() ? { trace: getTraceSnapshot() } : {}),
|
|
48
122
|
...extra,
|
|
49
123
|
};
|
|
50
124
|
}
|
|
@@ -64,6 +138,10 @@ export class ProcessMetricsCollector {
|
|
|
64
138
|
...(metrics.jscHeapSizeBytes !== undefined
|
|
65
139
|
? [`jscHeap=${ProcessMetricsCollector.formatBytes(metrics.jscHeapSizeBytes)}`]
|
|
66
140
|
: []),
|
|
141
|
+
...(metrics.eventLoopLagMeanMs !== undefined
|
|
142
|
+
? [`elLag=${metrics.eventLoopLagMeanMs}/${metrics.eventLoopLagP99Ms ?? 0}/${metrics.eventLoopLagMaxMs ?? 0}ms`]
|
|
143
|
+
: []),
|
|
144
|
+
...(metrics.gcDurationMs !== undefined ? [`gc=${metrics.gcDurationMs}ms`] : []),
|
|
67
145
|
].join(" ");
|
|
68
146
|
}
|
|
69
147
|
|
|
@@ -1,22 +1,39 @@
|
|
|
1
|
-
import { match as matchLocale } from "@formatjs/intl-localematcher";
|
|
2
1
|
import { parseAkanI18nEnv } from "akanjs/common";
|
|
3
|
-
import Negotiator from "negotiator";
|
|
4
2
|
import { AkanResponse } from "./akanResponse";
|
|
5
3
|
import type { WebProxy } from "./types";
|
|
6
4
|
|
|
7
5
|
function getLocale(request: Bun.BunRequest): string {
|
|
8
6
|
const i18n = parseAkanI18nEnv();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
const acceptLanguage = request.headers.get("accept-language");
|
|
8
|
+
if (!acceptLanguage) return i18n.defaultLocale;
|
|
9
|
+
return matchAcceptedLocale(acceptLanguage, i18n.locales, i18n.defaultLocale);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function matchAcceptedLocale(acceptLanguage: string, locales: string[], defaultLocale: string): string {
|
|
13
|
+
const localeByLower = new Map(locales.map((locale) => [locale.toLowerCase(), locale]));
|
|
14
|
+
const acceptedLanguages = acceptLanguage
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((part, index) => {
|
|
17
|
+
const [tag = "", ...params] = part.trim().split(";");
|
|
18
|
+
const quality = params
|
|
19
|
+
.map((param) => param.trim())
|
|
20
|
+
.find((param) => param.startsWith("q="))
|
|
21
|
+
?.slice(2);
|
|
22
|
+
const q = quality === undefined ? 1 : Number(quality);
|
|
23
|
+
return { tag: tag.trim().toLowerCase(), q: Number.isFinite(q) ? q : 0, index };
|
|
24
|
+
})
|
|
25
|
+
.filter(({ tag, q }) => tag.length > 0 && q > 0)
|
|
26
|
+
.sort((a, b) => b.q - a.q || a.index - b.index);
|
|
27
|
+
|
|
28
|
+
for (const { tag } of acceptedLanguages) {
|
|
29
|
+
if (tag === "*") return defaultLocale;
|
|
30
|
+
const exact = localeByLower.get(tag);
|
|
31
|
+
if (exact) return exact;
|
|
32
|
+
const [language] = tag.split("-");
|
|
33
|
+
const baseMatch = locales.find((locale) => locale.toLowerCase().split("-")[0] === language);
|
|
34
|
+
if (baseMatch) return baseMatch;
|
|
19
35
|
}
|
|
36
|
+
return defaultLocale;
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
export class LocaleWebProxy implements WebProxy {
|
|
@@ -27,6 +27,22 @@ import {
|
|
|
27
27
|
DatabaseAdaptorRole,
|
|
28
28
|
type DocumentStore,
|
|
29
29
|
} from "akanjs/service";
|
|
30
|
+
import { getCurrentTrace, traceDataLoaderBatch } from "akanjs/signal";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Times a store query and records it against the active request trace (no-op when
|
|
34
|
+
* tracing is disabled). Used to surface "queries per request" and DB latency share.
|
|
35
|
+
*/
|
|
36
|
+
const timedQuery = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
37
|
+
const trace = getCurrentTrace();
|
|
38
|
+
if (!trace) return await fn();
|
|
39
|
+
const start = performance.now();
|
|
40
|
+
try {
|
|
41
|
+
return await fn();
|
|
42
|
+
} finally {
|
|
43
|
+
trace.countDbQuery(performance.now() - start);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
30
46
|
|
|
31
47
|
export class DatabaseResolver {
|
|
32
48
|
static resolveDatabase(constant: ConstantModel, database: DatabaseModel): AdaptorCls<DatabaseInstance> {
|
|
@@ -55,6 +71,18 @@ export class DatabaseResolver {
|
|
|
55
71
|
const schema = new DocumentSchema();
|
|
56
72
|
database.model._onSchema(schema as any);
|
|
57
73
|
database.model._libsOnSchema(schema as any);
|
|
74
|
+
const filterMeta = getFilterMeta(database.filter);
|
|
75
|
+
const indexedSortFieldKeys = new Set<string>();
|
|
76
|
+
for (const sort of Object.values(filterMeta.sort)) {
|
|
77
|
+
if (!sort || typeof sort !== "object") continue;
|
|
78
|
+
const sortFields = Object.entries(sort as Record<string, 1 | -1>);
|
|
79
|
+
if (!sortFields.length) continue;
|
|
80
|
+
const fields = Object.fromEntries([["removedAt", 1] as const, ...sortFields]);
|
|
81
|
+
const key = Object.keys(fields).join(",");
|
|
82
|
+
if (indexedSortFieldKeys.has(key)) continue;
|
|
83
|
+
indexedSortFieldKeys.add(key);
|
|
84
|
+
schema.index(fields);
|
|
85
|
+
}
|
|
58
86
|
|
|
59
87
|
class DatabaseModelInstance extends adapt(`${modelName}Model`, ({ plug }) => ({
|
|
60
88
|
__database: plug(DatabaseAdaptorRole, (database) => database),
|
|
@@ -71,7 +99,12 @@ export class DatabaseResolver {
|
|
|
71
99
|
await this.__store.ensure();
|
|
72
100
|
this.__model = this.#createModelFacade() as unknown as Mdl<any, any>;
|
|
73
101
|
this.__loader = new DataLoader<string, any>(
|
|
74
|
-
async (ids) =>
|
|
102
|
+
async (ids) => {
|
|
103
|
+
traceDataLoaderBatch(ids.length);
|
|
104
|
+
const docs = await timedQuery(() => this.__store.find({ id: documentQueryHelper.oneOf([...ids]) }));
|
|
105
|
+
const byId = new Map(docs.map((doc) => [String(doc.id), doc]));
|
|
106
|
+
return ids.map((id) => byId.get(String(id)) ?? null);
|
|
107
|
+
},
|
|
75
108
|
{ name: `${modelName}Loader`, cache: false },
|
|
76
109
|
);
|
|
77
110
|
Object.assign(this, {
|
|
@@ -81,18 +114,35 @@ export class DatabaseResolver {
|
|
|
81
114
|
Object.entries(getLoaderInfos(database.model)).forEach(([key, loaderInfo]) => {
|
|
82
115
|
Object.assign(this, {
|
|
83
116
|
[key]: new DataLoader<any, any>(async (keys) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
traceDataLoaderBatch(keys.length);
|
|
118
|
+
if (loaderInfo.type === "query") {
|
|
119
|
+
const fields = loaderInfo.field as string[];
|
|
120
|
+
const query = { kind: "any", queries: keys } as QueryOf<unknown>;
|
|
121
|
+
const docs = await timedQuery(() =>
|
|
122
|
+
this.__store.find(documentQueryHelper.all(loaderInfo.defaultQuery, query)),
|
|
123
|
+
);
|
|
124
|
+
const byKey = new Map(docs.map((doc) => [fields.map((field) => String(doc[field])).join(""), doc]));
|
|
125
|
+
return keys.map(
|
|
126
|
+
(queryKey) => byKey.get(fields.map((field) => String(queryKey[field])).join("")) ?? null,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const field = loaderInfo.field as string;
|
|
130
|
+
const query = {
|
|
131
|
+
[field]: documentQueryHelper.oneOf([...keys]),
|
|
132
|
+
};
|
|
133
|
+
const docs = await timedQuery(() =>
|
|
134
|
+
this.__store.find(documentQueryHelper.all(loaderInfo.defaultQuery, query)),
|
|
95
135
|
);
|
|
136
|
+
if (loaderInfo.type === "arrayField") {
|
|
137
|
+
const byKey = new Map<string, unknown>();
|
|
138
|
+
for (const doc of docs) {
|
|
139
|
+
const values = Array.isArray(doc[field]) ? doc[field] : [];
|
|
140
|
+
for (const value of values) if (!byKey.has(String(value))) byKey.set(String(value), doc);
|
|
141
|
+
}
|
|
142
|
+
return keys.map((key) => byKey.get(String(key)) ?? null);
|
|
143
|
+
}
|
|
144
|
+
const byKey = new Map(docs.map((doc) => [String(doc[field]), doc]));
|
|
145
|
+
return keys.map((key) => byKey.get(String(key)) ?? null);
|
|
96
146
|
}),
|
|
97
147
|
});
|
|
98
148
|
});
|
|
@@ -105,7 +155,7 @@ export class DatabaseResolver {
|
|
|
105
155
|
}
|
|
106
156
|
const createFindManyChain = (
|
|
107
157
|
query: QueryOf<any>,
|
|
108
|
-
options: { sort?: any; skip?: number; limit?: number } = {},
|
|
158
|
+
options: { sort?: any; skip?: number; limit?: number; select?: any } = {},
|
|
109
159
|
) => {
|
|
110
160
|
const chain: any = {
|
|
111
161
|
sort(sort: any) {
|
|
@@ -117,8 +167,8 @@ export class DatabaseResolver {
|
|
|
117
167
|
limit(limit: number) {
|
|
118
168
|
return createFindManyChain(query, { ...options, limit });
|
|
119
169
|
},
|
|
120
|
-
select() {
|
|
121
|
-
return createFindManyChain(query, options);
|
|
170
|
+
select(select?: any) {
|
|
171
|
+
return createFindManyChain(query, { ...options, select });
|
|
122
172
|
},
|
|
123
173
|
|
|
124
174
|
then(resolve: (value: any[]) => void, reject: (reason: unknown) => void) {
|
|
@@ -130,7 +180,7 @@ export class DatabaseResolver {
|
|
|
130
180
|
};
|
|
131
181
|
return chain;
|
|
132
182
|
};
|
|
133
|
-
const createFindOneChain = (query: QueryOf<any>, options: { sort?: any; skip?: number } = {}) => {
|
|
183
|
+
const createFindOneChain = (query: QueryOf<any>, options: { sort?: any; skip?: number; select?: any } = {}) => {
|
|
134
184
|
const chain: any = {
|
|
135
185
|
sort(sort: any) {
|
|
136
186
|
return createFindOneChain(query, { ...options, sort });
|
|
@@ -138,8 +188,8 @@ export class DatabaseResolver {
|
|
|
138
188
|
skip(skip: number) {
|
|
139
189
|
return createFindOneChain(query, { ...options, skip });
|
|
140
190
|
},
|
|
141
|
-
select() {
|
|
142
|
-
return createFindOneChain(query, options);
|
|
191
|
+
select(select?: any) {
|
|
192
|
+
return createFindOneChain(query, { ...options, select });
|
|
143
193
|
},
|
|
144
194
|
|
|
145
195
|
then(resolve: (value: any | null) => void, reject: (reason: unknown) => void) {
|
|
@@ -153,10 +203,13 @@ export class DatabaseResolver {
|
|
|
153
203
|
};
|
|
154
204
|
return Object.assign(Model, {
|
|
155
205
|
refName: modelName,
|
|
156
|
-
pickOne: (query: QueryOf<any>,
|
|
157
|
-
pickById: (id: string | undefined,
|
|
206
|
+
pickOne: (query: QueryOf<any>, projection?: any) => store.pickOne(query, { select: projection }),
|
|
207
|
+
pickById: (id: string | undefined, projection?: any) => {
|
|
158
208
|
if (!id) throw new Error("No Document ID");
|
|
159
|
-
return store.
|
|
209
|
+
return store.findOne({ id }, { select: projection }).then((doc) => {
|
|
210
|
+
if (!doc) throw new Error(`No Document (${modelName}): ${id}`);
|
|
211
|
+
return doc;
|
|
212
|
+
});
|
|
160
213
|
},
|
|
161
214
|
exists: async (query: QueryOf<any>) => await store.exists(query),
|
|
162
215
|
sample: (query: QueryOf<any>, size = 1) => store.find(query, { sample: size, limit: size }),
|
|
@@ -184,20 +237,20 @@ export class DatabaseResolver {
|
|
|
184
237
|
}
|
|
185
238
|
|
|
186
239
|
async __list(query?: QueryOf<any>, queryOption?: ListQueryOption): Promise<any[]> {
|
|
187
|
-
const { find, sort, skip, limit, sample } = getListQuery(query, queryOption);
|
|
188
|
-
return await this.__store.find(find, { sort, skip, limit, sample });
|
|
240
|
+
const { find, sort, skip, limit, sample, select } = getListQuery(query, queryOption);
|
|
241
|
+
return await timedQuery(() => this.__store.find(find, { sort, skip, limit, sample, select }));
|
|
189
242
|
}
|
|
190
243
|
async __listIds(query?: QueryOf<any>, queryOption?: ListQueryOption): Promise<string[]> {
|
|
191
244
|
const { find, sort, skip, limit, sample } = getListQuery(query, queryOption);
|
|
192
|
-
return await this.__store.findIds(find, { sort, skip, limit, sample });
|
|
245
|
+
return await timedQuery(() => this.__store.findIds(find, { sort, skip, limit, sample }));
|
|
193
246
|
}
|
|
194
247
|
async __find(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<any | null> {
|
|
195
248
|
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
196
|
-
return await this.__store.findOne(find, { sort, skip, sample });
|
|
249
|
+
return await timedQuery(() => this.__store.findOne(find, { sort, skip, sample }));
|
|
197
250
|
}
|
|
198
251
|
async __findId(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<string | null> {
|
|
199
252
|
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
200
|
-
return await this.__store.findId(find, { sort, skip, sample });
|
|
253
|
+
return await timedQuery(() => this.__store.findId(find, { sort, skip, sample }));
|
|
201
254
|
}
|
|
202
255
|
async __pick(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<any> {
|
|
203
256
|
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
@@ -213,7 +266,7 @@ export class DatabaseResolver {
|
|
|
213
266
|
return await this.__store.exists(query);
|
|
214
267
|
}
|
|
215
268
|
async __count(query?: QueryOf<any>): Promise<number> {
|
|
216
|
-
return await this.__store.count(query);
|
|
269
|
+
return await timedQuery(() => this.__store.count(query));
|
|
217
270
|
}
|
|
218
271
|
async __insight(query?: QueryOf<any>): Promise<any> {
|
|
219
272
|
return await this.__store.insight(query);
|
|
@@ -254,13 +307,13 @@ export class DatabaseResolver {
|
|
|
254
307
|
return await this.__store.clone(data);
|
|
255
308
|
}
|
|
256
309
|
async __create(data: DataInputOf<any, any>) {
|
|
257
|
-
return await this.__store.create(data);
|
|
310
|
+
return await timedQuery(() => this.__store.create(data));
|
|
258
311
|
}
|
|
259
312
|
async [`create${className}`](data: DataInputOf<any, any>) {
|
|
260
313
|
return this.__create(data);
|
|
261
314
|
}
|
|
262
315
|
async __update(id: string, data: DataInputOf<any, any>) {
|
|
263
|
-
return await this.__store.update(id, data);
|
|
316
|
+
return await timedQuery(() => this.__store.update(id, data));
|
|
264
317
|
}
|
|
265
318
|
async [`update${className}`](id: string, data: DataInputOf<any, any>) {
|
|
266
319
|
return this.__update(id, data);
|
|
@@ -289,8 +342,6 @@ export class DatabaseResolver {
|
|
|
289
342
|
const queryOption = hasQueryOption ? lastArg : {};
|
|
290
343
|
return { query, queryOption };
|
|
291
344
|
};
|
|
292
|
-
const filterMeta = getFilterMeta(database.filter);
|
|
293
|
-
|
|
294
345
|
Object.entries(filterMeta.query).forEach(([queryKey, filterInfo]) => {
|
|
295
346
|
const queryFn = filterInfo.queryFn;
|
|
296
347
|
if (!queryFn) throw new Error(`No query function for key: ${queryKey}`);
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type BaseEnv,
|
|
3
|
+
type Cls,
|
|
4
|
+
ENDPOINT_META,
|
|
5
|
+
FIELD_META,
|
|
6
|
+
getEnv,
|
|
7
|
+
ID,
|
|
8
|
+
INTERNAL_META,
|
|
9
|
+
Int,
|
|
10
|
+
PrimitiveRegistry,
|
|
11
|
+
SLICE_META,
|
|
12
|
+
} from "akanjs/base";
|
|
2
13
|
import { capitalize, Logger } from "akanjs/common";
|
|
3
14
|
import { serialize } from "akanjs/constant";
|
|
4
15
|
import { documentQueryHelper } from "akanjs/document";
|
|
@@ -169,7 +180,12 @@ export class SignalResolver {
|
|
|
169
180
|
const sort = requestArgs[argLength + 2] ?? "latest";
|
|
170
181
|
const internalArgs = requestArgs.slice(argLength + 3);
|
|
171
182
|
const query = await sliceInfo.execFn?.apply(this, [...args, ...internalArgs, documentQueryHelper]);
|
|
172
|
-
return (await this[serviceName].__list(query, {
|
|
183
|
+
return (await this[serviceName].__list(query, {
|
|
184
|
+
skip,
|
|
185
|
+
limit,
|
|
186
|
+
sort,
|
|
187
|
+
select: SignalResolver.#selectForConstant(sliceInfo.light),
|
|
188
|
+
})) as any;
|
|
173
189
|
});
|
|
174
190
|
|
|
175
191
|
const insightKey = `${refName}Insight${capitalizedKey}`;
|
|
@@ -249,37 +265,37 @@ export class SignalResolver {
|
|
|
249
265
|
if (endpointInfo.signalOption.globalPrefix !== undefined) {
|
|
250
266
|
routeOptions[path] = { globalPrefix: endpointInfo.signalOption.globalPrefix };
|
|
251
267
|
}
|
|
268
|
+
const normalHttpHandler = async (req: Bun.BunRequest) =>
|
|
269
|
+
await SignalContext.try(endpoint, endpointInfo, key, async () => {
|
|
270
|
+
const context = await new SignalContext(key, req, {
|
|
271
|
+
endpointInfo,
|
|
272
|
+
adaptor: endpoint,
|
|
273
|
+
registry,
|
|
274
|
+
env,
|
|
275
|
+
live,
|
|
276
|
+
middleware,
|
|
277
|
+
}).init();
|
|
278
|
+
return await context.exec();
|
|
279
|
+
});
|
|
252
280
|
switch (endpointInfo.type) {
|
|
253
281
|
case "query":
|
|
254
|
-
routes[path] =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
endpointInfo,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
};
|
|
282
|
+
routes[path] = SignalResolver.#canUsePrimitiveQueryFastPath(endpointInfo, middleware)
|
|
283
|
+
? {
|
|
284
|
+
GET: async (req) => {
|
|
285
|
+
if (SignalResolver.#hasAuthCredential(req)) return await normalHttpHandler(req);
|
|
286
|
+
return await SignalContext.try(endpoint, endpointInfo, key, async () => {
|
|
287
|
+
const result = await endpointInfo.execFn?.call(endpoint);
|
|
288
|
+
return result instanceof Response ? result : Response.json(result);
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
: {
|
|
293
|
+
GET: normalHttpHandler,
|
|
294
|
+
};
|
|
268
295
|
break;
|
|
269
296
|
case "mutation":
|
|
270
297
|
routes[path] = {
|
|
271
|
-
POST:
|
|
272
|
-
await SignalContext.try(endpoint, endpointInfo, key, async () => {
|
|
273
|
-
const context = await new SignalContext(key, req, {
|
|
274
|
-
endpointInfo,
|
|
275
|
-
adaptor: endpoint,
|
|
276
|
-
registry,
|
|
277
|
-
env,
|
|
278
|
-
live,
|
|
279
|
-
middleware,
|
|
280
|
-
}).init();
|
|
281
|
-
return await context.exec();
|
|
282
|
-
}),
|
|
298
|
+
POST: normalHttpHandler,
|
|
283
299
|
};
|
|
284
300
|
break;
|
|
285
301
|
case "pubsub":
|
|
@@ -349,6 +365,29 @@ export class SignalResolver {
|
|
|
349
365
|
return trimmed ? `/${trimmed}` : "";
|
|
350
366
|
}
|
|
351
367
|
|
|
368
|
+
static #selectForConstant(constant: Cls): Record<string, true> | undefined {
|
|
369
|
+
const fields = (constant as { [FIELD_META]?: Record<string, unknown> })[FIELD_META];
|
|
370
|
+
if (!fields) return undefined;
|
|
371
|
+
return Object.fromEntries(Object.keys(fields).map((field) => [field, true]));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
static #canUsePrimitiveQueryFastPath(endpointInfo: EndpointInfo, middleware: Map<string, MiddlewareCls>) {
|
|
375
|
+
return (
|
|
376
|
+
process.env.AKAN_TRACE !== "1" &&
|
|
377
|
+
endpointInfo.args.length === 0 &&
|
|
378
|
+
endpointInfo.internalArgs.length === 0 &&
|
|
379
|
+
(endpointInfo.signalOption.guards?.length ?? 0) === 0 &&
|
|
380
|
+
(endpointInfo.signalOption.middlewares?.length ?? 0) === 0 &&
|
|
381
|
+
[...middleware.values()].every((MiddlewareCls) => MiddlewareCls.refName === "AccountMiddleware") &&
|
|
382
|
+
endpointInfo.returns.arrDepth === 0 &&
|
|
383
|
+
PrimitiveRegistry.has(endpointInfo.returns.returnRef as Cls)
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
static #hasAuthCredential(req: Request) {
|
|
388
|
+
return Boolean(req.headers.get("authorization") || req.headers.get("cookie")?.includes("jwt="));
|
|
389
|
+
}
|
|
390
|
+
|
|
352
391
|
static async handleWsOpen(ws: Bun.ServerWebSocket<any>, registry: InjectRegistry) {
|
|
353
392
|
await SignalResolver.#getWebsocket(registry).registerSocket(ws);
|
|
354
393
|
}
|
package/service/ipcTypes.ts
CHANGED
|
@@ -94,6 +94,11 @@ export interface AkanMetricsReport {
|
|
|
94
94
|
httpHtmlCacheHits?: number;
|
|
95
95
|
httpHtmlCacheMisses?: number;
|
|
96
96
|
httpHtmlCacheBypass?: number;
|
|
97
|
+
eventLoopLagMeanMs?: number;
|
|
98
|
+
eventLoopLagP99Ms?: number;
|
|
99
|
+
eventLoopLagMaxMs?: number;
|
|
100
|
+
gcDurationMs?: number;
|
|
101
|
+
trace?: unknown;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
export type AkanIpcMessage =
|