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
@@ -74,23 +74,17 @@ export interface DocumentStore {
74
74
  bulkWrite(
75
75
  operations: { updateOne: { filter: DocumentQuery; update: DocumentUpdate; upsert?: boolean } }[],
76
76
  ): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: string | null }>;
77
- find(
78
- query?: DocumentQuery,
79
- options?: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number },
80
- ): Promise<any[]>;
77
+ find(query?: DocumentQuery, options?: FindManyOptions): Promise<any[]>;
81
78
  findIds(
82
79
  query?: DocumentQuery,
83
80
  options?: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number },
84
81
  ): Promise<string[]>;
85
- findOne(
86
- query?: DocumentQuery,
87
- options?: { sort?: SortOption; skip?: number | null; sample?: boolean },
88
- ): Promise<any | null>;
82
+ findOne(query?: DocumentQuery, options?: FindOneOptions): Promise<any | null>;
89
83
  findId(
90
84
  query?: DocumentQuery,
91
85
  options?: { sort?: SortOption; skip?: number | null; sample?: boolean },
92
86
  ): Promise<string | null>;
93
- pickOne(query?: DocumentQuery, options?: { sort?: SortOption; skip?: number | null; sample?: boolean }): Promise<any>;
87
+ pickOne(query?: DocumentQuery, options?: FindOneOptions): Promise<any>;
94
88
  pickById(id: string): Promise<any>;
95
89
  exists(query?: DocumentQuery): Promise<string | null>;
96
90
  count(query?: DocumentQuery): Promise<number>;
@@ -137,6 +131,15 @@ type DocumentRecord = Record<string, unknown>;
137
131
  type MutableDocumentRecord = Record<string, unknown>;
138
132
  type FieldMap = Record<string, { getProps: () => Record<string, unknown>; [key: string]: unknown }>;
139
133
  type SortOption = Record<string, 1 | -1> | null | undefined;
134
+ type ProjectionOption = Partial<Record<string, boolean>> | null | undefined;
135
+ type FindManyOptions = {
136
+ sort?: SortOption;
137
+ skip?: number | null;
138
+ limit?: number | null;
139
+ sample?: number;
140
+ select?: ProjectionOption;
141
+ };
142
+ type FindOneOptions = { sort?: SortOption; skip?: number | null; sample?: boolean; select?: ProjectionOption };
140
143
  type QueryOperatorName = Exclude<
141
144
  DocumentQueryNode,
142
145
  { kind: "all" } | { kind: "any" } | { kind: "not" } | { kind: "raw" }
@@ -148,6 +151,7 @@ interface SqliteDocumentRow {
148
151
  removedAt?: number | string | null;
149
152
  _doc: string;
150
153
  }
154
+ type ProjectedSqliteDocumentRow = Omit<SqliteDocumentRow, "_doc"> & Record<string, unknown>;
151
155
 
