akanjs 2.3.9-rc.2 → 2.3.9-rc.4

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/common/Logger.ts CHANGED
@@ -2,12 +2,24 @@ import dayjs from "dayjs";
2
2
 
3
3
  const logLevels = ["trace", "verbose", "debug", "log", "info", "warn", "error"] as const;
4
4
  export type LogLevel = (typeof logLevels)[number];
5
+ export type LoggerMeta = Record<string, unknown>;
6
+
7
+ export interface LoggerSerializedError {
8
+ name?: string;
9
+ message: string;
10
+ stack?: string;
11
+ cause?: LoggerSerializedError | string;
12
+ digest?: string;
13
+ code?: string | number;
14
+ status?: number;
15
+ }
5
16
 
6
17
  export interface LoggerSinkEntry {
7
18
  stream: "stdout" | "stderr";
8
19
  level?: LogLevel;
9
20
  message: string;
10
21
  plainMessage: string;
22
+ meta?: LoggerMeta;
11
23
  }
12
24
 
13
25
  export type LoggerSink = (entry: LoggerSinkEntry) => void | Promise<void>;
@@ -33,6 +45,11 @@ const colorizeMap: { [key in LogLevel]: (text: string) => string } = {
33
45
 
34
46
  const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
35
47
 
48
+ function readErrorProp(error: unknown, key: string): unknown {
49
+ if (typeof error !== "object" || error === null || !(key in error)) return undefined;
50
+ return (error as Record<string, unknown>)[key];
51
+ }
52
+
36
53
  /** Log-level aware logger used by Akan runtime, CLI, and application services. */
37
54
  export class Logger {
38
55
  static level: LogLevel = (process.env?.AKAN_PUBLIC_LOG_LEVEL as LogLevel | undefined) ?? "log";
@@ -86,6 +103,9 @@ export class Logger {
86
103
  error(msg: string, context = "", name = this.name ?? "App") {
87
104
  if (Logger.#shouldLog("error")) Logger.#printMessages(name, msg, context, "error");
88
105
  }
106
+ errorObject(msg: string, error: unknown, meta: LoggerMeta = {}, context = "", name = this.name ?? "App") {
107
+ if (Logger.#shouldLog("error")) Logger.#printError(name, msg, error, meta, context);
108
+ }
89
109
  raw(msg: string, method?: "console" | "process") {
90
110
  Logger.rawLog(msg, method);
91
111
  }
@@ -114,6 +134,60 @@ export class Logger {
114
134
  static error(msg: string, context = "", name = "App") {
115
135
  if (Logger.#shouldLog("error")) Logger.#printMessages(name, msg, context, "error");
116
136
  }
137
+ static errorObject(msg: string, error: unknown, meta: LoggerMeta = {}, context = "", name = "App") {
138
+ if (Logger.#shouldLog("error")) Logger.#printError(name, msg, error, meta, context);
139
+ }
140
+ static createErrorId(input: { requestId?: string; scope?: string; sequence?: string | number } = {}) {
141
+ const parts = ["akanerr", input.requestId, input.scope, input.sequence, Date.now().toString(36)]
142
+ .filter((part): part is string | number => part !== undefined && part !== "")
143
+ .map((part) => String(part).replace(/[^a-zA-Z0-9_-]/g, "-"));
144
+ return parts.join("-");
145
+ }
146
+ static serializeError(error: unknown): LoggerSerializedError {
147
+ if (error instanceof Error) {
148
+ const digest = readErrorProp(error, "digest");
149
+ const code = readErrorProp(error, "code");
150
+ const status = readErrorProp(error, "status") ?? readErrorProp(error, "statusCode");
151
+ return {
152
+ name: error.name,
153
+ message: error.message,
154
+ ...(error.stack ? { stack: error.stack } : {}),
155
+ ...(error.cause !== undefined ? { cause: Logger.serializeError(error.cause) } : {}),
156
+ ...(typeof digest === "string" ? { digest } : {}),
157
+ ...(typeof code === "string" || typeof code === "number" ? { code } : {}),
158
+ ...(typeof status === "number" ? { status } : {}),
159
+ };
160
+ }
161
+ if (typeof error === "object" && error !== null) {
162
+ const message = readErrorProp(error, "message");
163
+ const name = readErrorProp(error, "name");
164
+ const stack = readErrorProp(error, "stack");
165
+ const cause = readErrorProp(error, "cause");
166
+ const digest = readErrorProp(error, "digest");
167
+ const code = readErrorProp(error, "code");
168
+ const status = readErrorProp(error, "status") ?? readErrorProp(error, "statusCode");
169
+ return {
170
+ ...(typeof name === "string" ? { name } : {}),
171
+ message: typeof message === "string" ? message : String(error),
172
+ ...(typeof stack === "string" ? { stack } : {}),
173
+ ...(cause !== undefined ? { cause: Logger.serializeError(cause) } : {}),
174
+ ...(typeof digest === "string" ? { digest } : {}),
175
+ ...(typeof code === "string" || typeof code === "number" ? { code } : {}),
176
+ ...(typeof status === "number" ? { status } : {}),
177
+ };
178
+ }
179
+ return { message: String(error) };
180
+ }
181
+ static formatError(error: unknown) {
182
+ const serialized = Logger.serializeError(error);
183
+ const details = serialized.stack ?? serialized.message;
184
+ if (!serialized.cause) return details;
185
+ const cause =
186
+ typeof serialized.cause === "string"
187
+ ? serialized.cause
188
+ : (serialized.cause.stack ?? serialized.cause.message);
189
+ return `${details}\nCaused by: ${cause}`;
190
+ }
117
191
  static #colorize(msg: string, logLevel: LogLevel) {
118
192
  return colorizeMap[logLevel](msg);
119
193
  }
@@ -137,12 +211,18 @@ export class Logger {
137
211
  }
138
212
  }
139
213
  }
214
+ static #printError(name: string | undefined, msg: string, error: unknown, meta: LoggerMeta, context: string) {
215
+ const serializedError = Logger.serializeError(error);
216
+ const message = `${msg}: ${Logger.formatError(error)}`;
217
+ Logger.#printMessages(name, message, context, "error", "stderr", { ...meta, error: serializedError });
218
+ }
140
219
  static #printMessages(
141
220
  name: string | undefined,
142
221
  content: string,
143
222
  context: string,
144
223
  logLevel: LogLevel,
145
224
  writeStreamType: "stdout" | "stderr" = logLevel === "error" ? "stderr" : "stdout",
225
+ meta?: LoggerMeta,
146
226
  ) {
147
227
  const now = dayjs();
148
228
  const replicaIdx = (process as unknown as NodeJS.Process | undefined)?.env?.AKAN_REPLICA_IDX;
@@ -158,7 +238,7 @@ export class Logger {
158
238
  const timeDiffMsg = clc.yellow(`+${now.diff(Logger.#startAt, "ms")}ms`);
159
239
  const message = `${processMsg} ${timestampMsg} ${logLevelMsg} ${contextMsg} ${contentMsg} ${timeDiffMsg}\n`;
160
240
  if (Logger.#shouldEmitSink(logLevel))
161
- Logger.#emit({ stream: writeStreamType, level: logLevel, message, plainMessage: Logger.#stripAnsi(message) });
241
+ Logger.#emit({ stream: writeStreamType, level: logLevel, message, plainMessage: Logger.#stripAnsi(message), meta });
162
242
  if (!Logger.#shouldWriteConsole(logLevel)) return;
163
243
  if (typeof window === "undefined")
164
244
  (process[writeStreamType] as unknown as NodeJS.WriteStream | undefined)?.write(message);
package/common/index.ts CHANGED
@@ -17,7 +17,14 @@ export { isPhoneNumber } from "./isPhoneNumber";
17
17
  export { isQueryEqual } from "./isQueryEqual";
18
18
  export { isValidDate } from "./isValidDate";
19
19
  export { decodeJwtPayload } from "./jwtDecode";
20
- export { Logger, type LoggerSink, type LoggerSinkEntry, type LogLevel } from "./Logger";
20
+ export {
21
+ Logger,
22
+ type LoggerMeta,
23
+ type LoggerSerializedError,
24
+ type LoggerSink,
25
+ type LoggerSinkEntry,
26
+ type LogLevel,
27
+ } from "./Logger";
21
28
  export {
22
29
  type AkanI18nConfig,
23
30
  type AkanI18nConfigInput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.3.9-rc.2",
3
+ "version": "2.3.9-rc.4",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,5 +1,5 @@
1
1
  import type { BaseEnv } from "akanjs/base";
2
- import { Logger, lowerlize } from "akanjs/common";
2
+ import { Logger } from "akanjs/common";
3
3
  import {
4
4
  type Adaptor,
5
5
  type AdaptorCls,
@@ -26,29 +26,17 @@ import type { SignalRoutes, WebsocketRoutes } from "../types";
26
26
  import { getPredefinedAdaptor, predefinedAdaptorRole } from "./predefinedAdaptor";
27
27
  import { collectAdaptors, resolveAdaptorHierarchy } from "./resolveAdaptorHierarchy";
28
28
  import { resolveServiceHierarchy } from "./resolveServiceHierarchy";
29
- import { reasonMessage, runStage, toError } from "./utils";
30
-
31
- interface DestroyableUse {
32
- onDestroy(): Promise<void> | void;
33
- }
34
-
35
- const isDestroyableUse = (value: unknown): value is DestroyableUse =>
36
- typeof value === "object" &&
37
- value !== null &&
38
- "onDestroy" in value &&
39
- typeof (value as { onDestroy?: unknown }).onDestroy === "function";
40
-
41
- const normalizeServiceRefName = (refName: string) => {
42
- const normalized = lowerlize(refName);
43
- return normalized.endsWith("Service") ? normalized.slice(0, -"Service".length) : normalized;
44
- };
45
-
46
- const normalizeSignalRefName = (refName: string) => {
47
- const normalized = lowerlize(refName);
48
- return normalized.endsWith("Signal") ? normalized : `${normalized}Signal`;
49
- };
50
-
51
- const normalizeAdaptorRefName = (refName: string) => lowerlize(refName);
29
+ import {
30
+ type DiModuleCandidate,
31
+ getModuleDependencyRefNames,
32
+ isDestroyableUse,
33
+ normalizeAdaptorRefName,
34
+ normalizeServiceRefName,
35
+ normalizeSignalRefName,
36
+ reasonMessage,
37
+ runStage,
38
+ toError,
39
+ } from "./utils";
52
40
 
53
41
  /**
54
42
  * Owns the app's DI container state (registry + live maps + init order) and
@@ -88,25 +76,32 @@ export class DiLifecycle {
88
76
  this.#middleware.set(middleware.refName, middleware);
89
77
  });
90
78
  this.webProxies.push(...defaultOption.getWebProxies());
79
+ const databaseCandidates = new Map<string, DiModuleCandidate>();
80
+ const serviceCandidates = new Map<string, DiModuleCandidate>();
91
81
  libs.forEach((lib) => {
92
82
  lib.option.getMiddlewares().forEach((middleware) => {
93
83
  this.#middleware.set(middleware.refName, middleware);
94
84
  });
95
85
  this.webProxies.push(...lib.option.getWebProxies());
96
86
  lib.database.forEach((mod) => {
97
-
98
- if (!mod.service.srv.enabled) return;
99
- this.#database.set(mod.constant.refName, mod);
87
+ databaseCandidates.set(mod.constant.refName, { refName: mod.constant.refName, module: mod });
100
88
  });
101
89
  lib.service.forEach((mod) => {
102
-
103
- if (!mod.service.srv.enabled) return;
104
- this.#service.set(mod.service.srv.refName, mod);
90
+ serviceCandidates.set(mod.service.srv.refName, { refName: mod.service.srv.refName, module: mod });
105
91
  });
106
92
  lib.scalar.forEach((mod) => {
107
93
  this.#scalar.set(mod.constant.refName, mod);
108
94
  });
109
95
  });
96
+ const disabledModules = this.#resolveDisabledModules(databaseCandidates, serviceCandidates);
97
+ databaseCandidates.forEach(({ refName, module }) => {
98
+ if (disabledModules.has(refName)) return;
99
+ this.#database.set(refName, module as DatabaseModule);
100
+ });
101
+ serviceCandidates.forEach(({ refName, module }) => {
102
+ if (disabledModules.has(refName)) return;
103
+ this.#service.set(refName, module as ServiceModule);
104
+ });
110
105
  this.#database.forEach((mod) => {
111
106
  const databaseAdaptor = DatabaseResolver.resolveDatabase(mod.constant, mod.database);
112
107
  this.#adaptor.set(databaseAdaptor.refName, databaseAdaptor);
@@ -120,6 +115,39 @@ export class DiLifecycle {
120
115
  }
121
116
  }
122
117
 
118
+ #resolveDisabledModules(
119
+ databaseCandidates: Map<string, DiModuleCandidate>,
120
+ serviceCandidates: Map<string, DiModuleCandidate>,
121
+ ) {
122
+ const candidates = new Map<string, DiModuleCandidate>([...databaseCandidates, ...serviceCandidates]);
123
+ const disabledReasons = new Map<string, string>();
124
+
125
+ candidates.forEach(({ refName, module }) => {
126
+ if (!module.service.srv.enabled) disabledReasons.set(refName, "service disabled");
127
+ });
128
+
129
+ let changed = true;
130
+ while (changed) {
131
+ changed = false;
132
+ candidates.forEach(({ refName, module }) => {
133
+ if (disabledReasons.has(refName)) return;
134
+ for (const dependencyRefName of getModuleDependencyRefNames(module)) {
135
+ if (dependencyRefName === refName) continue;
136
+ const dependencyReason = disabledReasons.get(dependencyRefName);
137
+ if (!dependencyReason) continue;
138
+ disabledReasons.set(refName, `depends on disabled module "${dependencyRefName}"`);
139
+ changed = true;
140
+ break;
141
+ }
142
+ });
143
+ }
144
+
145
+ disabledReasons.forEach((reason, refName) => {
146
+ this.logger.verbose(`Skipping disabled module "${refName}": ${reason}`);
147
+ });
148
+ return new Set(disabledReasons.keys());
149
+ }
150
+
123
151
  /** Run every init stage in dependency order and collect the generated routes. */
124
152
  async initializeAll(): Promise<SignalRoutes> {
125
153
  await this.#initializeUses();
@@ -1,8 +1,72 @@
1
+ import { INJECT_META } from "akanjs/base";
2
+ import { lowerlize } from "akanjs/common";
3
+ import type { InjectInfo } from "akanjs/service";
4
+ import type { DatabaseModule, ServiceModule } from "../akanLib";
5
+
1
6
  interface StageTask {
2
7
  label: string;
3
8
  run: () => Promise<unknown>;
4
9
  }
5
10
 
11
+ interface DestroyableUse {
12
+ onDestroy(): Promise<void> | void;
13
+ }
14
+
15
+ type InjectableCls = { [INJECT_META]?: unknown };
16
+
17
+ export interface DiModuleCandidate {
18
+ refName: string;
19
+ module: DatabaseModule | ServiceModule;
20
+ }
21
+
22
+ export const isDestroyableUse = (value: unknown): value is DestroyableUse =>
23
+ typeof value === "object" &&
24
+ value !== null &&
25
+ "onDestroy" in value &&
26
+ typeof (value as { onDestroy?: unknown }).onDestroy === "function";
27
+
28
+ export const normalizeServiceRefName = (refName: string) => {
29
+ const normalized = lowerlize(refName);
30
+ return normalized.endsWith("Service") ? normalized.slice(0, -"Service".length) : normalized;
31
+ };
32
+
33
+ export const normalizeSignalRefName = (refName: string) => {
34
+ const normalized = lowerlize(refName);
35
+ return normalized.endsWith("Signal") ? normalized : `${normalized}Signal`;
36
+ };
37
+
38
+ export const normalizeAdaptorRefName = (refName: string) => lowerlize(refName);
39
+
40
+ const normalizeInjectedServiceRefName = (propKey: string) =>
41
+ propKey.endsWith("Service") ? propKey.slice(0, -"Service".length) : propKey;
42
+
43
+ const normalizeInjectedDatabaseRefName = (propKey: string) =>
44
+ propKey.endsWith("Model") ? propKey.slice(0, -"Model".length) : propKey;
45
+
46
+ const normalizeInjectedSignalRefName = (propKey: string) => {
47
+ const normalized = lowerlize(propKey);
48
+ return normalized.endsWith("Signal") ? normalized.slice(0, -"Signal".length) : normalized;
49
+ };
50
+
51
+ const getModuleInjectables = (mod: DatabaseModule | ServiceModule): InjectableCls[] => {
52
+ const injectables: InjectableCls[] = [mod.service.srv, mod.signal.internal, mod.signal.endpoint, mod.signal.server];
53
+ if ("constant" in mod) injectables.push(mod.signal.slice);
54
+ return injectables;
55
+ };
56
+
57
+ export const getModuleDependencyRefNames = (mod: DatabaseModule | ServiceModule) => {
58
+ const dependencies = new Set<string>();
59
+ for (const injectable of getModuleInjectables(mod)) {
60
+ const injectMap = (injectable[INJECT_META] ?? {}) as Record<string, InjectInfo>;
61
+ for (const [propKey, injectInfo] of Object.entries(injectMap)) {
62
+ if (injectInfo.type === "service") dependencies.add(normalizeInjectedServiceRefName(propKey));
63
+ else if (injectInfo.type === "database") dependencies.add(normalizeInjectedDatabaseRefName(propKey));
64
+ else if (injectInfo.type === "signal") dependencies.add(normalizeInjectedSignalRefName(propKey));
65
+ }
66
+ }
67
+ return dependencies;
68
+ };
69
+
6
70
  /**
7
71
  * Run every task in parallel and, if any rejects, throw a single
8
72
  * `AggregateError` that enumerates every failing label + cause. This replaces
@@ -110,7 +110,7 @@ type InMsg = InitMsg | RenderMsg | CancelMsg | ReloadMsg | UpdateCssAssetsMsg |
110
110
  type RenderControl =
111
111
  | { type: "redirect"; location: string; method: "replace" | "push"; status: RedirectStatus }
112
112
  | { type: "not-found" }
113
- | { type: "error"; error: unknown };
113
+ | { type: "error"; error: unknown; errorId: string; digest: string };
114
114
  interface FlightRenderResult {
115
115
  chunks: Uint8Array[];
116
116
  bytes: number;
@@ -206,6 +206,7 @@ export class RscRenderer {
206
206
  #resultCacheHits = 0;
207
207
  #resultCacheMisses = 0;
208
208
  #resultCacheBypass = 0;
209
+ #renderErrorSequence = 0;
209
210
  readonly #send: (message: unknown) => void;
210
211
 
211
212
  constructor() {
@@ -223,6 +224,74 @@ export class RscRenderer {
223
224
  this.#send({ type: "hello" });
224
225
  }
225
226
 
227
+ #nextRenderErrorId(requestId: string | undefined, scope: string): string {
228
+ this.#renderErrorSequence += 1;
229
+ return Logger.createErrorId({ requestId, scope, sequence: this.#renderErrorSequence });
230
+ }
231
+
232
+ #createRenderErrorPayload(
233
+ error: unknown,
234
+ input: {
235
+ requestId?: string;
236
+ scope: string;
237
+ errorId?: string;
238
+ url?: URL | string;
239
+ pathname?: string;
240
+ routeId?: string;
241
+ trace?: RscTraceMetadata;
242
+ },
243
+ ) {
244
+ const errorId = input.errorId ?? this.#nextRenderErrorId(input.requestId, input.scope);
245
+ const serialized = Logger.serializeError(error);
246
+ const pathname = input.pathname ?? input.trace?.pathname;
247
+ const routeId = input.routeId ?? input.trace?.routeId;
248
+ const url = typeof input.url === "string" ? input.url : input.url?.href;
249
+ const meta = {
250
+ errorId,
251
+ requestId: input.requestId,
252
+ scope: input.scope,
253
+ url,
254
+ pathname,
255
+ routeId,
256
+ digest: errorId,
257
+ originalDigest: serialized.digest,
258
+ };
259
+ return {
260
+ message: serialized.message,
261
+ name: serialized.name,
262
+ stack: serialized.stack,
263
+ errorId,
264
+ digest: errorId,
265
+ requestId: input.requestId,
266
+ scope: input.scope,
267
+ pathname,
268
+ routeId,
269
+ meta,
270
+ };
271
+ }
272
+
273
+ #logRenderError(
274
+ message: string,
275
+ error: unknown,
276
+ input: {
277
+ requestId?: string;
278
+ scope: string;
279
+ errorId?: string;
280
+ url?: URL | string;
281
+ pathname?: string;
282
+ routeId?: string;
283
+ trace?: RscTraceMetadata;
284
+ },
285
+ ) {
286
+ const payload = this.#createRenderErrorPayload(error, input);
287
+ this.#logger.errorObject(
288
+ `${message} requestId=${input.requestId ?? "(none)"} scope=${input.scope} errorId=${payload.errorId}`,
289
+ error,
290
+ payload.meta,
291
+ );
292
+ return payload;
293
+ }
294
+
226
295
  #handleMessage(msg: InMsg): void {
227
296
  switch (msg.type) {
228
297
  case "init":
@@ -287,11 +356,11 @@ export class RscRenderer {
287
356
  this.#logger.verbose(`init complete in ${Date.now() - startedAt}ms`);
288
357
  this.#send({ type: "ready" });
289
358
  } catch (error) {
290
- this.#logger.error(`init failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
359
+ const payload = this.#logRenderError("init failed", error, { requestId: "__init__", scope: "rsc-init" });
291
360
  this.#send({
292
361
  type: "error",
362
+ ...payload,
293
363
  requestId: "__init__",
294
- message: error instanceof Error ? error.message : String(error),
295
364
  });
296
365
  }
297
366
  }
@@ -326,14 +395,15 @@ export class RscRenderer {
326
395
  this.#logger.verbose(`reload complete buildId=${msg.buildId} in ${Date.now() - startedAt}ms`);
327
396
  this.#send({ type: "reloaded", buildId: msg.buildId });
328
397
  } catch (error) {
329
- this.#logger.error(
330
- `reload failed buildId=${msg.buildId}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`,
331
- );
398
+ const payload = this.#logRenderError(`reload failed buildId=${msg.buildId}`, error, {
399
+ requestId: "__reload__",
400
+ scope: "rsc-reload",
401
+ });
332
402
  this.#send({
333
403
  type: "error",
404
+ ...payload,
334
405
  requestId: "__reload__",
335
406
  buildId: msg.buildId,
336
- message: error instanceof Error ? error.message : String(error),
337
407
  });
338
408
  }
339
409
  }
@@ -738,9 +808,13 @@ export class RscRenderer {
738
808
  this.#send({ type: "not-found", requestId });
739
809
  return;
740
810
  }
741
- this.#logger.error(
742
- `render[${requestId}] failed url=${url}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`,
743
- );
811
+ const payload = this.#logRenderError("[RSC] worker render failed", error, {
812
+ requestId,
813
+ scope: "rsc-render",
814
+ url,
815
+ pathname: activeRoute.url?.pathname,
816
+ routeId: activeRoute.match?.pathRoute.path,
817
+ });
744
818
  const fallbackUrl = activeRoute.url;
745
819
  const fallbackMatch = activeRoute.match;
746
820
  if (
@@ -762,8 +836,8 @@ export class RscRenderer {
762
836
  }
763
837
  this.#send({
764
838
  type: "error",
839
+ ...payload,
765
840
  requestId,
766
- message: error instanceof Error ? error.message : String(error),
767
841
  });
768
842
  } finally {
769
843
  this.#activeRenderReaders.delete(requestId);
@@ -834,6 +908,7 @@ export class RscRenderer {
834
908
  const controlRef: { current: RenderControl | null } = { current: null };
835
909
  const stream = await renderToReadableStream(element, clientManifest, {
836
910
  onError: (error) => {
911
+ if (controlRef.current?.type === "error") return controlRef.current.digest;
837
912
  if (isAkanRedirectError(error)) {
838
913
  controlRef.current = {
839
914
  type: "redirect",
@@ -851,8 +926,13 @@ export class RscRenderer {
851
926
  controlRef.current = { type: "not-found" };
852
927
  return error.digest;
853
928
  }
854
- controlRef.current = { type: "error", error };
855
- return error instanceof Error ? error.message : String(error);
929
+ const payload = this.#logRenderError("[RSC] worker render stream error", error, {
930
+ requestId: options.requestId,
931
+ scope: "rsc-stream",
932
+ trace: options.trace,
933
+ });
934
+ controlRef.current = { type: "error", error, errorId: payload.errorId, digest: payload.digest };
935
+ return payload.digest;
856
936
  },
857
937
  });
858
938
  const reader = stream.getReader();
@@ -991,11 +1071,14 @@ export class RscRenderer {
991
1071
  this.#stats.totalFlightChunks += result.chunksCount;
992
1072
  return true;
993
1073
  } catch (fallbackError) {
994
- this.#logger.error(
995
- `render[${requestId}] custom ${kind} fallback failed: ${
996
- fallbackError instanceof Error ? (fallbackError.stack ?? fallbackError.message) : String(fallbackError)
997
- }`,
998
- );
1074
+ this.#logRenderError(`render[${requestId}] custom ${kind} fallback failed`, fallbackError, {
1075
+ requestId,
1076
+ scope: `fallback:${kind}`,
1077
+ url,
1078
+ pathname,
1079
+ routeId: route.path,
1080
+ trace,
1081
+ });
999
1082
  return false;
1000
1083
  }
1001
1084
  }
@@ -1025,11 +1108,13 @@ export class RscRenderer {
1025
1108
  this.#stats.totalFlightChunks += result.chunksCount;
1026
1109
  return true;
1027
1110
  } catch (error) {
1028
- this.#logger.error(
1029
- `render[${requestId}] system not-found fallback failed: ${
1030
- error instanceof Error ? (error.stack ?? error.message) : String(error)
1031
- }`,
1032
- );
1111
+ this.#logRenderError(`render[${requestId}] system not-found fallback failed`, error, {
1112
+ requestId,
1113
+ scope: "fallback:system-not-found",
1114
+ url,
1115
+ pathname: url.pathname,
1116
+ trace,
1117
+ });
1033
1118
  return false;
1034
1119
  }
1035
1120
  }
@@ -1047,9 +1132,13 @@ export class RscRenderer {
1047
1132
  return;
1048
1133
  }
1049
1134
  if (control.type === "error") {
1050
- const message = control.error instanceof Error ? control.error.message : String(control.error);
1051
- this.#logger.verbose(`render[${requestId}] error`);
1052
- this.#send({ type: "error", requestId, message });
1135
+ const payload = this.#createRenderErrorPayload(control.error, {
1136
+ requestId,
1137
+ scope: "rsc-render-control",
1138
+ errorId: control.errorId,
1139
+ });
1140
+ this.#logger.verbose(`render[${requestId}] error errorId=${payload.errorId}`);
1141
+ this.#send({ type: "error", ...payload, requestId });
1053
1142
  return;
1054
1143
  }
1055
1144
  this.#logger.verbose(`render[${requestId}] not-found`);
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { type AkanI18nConfig, DEFAULT_AKAN_I18N, Logger } from "akanjs/common";
4
+ import { type AkanI18nConfig, DEFAULT_AKAN_I18N, Logger, type LoggerMeta } from "akanjs/common";
5
5
  import type { AkanTheme } from "akanjs/fetch";
6
6
  import type { AkanMetricsReport } from "akanjs/service";
7
7
  import type { ClientManifest } from "./artifact";
@@ -14,7 +14,7 @@ const DEFAULT_RSC_HOST_MAX_PENDING_CHUNKS = 256;
14
14
  export interface RscPending {
15
15
  onChunk: (data: Uint8Array) => void;
16
16
  onEnd: () => void;
17
- onError: (message: string) => void;
17
+ onError: (error: string | RscWorkerErrorPayload) => void;
18
18
  onMeta?: (meta: { theme?: AkanTheme; status?: number; trace?: RscTraceMetadata }) => void;
19
19
  onCacheState?: (state: RouteCacheRenderState) => void;
20
20
  onRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
@@ -25,6 +25,19 @@ export interface RscPending {
25
25
  export type RscRedirectMethod = "replace" | "push";
26
26
  export type RscRedirectStatus = 303 | 307 | 308;
27
27
 
28
+ export interface RscWorkerErrorPayload {
29
+ message: string;
30
+ name?: string;
31
+ stack?: string;
32
+ errorId?: string;
33
+ digest?: string;
34
+ requestId?: string;
35
+ scope?: string;
36
+ pathname?: string;
37
+ routeId?: string;
38
+ meta?: LoggerMeta;
39
+ }
40
+
28
41
  export interface RscWorkerInvalidateCacheMessage {
29
42
  type: "invalidate-cache";
30
43
  reason?: string;
@@ -35,6 +48,7 @@ export interface RscWorkerInvalidateCacheMessage {
35
48
  export type RscRenderResult =
36
49
  | {
37
50
  type: "stream";
51
+ requestId?: string;
38
52
  stream: ReadableStream<Uint8Array>;
39
53
  theme?: AkanTheme;
40
54
  status?: number;
@@ -66,6 +80,23 @@ function createRscRenderAbortError(reason?: unknown): Error {
66
80
  return error;
67
81
  }
68
82
 
83
+ export function createRscWorkerError(input: string | RscWorkerErrorPayload): Error {
84
+ if (typeof input === "string") return new Error(input);
85
+ const error = new Error(input.message);
86
+ if (input.name) error.name = input.name;
87
+ if (input.stack) error.stack = input.stack;
88
+ Object.assign(error, {
89
+ errorId: input.errorId,
90
+ digest: input.digest ?? input.errorId,
91
+ requestId: input.requestId,
92
+ scope: input.scope,
93
+ pathname: input.pathname,
94
+ routeId: input.routeId,
95
+ meta: input.meta,
96
+ });
97
+ return error;
98
+ }
99
+
69
100
  export function createIdempotentRscRenderCancel(onCancel: (reason?: unknown) => void): (reason?: unknown) => void {
70
101
  let cancelled = false;
71
102
  return (reason?: unknown) => {
@@ -89,6 +120,7 @@ export function createRscWorkerInvalidateCacheMessage(
89
120
  }
90
121
 
91
122
  export function createRscHostRenderStream(input: {
123
+ requestId?: string;
92
124
  setPending: (pending: RscPending) => void;
93
125
  deletePending: () => void;
94
126
  sendRenderOrQueue: () => void;
@@ -153,7 +185,17 @@ export function createRscHostRenderStream(input: {
153
185
  const settleStream = () => {
154
186
  if (settled) return;
155
187
  settled = true;
156
- resolve({ type: "stream", stream, theme, status, trace, lateControl, cacheState, cancel: cancelRender });
188
+ resolve({
189
+ type: "stream",
190
+ requestId: input.requestId,
191
+ stream,
192
+ theme,
193
+ status,
194
+ trace,
195
+ lateControl,
196
+ cacheState,
197
+ cancel: cancelRender,
198
+ });
157
199
  };
158
200
  input.setPending({
159
201
  onMeta: (meta) => {
@@ -182,16 +224,17 @@ export function createRscHostRenderStream(input: {
182
224
  settleStream();
183
225
  controller.close();
184
226
  },
185
- onError: (msg) => {
227
+ onError: (payload) => {
228
+ const error = createRscWorkerError(payload);
186
229
  settleLateControl(null);
187
230
  settleCacheState({ cacheable: false, reason: "error" });
188
231
  cleanupAbortListener();
189
232
  if (!settled) {
190
233
  settled = true;
191
- reject(new Error(msg));
234
+ reject(error);
192
235
  return;
193
236
  }
194
- controller.error(new Error(msg));
237
+ controller.error(error);
195
238
  },
196
239
  onRedirect: (location, method, status) => {
197
240
  settleLateControl(null);
@@ -262,7 +305,7 @@ type RscInMsg =
262
305
  }
263
306
  | { type: "not-found"; requestId: string }
264
307
  | { type: "metrics"; metrics: AkanMetricsReport }
265
- | { type: "error"; requestId: string; message: string; buildId?: number };
308
+ | ({ type: "error"; requestId: string; buildId?: number } & RscWorkerErrorPayload);
266
309
 
267
310
  export interface RscWorkerReloadInput {
268
311
  clientManifest: ClientManifest;
@@ -379,7 +422,7 @@ export class RscWorker {
379
422
  this.#pending.set(requestId, {
380
423
  onChunk: (data) => controller.enqueue(data),
381
424
  onEnd: () => controller.close(),
382
- onError: (msg) => controller.error(new Error(msg)),
425
+ onError: (payload) => controller.error(createRscWorkerError(payload)),
383
426
  });
384
427
  const send = () => {
385
428
 
@@ -412,6 +455,7 @@ export class RscWorker {
412
455
  ): Promise<RscRenderResult> {
413
456
  const requestId = crypto.randomUUID();
414
457
  return createRscHostRenderStream({
458
+ requestId,
415
459
  setPending: (pending) => {
416
460
  this.#pending.set(requestId, pending);
417
461
  },
@@ -656,8 +700,8 @@ export class RscWorker {
656
700
  case "error":
657
701
  if (message.requestId === "__init__") {
658
702
 
659
- if (!this.#readyResolved) this.#rejectReady(new Error(String(message.message)));
660
- else this.#logger.error(`[rsc] worker init error on restart: ${message.message}`);
703
+ if (!this.#readyResolved) this.#rejectReady(createRscWorkerError(message));
704
+ else this.#logger.errorObject("[rsc] worker init error on restart", createRscWorkerError(message), message.meta);
661
705
  return;
662
706
  }
663
707
  if (message.requestId === "__reload__") {
@@ -665,12 +709,12 @@ export class RscWorker {
665
709
  this.#pendingReload &&
666
710
  (message.buildId === undefined || this.#pendingReload.targetBuildId === message.buildId)
667
711
  ) {
668
- this.#pendingReload.reject(new Error(String(message.message)));
712
+ this.#pendingReload.reject(createRscWorkerError(message));
669
713
  this.#pendingReload = null;
670
714
  }
671
715
  return;
672
716
  }
673
- this.#resolvePending(message.requestId, (p) => p.onError(String(message.message)));
717
+ this.#resolvePending(message.requestId, (p) => p.onError(message));
674
718
  return;
675
719
  }
676
720
  }
@@ -595,11 +595,28 @@ export class SsrFromRscRenderer {
595
595
  : SsrFromRscRenderer.#clientBootstrap;
596
596
 
597
597
  const renderHtml = async () => {
598
- const root = await thenable;
599
- const stream = await renderToReadableStream(root, {
600
- bootstrapScriptContent: bootstrap,
601
- });
602
- await stream.allReady;
598
+ let root: ReactNode;
599
+ try {
600
+ root = await thenable;
601
+ } catch (error) {
602
+ input.onError?.({ scope: "flight-decode", error });
603
+ throw error;
604
+ }
605
+ let stream: ReadableStream<Uint8Array> & { allReady: Promise<void> };
606
+ try {
607
+ stream = await renderToReadableStream(root, {
608
+ bootstrapScriptContent: bootstrap,
609
+ });
610
+ } catch (error) {
611
+ input.onError?.({ scope: "html-render", error });
612
+ throw error;
613
+ }
614
+ try {
615
+ await stream.allReady;
616
+ } catch (error) {
617
+ input.onError?.({ scope: "html-all-ready", error });
618
+ throw error;
619
+ }
603
620
  return stream;
604
621
  };
605
622
  const requestContext = input.requestStore ?? input.request;
@@ -73,4 +73,5 @@ export interface SsrFromRscInput {
73
73
  injectThemeInitScript?: boolean;
74
74
  lateControl?: Promise<SsrLateRedirect | null>;
75
75
  onCancel?: (reason?: unknown) => void;
76
+ onError?: (event: { scope: "flight-decode" | "html-render" | "html-all-ready"; error: unknown }) => void;
76
77
  }
@@ -6,6 +6,7 @@ import {
6
6
  DEFAULT_AKAN_I18N,
7
7
  getBasePathFromPathname,
8
8
  Logger,
9
+ type LoggerMeta,
9
10
  parseAkanI18nEnv,
10
11
  } from "akanjs/common";
11
12
  import { type AkanRequestStore, createRequestStore, parseCookieHeader } from "akanjs/fetch";
@@ -57,6 +58,15 @@ const RESERVED_BASE_PATHS = new Set(["admin"]);
57
58
  const CLIENT_CLOSED_REQUEST_STATUS = 499;
58
59
  export const DEFAULT_HTML_RESULT_CACHE_MAX_BODY_BYTES = 2 * 1024 * 1024;
59
60
 
61
+ interface SsrErrorLogMeta extends LoggerMeta {
62
+ errorId: string;
63
+ requestId?: string;
64
+ scope: string;
65
+ pathname?: string;
66
+ routeId?: string;
67
+ url?: string;
68
+ }
69
+
60
70
  export function createRscRedirectResponse(
61
71
  location: string,
62
72
  method: RscRedirectMethod,
@@ -449,10 +459,16 @@ export class WebRouter {
449
459
  return createRscRedirectResponse(result.location, result.method, result.status);
450
460
  if (result.type === "not-found") return WebRouter.#rscNotFoundResponse();
451
461
  if (result.status && result.status >= 500)
452
- return this.#renderRscErrorResponse("__rsc", "Internal Server Error");
462
+ return this.#renderRscErrorResponse(
463
+ "__rsc",
464
+ new Error("RSC render returned an error status"),
465
+ req,
466
+ result.trace,
467
+ result.requestId,
468
+ );
453
469
  return createRscNavigationStreamResponse(result);
454
470
  } catch (err) {
455
- return this.#renderRscErrorResponse("__rsc", err);
471
+ return this.#renderRscErrorResponse("__rsc", err, req);
456
472
  }
457
473
  },
458
474
  "/__rsc/manifest": () =>
@@ -530,6 +546,8 @@ export class WebRouter {
530
546
  );
531
547
  }
532
548
 
549
+ let activeRscRequestId: string | undefined;
550
+ let activeRscTrace: RscTraceMetadata | undefined;
533
551
  try {
534
552
  this.#requestStats.fullSsr += 1;
535
553
  const manifest = await this.#ensureRoute(url);
@@ -548,6 +566,10 @@ export class WebRouter {
548
566
  clientManifest: manifest.clientManifest,
549
567
  signal: req.signal,
550
568
  });
569
+ if (rscResult.type === "stream") {
570
+ activeRscRequestId = rscResult.requestId ?? rscResult.trace?.navId;
571
+ activeRscTrace = rscResult.trace;
572
+ }
551
573
  if (rscResult.type === "redirect")
552
574
  return Response.redirect(new URL(rscResult.location, url.origin), rscResult.status);
553
575
  if (rscResult.type === "not-found") return this.#renderSystemNotFoundFallbackResponse(req, url);
@@ -574,6 +596,22 @@ export class WebRouter {
574
596
  onCancel: (reason: unknown) => {
575
597
  rscResult.cancel(reason);
576
598
  },
599
+ onError: ({ scope, error }) => {
600
+ const meta = WebRouter.#createSsrErrorLogMeta(error, {
601
+ scope,
602
+ requestId: rscResult.requestId ?? rscResult.trace?.navId,
603
+ pathname: url.pathname,
604
+ routeId: rscResult.trace?.routeId,
605
+ url: url.href,
606
+ });
607
+ this.#logger.errorObject(
608
+ `[SSR] ${scope} failed requestId=${meta.requestId ?? "(none)"} pathname=${
609
+ meta.pathname ?? "(unknown)"
610
+ } route=${meta.routeId ?? "(unknown)"} errorId=${meta.errorId}`,
611
+ error,
612
+ meta,
613
+ );
614
+ },
577
615
  });
578
616
  const responseStatus = rscResult.status ?? 200;
579
617
  const responseHeaders = WebRouter.#htmlResponseHeaders(responseStatus);
@@ -642,7 +680,11 @@ export class WebRouter {
642
680
  headers,
643
681
  });
644
682
  } catch (err) {
645
- return this.#renderErrorResponse(req, url.pathname, err);
683
+ return this.#renderErrorResponse(req, "html", err, {
684
+ requestId: activeRscRequestId,
685
+ routeId: activeRscTrace?.routeId,
686
+ pathname: activeRscTrace?.pathname ?? url.pathname,
687
+ });
646
688
  }
647
689
  },
648
690
  };
@@ -797,34 +839,110 @@ export class WebRouter {
797
839
  });
798
840
  }
799
841
 
800
- #renderErrorResponse(req: Request, scope: string, err: unknown): Promise<Response> {
842
+ #renderErrorResponse(
843
+ req: Request,
844
+ scope: string,
845
+ err: unknown,
846
+ input: { requestId?: string; pathname?: string; routeId?: string } = {},
847
+ ): Promise<Response> {
801
848
  if (WebRouter.#isExpectedRequestAbort(err)) return Promise.resolve(WebRouter.#clientClosedResponse());
802
849
  const message = err instanceof Error ? err.message : String(err);
803
- this.#logger.error(`[SSR] render failed scope=${scope}: ${message}`);
850
+ const url = new URL(req.url);
851
+ const meta = WebRouter.#createSsrErrorLogMeta(err, {
852
+ scope,
853
+ requestId: input.requestId,
854
+ pathname: input.pathname ?? url.pathname,
855
+ routeId: input.routeId,
856
+ url: url.href,
857
+ });
858
+ this.#logger.errorObject(
859
+ `[SSR] render failed requestId=${meta.requestId ?? "(none)"} scope=${meta.scope} pathname=${
860
+ meta.pathname ?? "(unknown)"
861
+ } route=${meta.routeId ?? "(unknown)"} errorId=${meta.errorId}`,
862
+ err,
863
+ meta,
864
+ );
804
865
  this.#hub?.broadcast({ type: "error", message });
805
866
  return createSystemPageResponse({
806
867
  kind: "error",
807
868
  method: req.method,
808
- pathname: scope,
809
- lang: WebRouter.#getLocale(new URL(req.url).pathname, this.#artifact.i18n),
810
- homeHref: this.#getSystemPageHomeHref(req, new URL(req.url).pathname),
811
- stylesheetHref: this.#getStylesheetHref(req, new URL(req.url).pathname),
869
+ pathname: url.pathname,
870
+ lang: WebRouter.#getLocale(url.pathname, this.#artifact.i18n),
871
+ homeHref: this.#getSystemPageHomeHref(req, url.pathname),
872
+ stylesheetHref: this.#getStylesheetHref(req, url.pathname),
812
873
  showDetails: !this.#prodMode,
813
874
  error: err,
875
+ }).then((response) => {
876
+ response.headers.set("X-Akan-Error-Id", meta.errorId);
877
+ if (meta.requestId) response.headers.set("X-Akan-Rsc-Nav-Id", meta.requestId);
878
+ return response;
814
879
  });
815
880
  }
816
881
 
817
- #renderRscErrorResponse(scope: string, err: unknown): Response {
882
+ #renderRscErrorResponse(
883
+ scope: string,
884
+ err: unknown,
885
+ req?: Request,
886
+ trace?: RscTraceMetadata,
887
+ requestId?: string,
888
+ ): Response {
818
889
  if (WebRouter.#isExpectedRequestAbort(err)) return WebRouter.#clientClosedResponse();
819
890
  const message = err instanceof Error ? err.message : String(err);
820
- this.#logger.error(`[SSR] render failed scope=${scope}: ${message}`);
891
+ const url = req ? new URL(req.url) : undefined;
892
+ const meta = WebRouter.#createSsrErrorLogMeta(err, {
893
+ scope,
894
+ pathname: trace?.pathname ?? url?.pathname,
895
+ routeId: trace?.routeId,
896
+ requestId: trace?.navId ?? requestId,
897
+ url: url?.href,
898
+ });
899
+ this.#logger.errorObject(
900
+ `[SSR] render failed requestId=${meta.requestId ?? "(none)"} scope=${meta.scope} pathname=${
901
+ meta.pathname ?? "(unknown)"
902
+ } route=${meta.routeId ?? "(unknown)"} errorId=${meta.errorId}`,
903
+ err,
904
+ meta,
905
+ );
821
906
  this.#hub?.broadcast({ type: "error", message });
822
907
  return new Response("Internal Server Error", {
823
908
  status: 500,
824
- headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
909
+ headers: {
910
+ "Content-Type": "text/plain; charset=utf-8",
911
+ "Cache-Control": "no-store",
912
+ "X-Akan-Error-Id": meta.errorId,
913
+ ...(meta.requestId ? { "X-Akan-Rsc-Nav-Id": meta.requestId } : {}),
914
+ },
825
915
  });
826
916
  }
827
917
 
918
+ static #createSsrErrorLogMeta(
919
+ error: unknown,
920
+ input: { scope: string; requestId?: string; pathname?: string; routeId?: string; url?: string },
921
+ ): SsrErrorLogMeta {
922
+ const requestId = WebRouter.#stringErrorProp(error, "requestId") ?? input.requestId;
923
+ const scope = WebRouter.#stringErrorProp(error, "scope") ?? input.scope;
924
+ const pathname = WebRouter.#stringErrorProp(error, "pathname") ?? input.pathname;
925
+ const routeId = WebRouter.#stringErrorProp(error, "routeId") ?? input.routeId;
926
+ const errorId =
927
+ WebRouter.#stringErrorProp(error, "errorId") ??
928
+ WebRouter.#stringErrorProp(error, "digest") ??
929
+ Logger.createErrorId({ requestId, scope });
930
+ return {
931
+ errorId,
932
+ requestId,
933
+ scope,
934
+ pathname,
935
+ routeId,
936
+ url: input.url,
937
+ };
938
+ }
939
+
940
+ static #stringErrorProp(error: unknown, key: string): string | undefined {
941
+ if (typeof error !== "object" || error === null || !(key in error)) return undefined;
942
+ const value = (error as Record<string, unknown>)[key];
943
+ return typeof value === "string" && value ? value : undefined;
944
+ }
945
+
828
946
  static #clientClosedResponse(): Response {
829
947
  return new Response(null, {
830
948
  status: CLIENT_CLOSED_REQUEST_STATUS,
@@ -212,8 +212,7 @@ export const buildInternal = {
212
212
  resolveField: <Returns extends ConstantFieldTypeInput, Nullable extends boolean = false>(
213
213
  returnRef: Returns,
214
214
  signalOption?: Pick<SignalOption<Returns, Nullable>, "nullable">,
215
- ) =>
216
- new InternalInfo("resolveField", returnRef, signalOption),
215
+ ) => new InternalInfo("resolveField", returnRef, signalOption),
217
216
  interval: (scheduleTime: number, signalOption?: SignalOption) =>
218
217
  new InternalInfo("interval", Any, {
219
218
  enabled: true,
@@ -1,10 +1,21 @@
1
1
  declare const logLevels: readonly ["trace", "verbose", "debug", "log", "info", "warn", "error"];
2
2
  export type LogLevel = (typeof logLevels)[number];
3
+ export type LoggerMeta = Record<string, unknown>;
4
+ export interface LoggerSerializedError {
5
+ name?: string;
6
+ message: string;
7
+ stack?: string;
8
+ cause?: LoggerSerializedError | string;
9
+ digest?: string;
10
+ code?: string | number;
11
+ status?: number;
12
+ }
3
13
  export interface LoggerSinkEntry {
4
14
  stream: "stdout" | "stderr";
5
15
  level?: LogLevel;
6
16
  message: string;
7
17
  plainMessage: string;
18
+ meta?: LoggerMeta;
8
19
  }
9
20
  export type LoggerSink = (entry: LoggerSinkEntry) => void | Promise<void>;
10
21
  /** Log-level aware logger used by Akan runtime, CLI, and application services. */
@@ -26,6 +37,7 @@ export declare class Logger {
26
37
  info(msg: string, context?: string, name?: string): void;
27
38
  warn(msg: string, context?: string, name?: string): void;
28
39
  error(msg: string, context?: string, name?: string): void;
40
+ errorObject(msg: string, error: unknown, meta?: LoggerMeta, context?: string, name?: string): void;
29
41
  raw(msg: string, method?: "console" | "process"): void;
30
42
  rawLog(msg: string, method?: "console" | "process"): void;
31
43
  static trace(msg: string, context?: string, name?: string): void;
@@ -35,6 +47,14 @@ export declare class Logger {
35
47
  static info(msg: string, context?: string, name?: string): void;
36
48
  static warn(msg: string, context?: string, name?: string): void;
37
49
  static error(msg: string, context?: string, name?: string): void;
50
+ static errorObject(msg: string, error: unknown, meta?: LoggerMeta, context?: string, name?: string): void;
51
+ static createErrorId(input?: {
52
+ requestId?: string;
53
+ scope?: string;
54
+ sequence?: string | number;
55
+ }): string;
56
+ static serializeError(error: unknown): LoggerSerializedError;
57
+ static formatError(error: unknown): string;
38
58
  static rawLog(msg?: string, method?: "console" | "process", outputStream?: "log" | "error"): void;
39
59
  static raw(msg?: string, method?: "console" | "process", outputStream?: "log" | "error"): void;
40
60
  }
@@ -13,7 +13,7 @@ export { isPhoneNumber } from "./isPhoneNumber.d.ts";
13
13
  export { isQueryEqual } from "./isQueryEqual.d.ts";
14
14
  export { isValidDate } from "./isValidDate.d.ts";
15
15
  export { decodeJwtPayload } from "./jwtDecode.d.ts";
16
- export { Logger, type LoggerSink, type LoggerSinkEntry, type LogLevel } from "./Logger.d.ts";
16
+ export { Logger, type LoggerMeta, type LoggerSerializedError, type LoggerSink, type LoggerSinkEntry, type LogLevel, } from "./Logger.d.ts";
17
17
  export { type AkanI18nConfig, type AkanI18nConfigInput, DEFAULT_AKAN_I18N, parseAkanI18nEnv, resolveAkanI18nConfig, } from "./localeConfig.d.ts";
18
18
  export { lowerlize } from "./lowerlize.d.ts";
19
19
  export { mergeVersion } from "./mergeVersion.d.ts";
@@ -1,7 +1,20 @@
1
+ import type { DatabaseModule, ServiceModule } from "../akanLib.d.ts";
1
2
  interface StageTask {
2
3
  label: string;
3
4
  run: () => Promise<unknown>;
4
5
  }
6
+ interface DestroyableUse {
7
+ onDestroy(): Promise<void> | void;
8
+ }
9
+ export interface DiModuleCandidate {
10
+ refName: string;
11
+ module: DatabaseModule | ServiceModule;
12
+ }
13
+ export declare const isDestroyableUse: (value: unknown) => value is DestroyableUse;
14
+ export declare const normalizeServiceRefName: (refName: string) => string;
15
+ export declare const normalizeSignalRefName: (refName: string) => string;
16
+ export declare const normalizeAdaptorRefName: (refName: string) => string;
17
+ export declare const getModuleDependencyRefNames: (mod: DatabaseModule | ServiceModule) => Set<string>;
5
18
  /**
6
19
  * Run every task in parallel and, if any rejects, throw a single
7
20
  * `AggregateError` that enumerates every failing label + cause. This replaces
@@ -1,4 +1,4 @@
1
- import { type AkanI18nConfig } from "akanjs/common";
1
+ import { type AkanI18nConfig, type LoggerMeta } from "akanjs/common";
2
2
  import type { AkanTheme } from "akanjs/fetch";
3
3
  import type { AkanMetricsReport } from "akanjs/service";
4
4
  import type { ClientManifest } from "./artifact.d.ts";
@@ -8,7 +8,7 @@ import type { BaseBuildArtifact, CssAsset } from "./types.d.ts";
8
8
  export interface RscPending {
9
9
  onChunk: (data: Uint8Array) => void;
10
10
  onEnd: () => void;
11
- onError: (message: string) => void;
11
+ onError: (error: string | RscWorkerErrorPayload) => void;
12
12
  onMeta?: (meta: {
13
13
  theme?: AkanTheme;
14
14
  status?: number;
@@ -21,6 +21,18 @@ export interface RscPending {
21
21
  }
22
22
  export type RscRedirectMethod = "replace" | "push";
23
23
  export type RscRedirectStatus = 303 | 307 | 308;
24
+ export interface RscWorkerErrorPayload {
25
+ message: string;
26
+ name?: string;
27
+ stack?: string;
28
+ errorId?: string;
29
+ digest?: string;
30
+ requestId?: string;
31
+ scope?: string;
32
+ pathname?: string;
33
+ routeId?: string;
34
+ meta?: LoggerMeta;
35
+ }
24
36
  export interface RscWorkerInvalidateCacheMessage {
25
37
  type: "invalidate-cache";
26
38
  reason?: string;
@@ -29,6 +41,7 @@ export interface RscWorkerInvalidateCacheMessage {
29
41
  }
30
42
  export type RscRenderResult = {
31
43
  type: "stream";
44
+ requestId?: string;
32
45
  stream: ReadableStream<Uint8Array>;
33
46
  theme?: AkanTheme;
34
47
  status?: number;
@@ -47,9 +60,11 @@ export type RscRenderResult = {
47
60
  export declare function getRscHostMaxPendingChunks(value?: string | undefined): number;
48
61
  export declare function nextRscHostPendingChunkCount(currentPendingChunks: number, desiredSize: number | null): number;
49
62
  export declare function isRscHostPendingChunkOverflow(pendingChunks: number, maxPendingChunks: number): boolean;
63
+ export declare function createRscWorkerError(input: string | RscWorkerErrorPayload): Error;
50
64
  export declare function createIdempotentRscRenderCancel(onCancel: (reason?: unknown) => void): (reason?: unknown) => void;
51
65
  export declare function createRscWorkerInvalidateCacheMessage(invalidation?: string | RouteCacheInvalidation): RscWorkerInvalidateCacheMessage;
52
66
  export declare function createRscHostRenderStream(input: {
67
+ requestId?: string;
53
68
  setPending: (pending: RscPending) => void;
54
69
  deletePending: () => void;
55
70
  sendRenderOrQueue: () => void;
@@ -70,4 +70,8 @@ export interface SsrFromRscInput {
70
70
  injectThemeInitScript?: boolean;
71
71
  lateControl?: Promise<SsrLateRedirect | null>;
72
72
  onCancel?: (reason?: unknown) => void;
73
+ onError?: (event: {
74
+ scope: "flight-decode" | "html-render" | "html-all-ready";
75
+ error: unknown;
76
+ }) => void;
73
77
  }