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.
Files changed (81) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/cli/application/application.command.ts +4 -1
  4. package/cli/application/application.runner.ts +6 -8
  5. package/cli/build.ts +3 -1
  6. package/cli/cloud/cloud.runner.ts +7 -8
  7. package/cli/index.js +288 -115
  8. package/cli/library/library.runner.ts +2 -2
  9. package/cli/module/module.runner.ts +2 -2
  10. package/cli/npmRegistry.ts +13 -0
  11. package/cli/openBrowser.ts +15 -0
  12. package/cli/pluralizeName.ts +5 -0
  13. package/cli/scalar/scalar.prompt.ts +2 -2
  14. package/cli/scalar/scalar.runner.ts +2 -2
  15. package/cli/semver.ts +18 -0
  16. package/cli/templates/lib/sig.ts +2 -2
  17. package/cli/workspace/workspace.runner.ts +3 -3
  18. package/client/cookie.ts +10 -15
  19. package/common/index.ts +1 -0
  20. package/common/jwtDecode.ts +17 -0
  21. package/constant/serialize.ts +1 -1
  22. package/devkit/akanApp/akanApp.host.ts +46 -9
  23. package/devkit/akanConfig/akanConfig.ts +2 -1
  24. package/devkit/capacitor.base.config.ts +18 -4
  25. package/devkit/capacitorApp.ts +118 -64
  26. package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
  27. package/devkit/mobile/mobileTarget.ts +2 -1
  28. package/devkit/scanInfo.ts +1 -0
  29. package/document/dataLoader.ts +140 -6
  30. package/document/database.ts +1 -1
  31. package/package.json +7 -13
  32. package/server/akanApp.ts +250 -44
  33. package/server/di/diLifecycle.ts +1 -1
  34. package/server/processMetricsCollector.ts +79 -1
  35. package/server/proxy/localeWebProxy.ts +29 -12
  36. package/server/resolver/database.resolver.ts +82 -31
  37. package/server/resolver/signal.resolver.ts +67 -28
  38. package/service/ipcTypes.ts +5 -0
  39. package/service/predefinedAdaptor/database.adaptor.ts +95 -27
  40. package/service/predefinedAdaptor/solidSqlite.ts +7 -7
  41. package/service/predefinedAdaptor/storage.adaptor.ts +35 -9
  42. package/service/serviceModule.ts +1 -6
  43. package/signal/base.signal.ts +1 -1
  44. package/signal/index.ts +1 -0
  45. package/signal/middleware.ts +5 -1
  46. package/signal/signalContext.ts +85 -31
  47. package/signal/signalRegistry.ts +35 -10
  48. package/signal/trace.ts +279 -0
  49. package/types/cli/npmRegistry.d.ts +1 -0
  50. package/types/cli/openBrowser.d.ts +1 -0
  51. package/types/cli/pluralizeName.d.ts +1 -0
  52. package/types/cli/semver.d.ts +1 -0
  53. package/types/client/cookie.d.ts +6 -1
  54. package/types/common/index.d.ts +1 -0
  55. package/types/common/jwtDecode.d.ts +2 -0
  56. package/types/devkit/capacitorApp.d.ts +14 -5
  57. package/types/devkit/incrementalBuilder/incrementalBuilder.host.d.ts +9 -5
  58. package/types/document/dataLoader.d.ts +21 -2
  59. package/types/document/database.d.ts +1 -1
  60. package/types/server/processMetricsCollector.d.ts +2 -0
  61. package/types/service/ipcTypes.d.ts +5 -0
  62. package/types/service/predefinedAdaptor/database.adaptor.d.ts +26 -32
  63. package/types/service/predefinedAdaptor/solidSqlite.d.ts +3 -3
  64. package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -2
  65. package/types/service/serviceModule.d.ts +1 -1
  66. package/types/signal/index.d.ts +1 -0
  67. package/types/signal/signalContext.d.ts +4 -1
  68. package/types/signal/signalRegistry.d.ts +25 -4
  69. package/types/signal/trace.d.ts +97 -0
  70. package/types/ui/Signal/style.d.ts +15 -0
  71. package/ui/Signal/Arg.tsx +22 -15
  72. package/ui/Signal/Doc.tsx +30 -24
  73. package/ui/Signal/Listener.tsx +15 -39
  74. package/ui/Signal/Message.tsx +32 -50
  75. package/ui/Signal/Object.tsx +16 -13
  76. package/ui/Signal/PubSub.tsx +29 -47
  77. package/ui/Signal/Response.tsx +7 -17
  78. package/ui/Signal/RestApi.tsx +41 -57
  79. package/ui/Signal/WebSocket.tsx +1 -1
  80. package/ui/Signal/style.ts +36 -0
  81. 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