152
156
  interface DocumentDatabaseOwner {
153
157
  getConnection(): AkanSqlClient;
@@ -510,6 +514,7 @@ export class SqliteDocumentStore {
510
514
  readonly table: string;
511
515
  readonly compiler: QueryCompiler;
512
516
  #insertStmt: AkanSqlStatement | null = null;
517
+ #readStmtCache = new Map<string, AkanSqlStatement>();
513
518
 
514
519
  constructor(
515
520
  private readonly owner: DocumentDatabaseOwner,
@@ -637,20 +642,23 @@ export class SqliteDocumentStore {
637
642
  return { acknowledged: true, matchedCount, modifiedCount, upsertedId };
638
643
  }
639
644
 
640
- async find(
641
- query?: DocumentQuery,
642
- options: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number } = {},
643
- ) {
645
+ async find(query?: DocumentQuery, options: FindManyOptions = {}) {
644
646
  const { where, params } = this.safeQuery(query);
645
647
  const limitValue = Number(options.limit ?? 0);
646
648
  const skipValue = Number(options.skip ?? 0);
647
649
  const limit = limitValue ? ` LIMIT ${limitValue}` : "";
648
650
  const offset = skipValue ? ` OFFSET ${skipValue}` : "";
649
651
  const order = options.sample ? "ORDER BY random()" : `ORDER BY ${this.compiler.orderBy(options.sort ?? undefined)}`;
650
- const rows = await this.owner
651
- .getConnection()
652
- .prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`)
653
- .all<SqliteDocumentRow>(...params);
652
+ const projection = this.normalizeProjection(options.select);
653
+ if (projection) {
654
+ const rows = await this.prepareReadStmt(
655
+ `SELECT ${this.projectionSql(projection)} FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
656
+ ).all<ProjectedSqliteDocumentRow>(...params);
657
+ return rows.map((row) => this.fromProjectedRow(row, projection));
658
+ }
659
+ const rows = await this.prepareReadStmt(
660
+ `SELECT * FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
661
+ ).all<SqliteDocumentRow>(...params);
654
662
  return rows.map((row) => this.hydrate(this.fromRow(row)));
655
663
  }
656
664
 
@@ -664,14 +672,13 @@ export class SqliteDocumentStore {
664
672
  const limit = limitValue ? ` LIMIT ${limitValue}` : "";
665
673
  const offset = skipValue ? ` OFFSET ${skipValue}` : "";
666
674
  const order = options.sample ? "ORDER BY random()" : `ORDER BY ${this.compiler.orderBy(options.sort ?? undefined)}`;
667
- const rows = await this.owner
668
- .getConnection()
669
- .prepare(`SELECT "id" FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`)
670
- .all<{ id: string }>(...params);
675
+ const rows = await this.prepareReadStmt(
676
+ `SELECT "id" FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
677
+ ).all<{ id: string }>(...params);
671
678
  return rows.map((row) => row.id);
672
679
  }
673
680
 
674
- async findOne(query?: DocumentQuery, options: { sort?: SortOption; skip?: number | null; sample?: boolean } = {}) {
681
+ async findOne(query?: DocumentQuery, options: FindOneOptions = {}) {
675
682
  return (await this.find(query, { ...options, limit: 1, sample: options.sample ? 1 : undefined })).at(0) ?? null;
676
683
  }
677
684
 
@@ -679,7 +686,7 @@ export class SqliteDocumentStore {
679
686
  return (await this.findIds(query, { ...options, limit: 1, sample: options.sample ? 1 : undefined })).at(0) ?? null;
680
687
  }
681
688
 
682
- async pickOne(query?: DocumentQuery, options: { sort?: SortOption; skip?: number | null; sample?: boolean } = {}) {
689
+ async pickOne(query?: DocumentQuery, options: FindOneOptions = {}) {
683
690
  const doc = await this.findOne(query, options);
684
691
  if (!doc) throw new Error(`No Document (${this.table}): ${JSON.stringify(query)}`);
685
692
  return doc;
@@ -697,10 +704,9 @@ export class SqliteDocumentStore {
697
704
 
698
705
  async count(query?: DocumentQuery) {
699
706
  const { where, params } = this.safeQuery(query);
700
- const row = await this.owner
701
- .getConnection()
702
- .prepare(`SELECT count(*) as count FROM ${quoteIdent(this.table)} WHERE ${where}`)
703
- .get<{ count: number }>(...params);
707
+ const row = await this.prepareReadStmt(
708
+ `SELECT count(*) as count FROM ${quoteIdent(this.table)} WHERE ${where}`,
709
+ ).get<{ count: number }>(...params);
704
710
  return row?.count ?? 0;
705
711
  }
706
712
 
@@ -901,6 +907,55 @@ export class SqliteDocumentStore {
901
907
  };
902
908
  }
903
909
 
910
+ private normalizeProjection(select: ProjectionOption): string[] | null {
911
+ if (!select) return null;
912
+ const fields = Object.entries(select)
913
+ .filter(([, included]) => included)
914
+ .map(([field]) => field);
915
+ if (!fields.length) return null;
916
+ return [...new Set(fields.filter((field) => field !== "_doc"))];
917
+ }
918
+
919
+ private projectionSql(fields: string[]) {
920
+ const jsonFields = fields.filter((field) => !BASE_COLUMNS.has(field));
921
+ const baseColumns = [...BASE_COLUMNS].map((field) => quoteIdent(field));
922
+ const jsonColumns = jsonFields.map(
923
+ (field, idx) => `${this.compiler.fieldExpr(field)} AS ${quoteIdent(this.projectionAlias(idx))}`,
924
+ );
925
+ return [...baseColumns, ...jsonColumns].join(", ");
926
+ }
927
+
928
+ private projectionAlias(idx: number) {
929
+ return `__akan_projection_${idx}`;
930
+ }
931
+
932
+ private fromProjectedRow(row: ProjectedSqliteDocumentRow, fields: string[]) {
933
+ const doc: DocumentRecord = {
934
+ id: row.id,
935
+ createdAt: Number(row.createdAt),
936
+ updatedAt: Number(row.updatedAt),
937
+ removedAt: row.removedAt ? Number(row.removedAt) : undefined,
938
+ };
939
+ const jsonFields = fields.filter((field) => !BASE_COLUMNS.has(field));
940
+ for (const [idx, field] of jsonFields.entries()) {
941
+ const value = this.parseProjectedValue(row[this.projectionAlias(idx)]);
942
+ const props = (this.database.doc[FIELD_META] as unknown as FieldMap)[field]?.getProps?.();
943
+ doc[field] = props ? this.decodeFieldValue(value, props) : value;
944
+ }
945
+ return doc;
946
+ }
947
+
948
+ private parseProjectedValue(value: unknown) {
949
+ if (typeof value !== "string") return value;
950
+ const trimmed = value.trim();
951
+ if (!trimmed || (trimmed[0] !== "{" && trimmed[0] !== "[")) return value;
952
+ try {
953
+ return JSON.parse(trimmed);
954
+ } catch {
955
+ return value;
956
+ }
957
+ }
958
+
904
959
  private decodeDocumentPayload(payload: Record<string, unknown>) {
905
960
  const fields = this.database.doc[FIELD_META] as unknown as FieldMap;
906
961
  return Object.fromEntries(
@@ -1011,6 +1066,19 @@ export class SqliteDocumentStore {
1011
1066
  return this.#insertStmt;
1012
1067
  }
1013
1068
 
1069
+ private prepareReadStmt(sql: string) {
1070
+ const cached = this.#readStmtCache.get(sql);
1071
+ if (cached) return cached;
1072
+
1073
+ if (this.#readStmtCache.size >= 128) {
1074
+ const oldest = this.#readStmtCache.keys().next().value;
1075
+ if (oldest) this.#readStmtCache.delete(oldest);
1076
+ }
1077
+ const stmt = this.owner.getConnection().prepare(sql);
1078
+ this.#readStmtCache.set(sql, stmt);
1079
+ return stmt;
1080
+ }
1081
+
1014
1082
  private assertValidRefName(refName: string) {
1015
1083
  if (!REF_NAME_RE.test(refName) || RESERVED_RE.test(refName))
1016
1084
  throw new Error(`Invalid database identifier: ${refName}`);
@@ -19,7 +19,7 @@ export interface SolidEnv extends BaseEnv {
19
19
  solid?: SolidConfig;
20
20
  }
21
21
 
22
- export type SolidValueType = "string" | "number" | "buffer";
22
+ export type SolidValueType = "string" | "number" | "buffer" | "json";
23
23
 
24
24
  const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
25
25
 
@@ -62,19 +62,19 @@ export const openSolidDatabase = async (config: Required<SolidConfig>) => {
62
62
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
63
63
  };
64
64
 
65
- export const encodeSolidValue = (value: string | number | Buffer): { type: SolidValueType; value: string | Buffer } => {
65
+ export const encodeSolidValue = (value: unknown): { type: SolidValueType; value: string | Buffer } => {
66
66
  if (Buffer.isBuffer(value)) return { type: "buffer", value };
67
67
  if (typeof value === "number") return { type: "number", value: String(value) };
68
- return { type: "string", value };
68
+ if (typeof value === "string") return { type: "string", value };
69
+
70
+ return { type: "json", value: JSON.stringify(value ?? null) };
69
71
  };
70
72
 
71
- export const decodeSolidValue = <T extends string | number | Buffer>(
72
- type: SolidValueType,
73
- value: string | Buffer | null,
74
- ): T | undefined => {
73
+ export const decodeSolidValue = <T>(type: SolidValueType, value: string | Buffer | null): T | undefined => {
75
74
  if (value === null) return undefined;
76
75
  if (type === "buffer") return Buffer.isBuffer(value) ? (value as T) : (Buffer.from(value) as T);
77
76
  if (type === "number") return Number(value) as T;
77
+ if (type === "json") return JSON.parse(typeof value === "string" ? value : value.toString()) as T;
78
78
  return String(value) as T;
79
79
  };
80
80
 
@@ -16,6 +16,7 @@ export interface UploadRequest {
16
16
  meta?: { [key: string]: string };
17
17
  rename?: string;
18
18
  host?: string;
19
+ access?: "public" | "private";
19
20
  }
20
21
  export interface CopyRequest {
21
22
  bucket: string;
@@ -29,6 +30,7 @@ export interface UploadFromStreamRequest {
29
30
  body: ReadableStream;
30
31
  mimetype: string;
31
32
  root?: string;
33
+ access?: "public" | "private";
32
34
  updateProgress: (progress: { loaded?: number; total?: number; part?: number }) => void;
33
35
  uploadSuccess: (url: string) => void;
34
36
  }
@@ -47,17 +49,22 @@ export interface StorageAdaptor {
47
49
  saveData(request: DownloadRequest): Promise<LocalFilePath>;
48
50
  copyData(request: CopyRequest): Promise<string>;
49
51
  deleteData(url: string): Promise<boolean>;
52
+ deleteDataByPath(path: string): Promise<boolean>;
50
53
  }
51
54
 
52
55
  export interface BlobStorageOptions extends BaseEnv {
53
- blobStorage?: { baseDir?: string; urlPrefix?: string };
56
+ blobStorage?: { baseDir?: string; privateBaseDir?: string; urlPrefix?: string };
54
57
  }
55
58
 
56
59
  export class BlobStorage
57
60
  extends adapt("blobStorage", ({ env }) => ({
58
61
  root: env(
59
62
  ({ appName, blobStorage = { baseDir: "local", urlPrefix: "/backend/localFile/getBlob" } }: BlobStorageOptions) =>
60
- `${process.env.AKAN_WORKSPACE_ROOT ?? "."}/${blobStorage.baseDir}/${appName}/backend`,
63
+ `${process.env.AKAN_WORKSPACE_ROOT ?? "."}/${blobStorage.baseDir ?? "local"}/${appName}/backend`,
64
+ ),
65
+ privateRoot: env(
66
+ ({ appName, blobStorage = { privateBaseDir: "local" } }: BlobStorageOptions) =>
67
+ `${process.env.AKAN_WORKSPACE_ROOT ?? "."}/${blobStorage.privateBaseDir ?? "local"}/${appName}/server-private`,
61
68
  ),
62
69
  urlPrefix: env(
63
70
  ({ blobStorage = { urlPrefix: "/backend/localFile/getBlob" } }: BlobStorageOptions) => blobStorage.urlPrefix,
@@ -68,12 +75,15 @@ export class BlobStorage
68
75
  #localPathToUrl(path: string) {
69
76
  return `${this.urlPrefix}/${path}`;
70
77
  }
78
+ #resolveFilePath(path: string) {
79
+ return path.startsWith("private/") ? `${this.privateRoot}/${path}` : `${this.root}/${path}`;
80
+ }
71
81
  async readData(path: string): Promise<ReadableStream> {
72
- const filePath = `${this.root}/${path}`;
82
+ const filePath = this.#resolveFilePath(path);
73
83
  return Bun.file(filePath).stream();
74
84
  }
75
85
  async readDataAsJson<T>(path: string) {
76
- const filePath = `${this.root}/${path}`;
86
+ const filePath = this.#resolveFilePath(path);
77
87
  return Bun.file(filePath).json() as T;
78
88
  }
79
89
  async getDataList(prefix?: string) {
@@ -81,14 +91,21 @@ export class BlobStorage
81
91
  const paths = Array.from(new Bun.Glob("*").scanSync({ cwd: dir, onlyFiles: false }));
82
92
  return paths.map((path) => this.#localPathToUrl(path));
83
93
  }
84
- async uploadDataFromLocal({ path, localPath, meta }: UploadRequest) {
85
- const filePath = `${this.root}/${path}`;
94
+ async uploadDataFromLocal({ path, localPath, meta, access = "public" }: UploadRequest) {
95
+ const filePath = access === "private" ? `${this.privateRoot}/${path}` : `${this.root}/${path}`;
86
96
  await Bun.write(filePath, Bun.file(localPath));
87
97
  if (meta) await Bun.write(`${filePath}.meta`, JSON.stringify(meta));
88
98
  return this.#localPathToUrl(path);
89
99
  }
90
- async uploadDataFromStream({ path, body, mimetype, updateProgress, uploadSuccess }: UploadFromStreamRequest) {
91
- const filePath = `${this.root}/${path}`;
100
+ async uploadDataFromStream({
101
+ path,
102
+ body,
103
+ mimetype,
104
+ updateProgress,
105
+ uploadSuccess,
106
+ access = "public",
107
+ }: UploadFromStreamRequest) {
108
+ const filePath = access === "private" ? `${this.privateRoot}/${path}` : `${this.root}/${path}`;
92
109
  try {
93
110
  await Bun.write(filePath, new Response(body));
94
111
  uploadSuccess(this.#localPathToUrl(path));
@@ -106,12 +123,21 @@ export class BlobStorage
106
123
  await Bun.write(`${this.root}/${pastePath}`, Bun.file(`${this.root}/${copyPath}`));
107
124
  return pastePath;
108
125
  }
126
+ async deleteDataByPath(path: string) {
127
+ try {
128
+ await Bun.file(this.#resolveFilePath(path)).delete();
129
+ return true;
130
+ } catch (error) {
131
+ this.logger.error(error instanceof Error ? error.message : "Unknown error");
132
+ return false;
133
+ }
134
+ }
109
135
  async deleteData(url: string) {
110
136
  try {
111
137
  const basePath = this.#localPathToUrl("");
112
138
  if (!url.startsWith(basePath)) throw new Error("Invalid Base URL, Unable to delete data");
113
139
  const path = url.replace(basePath, "");
114
- await Bun.file(`${this.root}/${path}`).delete();
140
+ await this.deleteDataByPath(path);
115
141
  return true;
116
142
  } catch (error) {
117
143
  this.logger.error(error instanceof Error ? error.message : "Unknown error");
@@ -45,12 +45,7 @@ export class ServiceModel<
45
45
  this.cnst,
46
46
  this.db,
47
47
  Object.assign({}, this.srvMap, ...srvs.map((srv) => srv.srvMap)),
48
- ) as unknown as ServiceModel<
49
- Srv & MergeAllKeyOfObjects<SrvModules, "srv">,
50
- CnstModel,
51
- DbModel,
52
- SrvMap & MergeAllKeyOfObjects<SrvModules, "srvMap">
53
- >;
48
+ ) as unknown as ServiceModel<Srv, CnstModel, DbModel, SrvMap & MergeAllKeyOfObjects<SrvModules, "srvMap">>;
54
49
  }
55
50
 
56
51
  static getDefaultDbServiceMethods(className: string) {
@@ -31,7 +31,7 @@ export class BaseEndpoint extends endpoint(srv.base, ({ query, mutation, message
31
31
  })) {}
32
32
 
33
33
  export class Base extends serverSignal(BaseEndpoint, BaseInternal) {}
34
- export const base = SignalRegistry.registerService(BaseInternal, BaseEndpoint, Base);
34
+ export const base = SignalRegistry.registerService("base" as const, BaseInternal, BaseEndpoint, Base);
35
35
 
36
36
  const createBaseFetch = () => FetchClient.from(base);
37
37
  type BaseFetch = ReturnType<typeof createBaseFetch>;
package/signal/index.ts CHANGED
@@ -15,4 +15,5 @@ export * from "./signalContext";
15
15
  export * from "./signalRegistry";
16
16
  export * from "./slice";
17
17
  export * from "./sliceInfo";
18
+ export * from "./trace";
18
19
  export * from "./types";
@@ -2,6 +2,7 @@ import type { BaseEnv, Cls, PromiseOrObject } from "akanjs/base";
2
2
  import { type CacheAdaptor, CacheAdaptorRole } from "akanjs/service";
3
3
  import dayjs from "dayjs";
4
4
  import type { SignalContext } from "./signalContext";
5
+ import { traceCache } from "./trace";
5
6
 
6
7
  export interface Middleware<Env extends BaseEnv = BaseEnv> {
7
8
  use(env: Env): PromiseOrObject<(context: SignalContext, next: () => Promise<unknown>) => PromiseOrObject<unknown>>;
@@ -52,12 +53,15 @@ export class Cache extends middleware("cache") {
52
53
  if (cached) {
53
54
  context.adaptor.logger.debug(`Cache hit ${context.key}`);
54
55
  try {
55
- return JSON.parse(cached);
56
+ const parsed = JSON.parse(cached);
57
+ traceCache(true);
58
+ return parsed;
56
59
  } catch (parseError) {
57
60
  context.adaptor.logger.warn(`Cache parse error ${context.key}: ${String(parseError)}`);
58
61
  await cache.delete(topic, key);
59
62
  }
60
63
  }
64
+ traceCache(false);
61
65
 
62
66
  const result = await next();
63
67
 
@@ -18,6 +18,7 @@ import type { Adaptor, AdaptorCls, DatabaseService, InjectRegistry, LiveRegistry
18
18
  import type { Internal, InternalCls, InternalInfo, MiddlewareCls } from ".";
19
19
  import type { EndpointInfo } from "./endpointInfo";
20
20
  import { Exception } from "./exception";
21
+ import { isTraceEnabled, runWithTrace, SignalTrace, traceSpan } from "./trace";
21
22
 
22
23
  export type SignalTransportType = "http" | "websocket";
23
24
 
@@ -54,6 +55,7 @@ export class SignalContext<
54
55
  adaptor: Adaptor;
55
56
  args: unknown[] = [];
56
57
  internalArgs: unknown[] = [];
58
+ trace: SignalTrace | null = null;
57
59
  #registry: InjectRegistry;
58
60
  #env: Env;
59
61
  #live: LiveRegistry;
@@ -87,6 +89,7 @@ export class SignalContext<
87
89
  this.#env = env;
88
90
  this.#live = live;
89
91
  this.#middleware = middleware;
92
+ if (isTraceEnabled()) this.trace = new SignalTrace(key, endpointInfo.type);
90
93
  }
91
94
 
92
95
  getAdaptor<T extends Adaptor>(adaptorCls: AdaptorCls<T>): T {
@@ -97,11 +100,18 @@ export class SignalContext<
97
100
  return instance as T;
98
101
  }
99
102
  async init() {
100
- this.args = await this.ctx.getArgs(this.endpointInfo);
103
+ if (this.trace) {
104
+ const start = performance.now();
105
+ this.args = await this.ctx.getArgs(this.endpointInfo);
106
+ this.trace.recordSpan("argParse", performance.now() - start);
107
+ } else {
108
+ this.args = await this.ctx.getArgs(this.endpointInfo);
109
+ }
101
110
  return this;
102
111
  }
103
112
  async #checkGuards() {
104
113
  const guards = this.endpointInfo.signalOption.guards ?? [];
114
+ if (guards.length === 0) return;
105
115
  for (const GuardCls of guards) {
106
116
  const guard = new GuardCls();
107
117
  const canPass = guard.canPass(this);
@@ -109,41 +119,72 @@ export class SignalContext<
109
119
  }
110
120
  }
111
121
  async exec() {
122
+ if (!this.trace) return await this.#exec();
123
+ return await runWithTrace(this.trace, async () => {
124
+ try {
125
+ return await this.#exec();
126
+ } finally {
127
+ this.trace?.finalize();
128
+ }
129
+ });
130
+ }
131
+ async #exec() {
112
132
  if (!this.endpointInfo.execFn) throw new Exception.Error("Exec function is not set");
113
- const middlewares = [...this.#middleware.values(), ...(this.endpointInfo.signalOption.middlewares ?? [])];
133
+ const endpointMiddlewares = this.endpointInfo.signalOption.middlewares ?? [];
114
134
  const coreExec = async () => {
115
135
  if (!this.endpointInfo.execFn) throw new Exception.Error("Exec function is not set");
116
- await this.#checkGuards();
117
- this.internalArgs = await Promise.all(
118
- this.endpointInfo.internalArgs.map((arg) => {
119
- const argValue = new arg.argRef().getArg(this) ?? null;
120
- if (argValue === null && !arg.option?.nullable)
121
- throw new Exception.Unauthorized(`Internal Argument ${arg.argRef.name} is required`);
122
- return argValue;
123
- }),
136
+ if (this.trace) await traceSpan("guards", () => this.#checkGuards());
137
+ else await this.#checkGuards();
138
+ if (this.endpointInfo.internalArgs.length > 0) {
139
+ this.internalArgs = await Promise.all(
140
+ this.endpointInfo.internalArgs.map((arg) => {
141
+ const argValue = new arg.argRef().getArg(this) ?? null;
142
+ if (argValue === null && !arg.option?.nullable)
143
+ throw new Exception.Unauthorized(`Internal Argument ${arg.argRef.name} is required`);
144
+ return argValue;
145
+ }),
146
+ );
147
+ }
148
+ if (!this.trace) return await this.endpointInfo.execFn.call(this.adaptor, ...this.args, ...this.internalArgs);
149
+ return await traceSpan(
150
+ "handler",
151
+ async () => await this.endpointInfo.execFn?.call(this.adaptor, ...this.args, ...this.internalArgs),
124
152
  );
125
- const result = await this.endpointInfo.execFn.call(this.adaptor, ...this.args, ...this.internalArgs);
126
- return result;
127
153
  };
128
154
  let next = coreExec;
129
- for (let i = middlewares.length - 1; i >= 0; i--) {
130
- const MiddlewareCls = middlewares[i];
131
- if (!MiddlewareCls) continue;
132
- const middleware = new MiddlewareCls();
133
- const currentNext = next;
134
- next = async () => await (await middleware.use(this.getEnv()))(this, currentNext);
155
+ if (this.#middleware.size > 0 || endpointMiddlewares.length > 0) {
156
+ const middlewares = [...this.#middleware.values(), ...endpointMiddlewares];
157
+ for (let i = middlewares.length - 1; i >= 0; i--) {
158
+ const MiddlewareCls = middlewares[i];
159
+ if (!MiddlewareCls) continue;
160
+ const middleware = new MiddlewareCls();
161
+ const currentNext = next;
162
+ next = async () => await (await middleware.use(this.getEnv()))(this, currentNext);
163
+ }
135
164
  }
136
- const result = await next();
165
+ const result = this.trace ? await traceSpan("execChain", () => next()) : await next();
137
166
  if (this.endpointInfo.type === "pubsub") return;
138
167
  if (result instanceof Response) return result;
139
- const resolved = await SignalContext.resolveReturn(result, {
140
- signalContext: this,
141
- returnRef: this.endpointInfo.returns.returnRef,
142
- arrDepth: this.endpointInfo.returns.arrDepth,
143
- registry: this.#registry,
144
- live: this.#live,
145
- });
146
- return this.ctx.makeResponse(resolved, this.endpointInfo);
168
+ if (!this.trace) {
169
+ const resolved = await SignalContext.resolveReturn(result, {
170
+ signalContext: this,
171
+ returnRef: this.endpointInfo.returns.returnRef,
172
+ arrDepth: this.endpointInfo.returns.arrDepth,
173
+ registry: this.#registry,
174
+ live: this.#live,
175
+ });
176
+ return this.ctx.makeResponse(resolved, this.endpointInfo);
177
+ }
178
+ const resolved = await traceSpan("resolveReturn", () =>
179
+ SignalContext.resolveReturn(result, {
180
+ signalContext: this,
181
+ returnRef: this.endpointInfo.returns.returnRef,
182
+ arrDepth: this.endpointInfo.returns.arrDepth,
183
+ registry: this.#registry,
184
+ live: this.#live,
185
+ }),
186
+ );
187
+ return await traceSpan("serialize", async () => this.ctx.makeResponse(resolved, this.endpointInfo));
147
188
  }
148
189
  static async try(
149
190
  endpoint: Adaptor,
@@ -257,6 +298,11 @@ export class SignalContext<
257
298
  service: DatabaseService,
258
299
  { arrDepth, nullable }: { arrDepth: number; nullable: boolean },
259
300
  ): Promise<unknown> {
301
+ if (value === null || value === undefined) {
302
+ if (nullable) return null;
303
+ throw new Error(`Document ${value} is not found`);
304
+ }
305
+ if (arrDepth > 0 && Array.isArray(value) && value.length === 0) return [];
260
306
  if (arrDepth === 0)
261
307
  return await service.__load(String(value)).then((doc) => {
262
308
  if (doc === null) {
@@ -300,19 +346,24 @@ export class SignalContext<
300
346
  export class HttpExecutionContext<Appended = unknown> {
301
347
  req: Bun.BunRequest & Appended;
302
348
  res = Response;
303
- url: URL;
349
+ #url: URL | null = null;
304
350
  params: RuntimeRecord = {};
305
351
  searchParams: RuntimeRecord = {};
306
352
  body: RuntimeRecord = {};
307
353
  constructor(req: Bun.BunRequest) {
308
354
  this.req = req as Bun.BunRequest & Appended;
309
- this.url = new URL(req.url);
355
+ }
356
+ get url() {
357
+ if (!this.#url) this.#url = new URL(this.req.url);
358
+ return this.#url;
310
359
  }
311
360
  async getArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
361
+ if (endpointInfo.args.length === 0) return [];
312
362
  this.params = this.req.params;
313
363
 
314
- const hasUploadArgs = endpointInfo.args.some((arg) => arg.type === "body" && arg.argRef === Upload);
315
- if (endpointInfo.type === "mutation" && this.req.body) {
364
+ const hasBodyArgs = endpointInfo.args.some((arg) => arg.type === "body");
365
+ const hasUploadArgs = hasBodyArgs && endpointInfo.args.some((arg) => arg.type === "body" && arg.argRef === Upload);
366
+ if (endpointInfo.type === "mutation" && hasBodyArgs && this.req.body) {
316
367
  if (hasUploadArgs) {
317
368
  const formData = await this.req.formData();
318
369
  this.body = {};
@@ -361,6 +412,9 @@ export class HttpExecutionContext<Appended = unknown> {
361
412
  }
362
413
  makeResponse(result: unknown, endpointInfo: EndpointInfo) {
363
414
  if (result instanceof Response) return result;
415
+ if (endpointInfo.returns.arrDepth === 0 && PrimitiveRegistry.has(endpointInfo.returns.returnRef as Cls)) {
416
+ return this.res.json(result);
417
+ }
364
418
  const value = serialize(endpointInfo.returns.returnRef, endpointInfo.returns.arrDepth, result, "object", {
365
419
  nullable: endpointInfo.returns.nullable,
366
420
  });