- if (process.env.AKAN_MEMORY_GC_ON_REPORT === "1") Bun.gc(true);
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
- if (!request.headers.get("accept-language")) return i18n.defaultLocale;
10
- const negotiatorHeaders: Record<string, string> = {};
11
- request.headers.forEach((value, key) => {
12
- negotiatorHeaders[key] = value;
13
- });
14
- try {
15
- const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
16
- return matchLocale(languages, i18n.locales, i18n.defaultLocale);
17
- } catch {
18
- return i18n.defaultLocale;
7
+ const acceptLanguage = request.headers.get("accept-language");
8
+ if (!acceptLanguage) return i18n.defaultLocale;
9
+ return matchAcceptedLocale(acceptLanguage, i18n.locales, i18n.defaultLocale);
10
+ }
11
+
12
+ function matchAcceptedLocale(acceptLanguage: string, locales: string[], defaultLocale: string): string {
13
+ const localeByLower = new Map(locales.map((locale) => [locale.toLowerCase(), locale]));
14
+ const acceptedLanguages = acceptLanguage
15
+ .split(",")
16
+ .map((part, index) => {
17
+ const [tag = "", ...params] = part.trim().split(";");
18
+ const quality = params
19
+ .map((param) => param.trim())
20
+ .find((param) => param.startsWith("q="))
21
+ ?.slice(2);
22
+ const q = quality === undefined ? 1 : Number(quality);
23
+ return { tag: tag.trim().toLowerCase(), q: Number.isFinite(q) ? q : 0, index };
24
+ })
25
+ .filter(({ tag, q }) => tag.length > 0 && q > 0)
26
+ .sort((a, b) => b.q - a.q || a.index - b.index);
27
+
28
+ for (const { tag } of acceptedLanguages) {
29
+ if (tag === "*") return defaultLocale;
30
+ const exact = localeByLower.get(tag);
31
+ if (exact) return exact;
32
+ const [language] = tag.split("-");
33
+ const baseMatch = locales.find((locale) => locale.toLowerCase().split("-")[0] === language);
34
+ if (baseMatch) return baseMatch;
19
35
  }
36
+ return defaultLocale;
20
37
  }
21
38
 
22
39
  export class LocaleWebProxy implements WebProxy {
@@ -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) => await Promise.all(ids.map((id) => this.__store.findOne({ id }))),
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
- return await Promise.all(
85
- keys.map(async (key) => {
86
- const query =
87
- loaderInfo.type === "query"
88
- ? key
89
- : {
90
- [loaderInfo.field as string]:
91
- loaderInfo.type === "arrayField" ? documentQueryHelper.has(key) : key,
92
- };
93
- return await this.__store.findOne(documentQueryHelper.all(loaderInfo.defaultQuery, query));
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>, _projection?: any) => store.pickOne(query),
157
- pickById: (id: string | undefined, _projection?: any) => {
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.pickById(id);
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 { type BaseEnv, type Cls, ENDPOINT_META, getEnv, ID, INTERNAL_META, Int, SLICE_META } from "akanjs/base";
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, { skip, limit, sort })) as any;
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
- GET: async (req) =>
256
- await SignalContext.try(endpoint, endpointInfo, key, async () => {
257
- const context = await new SignalContext(key, req, {
258
- endpointInfo,
259
- adaptor: endpoint,
260
- registry,
261
- env,
262
- live,
263
- middleware,
264
- }).init();
265
- return await context.exec();
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: async (req) =>
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
  }
@@ -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 =