akanjs 2.2.10 → 2.2.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # akanjs
2
2
 
3
+ ## 2.2.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 666e46c: Improve SSR hydration payload handling, redirect status propagation, and restore dev HMR incremental refresh behavior.
8
+ - 666e46c: Align RSC not-found responses with HTTP 404 semantics and add request-scoped policy tracking for future cache decisions.
9
+
10
+ ## 2.2.11
11
+
12
+ ### Patch Changes
13
+
14
+ - 8190632: Add Akan server console support with CLI/build integration and documentation for console-oriented workflows.
15
+ - 4bce7f9: Add initial LLM discovery docs and stabilize Akan client/runtime behavior.
16
+
17
+ - Add `/llms.txt` documentation discovery for Akan docs.
18
+ - Add `wsConnect` support for automatic WebSocket connections.
19
+ - Delay client bootstrap module execution until the SSR fizz stream is ready.
20
+ - Improve route tree, HMR, fetch, store, and SSR/client runtime stability.
21
+
3
22
  ## 2.2.7
4
23
 
5
24
  ### Patch Changes
package/client/cookie.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getEnv } from "akanjs/base";
2
2
  import { decodeJwtPayload, Logger } from "akanjs/common";
3
3
  import type { Account } from "akanjs/fetch";
4
- import { requestStorage } from "akanjs/fetch";
4
+ import { cookies as serverCookies, headers as serverHeaders } from "akanjs/fetch";
5
5
  import { loadCapacitorCore } from "./capacitor";
6
6
  import { storage } from "./storage";
7
7
  import { fetch } from "./useClient";
@@ -30,11 +30,7 @@ function parseCookieHeader(cookieHeader: string): Map<string, { name: string; va
30
30
  }
31
31
 
32
32
  export const cookies = (): Map<string, { name: string; value: string }> => {
33
- if (getEnv().side === "server") {
34
- const req = requestStorage?.getStore();
35
- if (!req) return new Map();
36
- return parseCookieHeader(req.headers.get("cookie") ?? "");
37
- }
33
+ if (getEnv().side === "server") return serverCookies();
38
34
  return parseCookieHeader(document.cookie);
39
35
  };
40
36
 
@@ -75,13 +71,7 @@ export const removeCookie = (key: string, options: { path: string } = { path: "/
75
71
  };
76
72
  export const headers = (): Map<string, string> => {
77
73
  if (getEnv().side !== "server") return new Map();
78
- const req = requestStorage?.getStore();
79
- if (!req) return new Map();
80
- const map = new Map<string, string>();
81
- req.headers.forEach((value, key) => {
82
- map.set(key, value);
83
- });
84
- return map;
74
+ return serverHeaders();
85
75
  };
86
76
 
87
77
  export const getHeader = (key: string): string | undefined => {
package/client/router.ts CHANGED
@@ -35,12 +35,18 @@ interface CSRClientRouterOption extends RouterOptions {
35
35
  router: RouterInstance;
36
36
  }
37
37
  export type RedirectMethod = "replace" | "push";
38
+ export type RedirectStatus = 303 | 307 | 308;
39
+ export interface RedirectOptions {
40
+ method?: RedirectMethod;
41
+ status?: RedirectStatus;
42
+ }
38
43
 
39
44
  export class AkanRedirectError extends Error {
40
45
  readonly digest = "AKAN_REDIRECT";
41
46
  constructor(
42
47
  readonly location: string,
43
48
  readonly method: RedirectMethod = "replace",
49
+ readonly status: RedirectStatus = 307,
44
50
  ) {
45
51
  super(`Redirect to ${location}`);
46
52
  this.name = "AkanRedirectError";
@@ -246,7 +252,9 @@ class Router {
246
252
  this.#instance.refresh();
247
253
  return undefined as never;
248
254
  }
249
- redirect(href: string): never {
255
+ redirect(href: string, options: RedirectOptions = {}): never {
256
+ const method = options.method ?? "replace";
257
+ const status = options.status ?? 307;
250
258
  if (getEnv().side === "server") {
251
259
  const { getRequest, headers: requestHeaders } = getServerRequestContext();
252
260
  const h = requestHeaders();
@@ -257,9 +265,9 @@ class Router {
257
265
  const basePath = getServerBasePath(reqPathname, lang, h.get("x-base-path") ?? undefined, this.#prefix);
258
266
  const { pathname, href: fullHref } = getPathInfo(href, lang, shouldExposeBasePath() ? basePath : "");
259
267
  Logger.log(`redirect to:${pathname}`);
260
- throw new AkanRedirectError(fullHref, "replace");
268
+ throw new AkanRedirectError(fullHref, method, status);
261
269
  } else {
262
- this.#instance.replace(href);
270
+ this.#instance[method](href);
263
271
  }
264
272
  return undefined as never;
265
273
  }
@@ -1,15 +1,35 @@
1
- export interface RequestStorage {
2
- run<T>(store: Request, callback: () => T): T;
3
- getStore(): Request | undefined;
1
+ export type AkanTheme = "css" | "system" | (string & {});
2
+
3
+ export interface AkanRequestPolicy {
4
+ routeId?: string;
5
+ rscCache?: "public" | false;
6
+ rscCacheTtl?: number;
7
+ cacheable?: boolean;
8
+ revalidate?: number | false;
9
+ tags: Set<string>;
4
10
  }
5
11
 
6
- export type AkanTheme = "css" | "system" | (string & {});
12
+ export interface AkanDynamicUsage {
13
+ headers: boolean;
14
+ cookies: boolean;
15
+ }
16
+
17
+ export interface AkanRequestStore {
18
+ request: Request;
19
+ theme?: AkanTheme;
20
+ queryCache: Map<string, Promise<unknown>>;
21
+ policy: AkanRequestPolicy;
22
+ dynamicUsage: AkanDynamicUsage;
23
+ }
24
+
25
+ export interface RequestStorage {
26
+ run<T>(store: Request | AkanRequestStore, callback: () => T): T;
27
+ getStore(): AkanRequestStore | undefined;
28
+ }
7
29
 
8
30
  declare global {
9
31
  var __AKAN_REQUEST_STORAGE__: RequestStorage | undefined;
10
- var __AKAN_REQUEST_THEME__: WeakMap<Request, AkanTheme> | undefined;
11
- var __AKAN_REQUEST_QUERY_CACHE__: WeakMap<Request, Map<string, Promise<unknown>>> | undefined;
12
- var __AKAN_REQUEST_FALLBACK_STACK__: Request[] | undefined;
32
+ var __AKAN_REQUEST_FALLBACK_STACK__: AkanRequestStore[] | undefined;
13
33
  }
14
34
 
15
35
  let _requestStorage: RequestStorage | null = null;
@@ -17,74 +37,120 @@ if (typeof window === "undefined") {
17
37
  try {
18
38
 
19
39
  const { AsyncLocalStorage } = require("node:async_hooks") as typeof import("node:async_hooks");
20
- globalThis.__AKAN_REQUEST_STORAGE__ ??= new AsyncLocalStorage() as RequestStorage;
40
+ const als = new AsyncLocalStorage<AkanRequestStore>();
41
+ globalThis.__AKAN_REQUEST_STORAGE__ ??= {
42
+ run<T>(store: Request | AkanRequestStore, callback: () => T): T {
43
+ return als.run(normalizeRequestStore(store), callback);
44
+ },
45
+ getStore(): AkanRequestStore | undefined {
46
+ return als.getStore();
47
+ },
48
+ };
21
49
  _requestStorage = globalThis.__AKAN_REQUEST_STORAGE__;
22
50
  } catch {}
23
51
  }
24
52
 
25
53
  export const requestStorage: RequestStorage | null = _requestStorage;
26
54
 
27
- function requestThemeMap(): WeakMap<Request, AkanTheme> {
28
- globalThis.__AKAN_REQUEST_THEME__ ??= new WeakMap<Request, AkanTheme>();
29
- return globalThis.__AKAN_REQUEST_THEME__;
55
+ function createRequestPolicy(): AkanRequestPolicy {
56
+ return { tags: new Set() };
57
+ }
58
+
59
+ export function createRequestStore(
60
+ request: Request,
61
+ policy: Partial<Omit<AkanRequestPolicy, "tags">> = {},
62
+ ): AkanRequestStore {
63
+ return {
64
+ request,
65
+ queryCache: new Map(),
66
+ policy: { ...createRequestPolicy(), ...policy },
67
+ dynamicUsage: { headers: false, cookies: false },
68
+ };
69
+ }
70
+
71
+ function isRequestStore(store: Request | AkanRequestStore | undefined): store is AkanRequestStore {
72
+ return Boolean(store && typeof store === "object" && "request" in store && store.request instanceof Request);
73
+ }
74
+
75
+ function normalizeRequestStore(store: Request | AkanRequestStore): AkanRequestStore {
76
+ return isRequestStore(store) ? store : createRequestStore(store);
30
77
  }
31
78
 
32
- function requestQueryCacheMap(): WeakMap<Request, Map<string, Promise<unknown>>> {
33
- globalThis.__AKAN_REQUEST_QUERY_CACHE__ ??= new WeakMap<Request, Map<string, Promise<unknown>>>();
34
- return globalThis.__AKAN_REQUEST_QUERY_CACHE__;
79
+ function getActiveRequestStore(): AkanRequestStore | undefined {
80
+ const store = requestStorage?.getStore() as Request | AkanRequestStore | undefined;
81
+ if (store) return isRequestStore(store) ? store : createRequestStore(store);
82
+ return globalThis.__AKAN_REQUEST_FALLBACK_STACK__?.at(-1);
35
83
  }
36
84
 
37
85
  /** Stores theme preference on the active request when server rendering. */
38
86
  export function setRequestTheme(theme: AkanTheme | undefined): void {
39
- const req = getRequest();
40
- if (!req || theme === undefined) return;
41
- requestThemeMap().set(req, theme);
87
+ const store = getRequestStore();
88
+ if (!store || theme === undefined) return;
89
+ store.theme = theme;
42
90
  }
43
91
 
44
92
  export function getRequestTheme(): AkanTheme | undefined {
45
- const req = getRequest();
46
- if (!req) return undefined;
47
- return requestThemeMap().get(req);
93
+ return getRequestStore()?.theme;
48
94
  }
49
95
 
50
96
  export function pushRequestFallback(req: Request): () => void {
51
97
  globalThis.__AKAN_REQUEST_FALLBACK_STACK__ ??= [];
52
98
  const stack = globalThis.__AKAN_REQUEST_FALLBACK_STACK__;
53
- stack.push(req);
99
+ const store = createRequestStore(req);
100
+ stack.push(store);
54
101
  return () => {
55
- const index = stack.lastIndexOf(req);
102
+ const index = stack.lastIndexOf(store);
56
103
  if (index >= 0) stack.splice(index, 1);
57
104
  };
58
105
  }
59
106
 
107
+ /** Returns the active server request store from AsyncLocalStorage or the fallback stack. */
108
+ export function getRequestStore(): AkanRequestStore | undefined {
109
+ return getActiveRequestStore();
110
+ }
111
+
60
112
  /** Returns the active server request from AsyncLocalStorage or the fallback stack. */
61
113
  export function getRequest(): Request | undefined {
62
- return requestStorage?.getStore() ?? globalThis.__AKAN_REQUEST_FALLBACK_STACK__?.at(-1);
114
+ return getRequestStore()?.request;
115
+ }
116
+
117
+ export function getRequestPolicy(): AkanRequestPolicy | undefined {
118
+ return getRequestStore()?.policy;
119
+ }
120
+
121
+ export function updateRequestPolicy(
122
+ patch: Partial<Omit<AkanRequestPolicy, "tags">> & { tags?: Iterable<string> },
123
+ ): AkanRequestPolicy | undefined {
124
+ const policy = getRequestPolicy();
125
+ if (!policy) return undefined;
126
+ const { tags, ...rest } = patch;
127
+ Object.assign(policy, rest);
128
+ if (tags) for (const tag of tags) policy.tags.add(tag);
129
+ return policy;
130
+ }
131
+
132
+ export function getRequestDynamicUsage(): AkanDynamicUsage | undefined {
133
+ return getRequestStore()?.dynamicUsage;
63
134
  }
64
135
 
65
136
  /** Deduplicates a promise-producing query within the active request. */
66
137
  export function memoizeRequestQuery<T>(key: string, factory: () => Promise<T>): Promise<T> {
67
- const req = getRequest();
68
- if (!req) return factory();
69
- const cacheMap = requestQueryCacheMap();
70
- let cache = cacheMap.get(req);
71
- if (!cache) {
72
- cache = new Map<string, Promise<unknown>>();
73
- cacheMap.set(req, cache);
74
- }
75
- const existing = cache.get(key);
138
+ const store = getRequestStore();
139
+ if (!store) return factory();
140
+ const existing = store.queryCache.get(key);
76
141
  if (existing) return existing as Promise<T>;
77
142
  const promise = factory();
78
- cache.set(key, promise);
143
+ store.queryCache.set(key, promise);
79
144
  return promise;
80
145
  }
81
146
 
82
147
  /** Returns current request headers as a Map, or an empty Map outside a request. */
83
148
  export function headers(): Map<string, string> {
84
- const req = getRequest();
149
+ const store = getRequestStore();
85
150
  const map = new Map<string, string>();
86
- if (!req) return map;
87
- req.headers.forEach((value, key) => {
151
+ if (!store) return map;
152
+ store.dynamicUsage.headers = true;
153
+ store.request.headers.forEach((value, key) => {
88
154
  map.set(key, value);
89
155
  });
90
156
  return map;
@@ -121,7 +187,8 @@ export function parseCookieHeader(cookieHeader: string): Map<string, CookieEntry
121
187
 
122
188
  /** Returns parsed cookies from the current request, or an empty Map outside a request. */
123
189
  export function cookies(): Map<string, CookieEntry> {
124
- const req = getRequest();
125
- if (!req) return new Map();
126
- return parseCookieHeader(req.headers.get("cookie") ?? "");
190
+ const store = getRequestStore();
191
+ if (!store) return new Map();
192
+ store.dynamicUsage.cookies = true;
193
+ return parseCookieHeader(store.request.headers.get("cookie") ?? "");
127
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.2.10",
3
+ "version": "2.2.12",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -39,6 +39,19 @@ interface AkanAppPrepared {
39
39
  webProxyRunner: WebProxyRunner | null;
40
40
  }
41
41
 
42
+ export interface AkanServerConsoleInfo {
43
+ name: string;
44
+ status: AkanServer["status"];
45
+ serverMode: AkanServer["serverMode"];
46
+ env: Pick<BaseEnv, "appName" | "environment" | "operationMode" | "repoName" | "serveDomain" | "databaseMode">;
47
+ services: string[];
48
+ signals: string[];
49
+ adaptors: string[];
50
+ uses: string[];
51
+ serviceStages: string[][];
52
+ adaptorStages: string[][];
53
+ }
54
+
42
55
  export class AkanServer {
43
56
  status: "stopped" | "initializing" | "initialized" | "starting" | "running" | "stopping" = "stopped";
44
57
 
@@ -120,6 +133,29 @@ export class AkanServer {
120
133
  return this.#di.getAdaptor<T>(refName);
121
134
  }
122
135
 
136
+ inspectConsole(): AkanServerConsoleInfo {
137
+ this.#assertCanGet();
138
+ return {
139
+ name: this.name,
140
+ status: this.status,
141
+ serverMode: this.serverMode,
142
+ env: {
143
+ appName: this.env.appName,
144
+ environment: this.env.environment,
145
+ operationMode: this.env.operationMode,
146
+ repoName: this.env.repoName,
147
+ serveDomain: this.env.serveDomain,
148
+ databaseMode: this.env.databaseMode,
149
+ },
150
+ services: [...this.#di.registry.serviceCls.keys()].sort((a, b) => a.localeCompare(b)),
151
+ signals: [...this.#di.registry.serverSignalCls.keys()].sort((a, b) => a.localeCompare(b)),
152
+ adaptors: [...this.#di.registry.adaptorCls.keys()].sort((a, b) => a.localeCompare(b)),
153
+ uses: [...this.#di.registry.uses.keys()].sort((a, b) => a.localeCompare(b)),
154
+ serviceStages: this.#di.hierarchy.serviceStages.map((stage) => [...stage]),
155
+ adaptorStages: this.#di.hierarchy.adaptorStages.map((stage) => [...stage]),
156
+ };
157
+ }
158
+
123
159
  async init({ routes: initRoutes = true, web = true }: { routes?: boolean; web?: boolean } = {}) {
124
160
  if (this.status !== "stopped") throw new Error("AkanServer is not able to init. It is already running.");
125
161
  this.status = "initializing";
@@ -248,14 +284,14 @@ export class AkanServer {
248
284
  }
249
285
 
250
286
  async start({ listen, web = true }: { listen?: boolean; web?: boolean } = {}) {
251
- const isScriptCommand = process.env.AKAN_COMMAND_TYPE === "script";
252
- const shouldListen = (listen ?? !isScriptCommand) && this.serverMode !== "batch";
287
+ const isNoListenCommand = process.env.AKAN_COMMAND_TYPE === "script" || process.env.AKAN_COMMAND_TYPE === "console";
288
+ const shouldListen = (listen ?? !isNoListenCommand) && this.serverMode !== "batch";
253
289
  await this.init({ routes: shouldListen, web });
254
290
  if (!shouldListen) {
255
291
  const websocket = this.#di.getWebsocketAdaptor();
256
292
  if (websocket) SignalResolver.setLocalPublish((roomId, data) => this.#localPublish?.(roomId, data), websocket);
257
293
  this.status = "running";
258
- if (!isScriptCommand) {
294
+ if (!isNoListenCommand) {
259
295
  this.#startMetricsReporting();
260
296
  this.#di.registerSchedule(this.serverMode);
261
297
  process.on("message", (message) => this.#handleIpcMessage(message as AkanIpcMessage));
@@ -0,0 +1,189 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { inspect } from "node:util";
3
+ import type { BaseEnv } from "akanjs/base";
4
+ import type { Adaptor, Service } from "akanjs/service";
5
+ import type { ServerSignal } from "akanjs/signal";
6
+ import type { AkanServer } from "./akanServer";
7
+
8
+ type AsyncFunctionConstructor = new (...args: string[]) => (scope: object) => Promise<unknown>;
9
+
10
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as AsyncFunctionConstructor;
11
+
12
+ export interface AkanConsoleOptions {
13
+ prompt?: string;
14
+ globals?: Record<string, unknown>;
15
+ input?: typeof process.stdin;
16
+ output?: typeof process.stdout;
17
+ }
18
+
19
+ export interface AkanConsoleContext extends Record<string, unknown> {
20
+ server: AkanServer;
21
+ env: AkanServer["env"];
22
+ get: AkanServer["get"];
23
+ service: <T = Service>(refName: string) => T;
24
+ signal: <T = ServerSignal>(refName: string) => T;
25
+ adaptor: <T = Adaptor>(refName: string) => T;
26
+ methods: (value: unknown) => string[];
27
+ debug: () => ReturnType<AkanServer["inspectConsole"]>;
28
+ }
29
+
30
+ export const assertAkanConsoleAllowed = (
31
+ env: Pick<BaseEnv, "environment" | "operationMode"> = {
32
+ environment: (process.env.AKAN_PUBLIC_ENV ?? "debug") as BaseEnv["environment"],
33
+ operationMode: (process.env.AKAN_PUBLIC_OPERATION_MODE ?? "cloud") as BaseEnv["operationMode"],
34
+ },
35
+ ) => {
36
+ const isProductionLike =
37
+ env.environment === "main" ||
38
+ env.operationMode === "cloud" ||
39
+ env.operationMode === "edge" ||
40
+ process.env.NODE_ENV === "production";
41
+ if (!isProductionLike || process.env.AKAN_CONSOLE === "1") return;
42
+
43
+ throw new Error(
44
+ [
45
+ "Akan console is disabled for production-like environments.",
46
+ "Run with AKAN_CONSOLE=1 only for the exec command that opens the console.",
47
+ "Example: AKAN_CONSOLE=1 bun console.js",
48
+ ].join("\n"),
49
+ );
50
+ };
51
+
52
+ export const getAkanConsoleMethods = (value: unknown): string[] => {
53
+ const names = new Set<string>();
54
+ let proto =
55
+ typeof value === "function"
56
+ ? value.prototype
57
+ : value && (typeof value === "object" || typeof value === "function")
58
+ ? Object.getPrototypeOf(value)
59
+ : null;
60
+
61
+ while (proto && proto !== Object.prototype) {
62
+ for (const name of Object.getOwnPropertyNames(proto)) {
63
+ if (name === "constructor") continue;
64
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
65
+ if (typeof descriptor?.value === "function") names.add(name);
66
+ }
67
+ proto = Object.getPrototypeOf(proto);
68
+ }
69
+
70
+ return [...names].sort((a, b) => a.localeCompare(b));
71
+ };
72
+
73
+ export const createAkanConsoleContext = (
74
+ server: AkanServer,
75
+ globals: Record<string, unknown> = {},
76
+ ): AkanConsoleContext => {
77
+ const context = {
78
+ server,
79
+ env: server.env,
80
+ get: server.get.bind(server) as AkanServer["get"],
81
+ service: server.getService.bind(server),
82
+ signal: server.getSignal.bind(server),
83
+ adaptor: server.getAdaptor.bind(server),
84
+ methods: getAkanConsoleMethods,
85
+ debug: () => server.inspectConsole(),
86
+ ...globals,
87
+ };
88
+ return context;
89
+ };
90
+
91
+ const createScope = (context: Record<string, unknown>) =>
92
+ new Proxy(context, {
93
+ has: () => true,
94
+ get(target, prop) {
95
+ if (prop === Symbol.unscopables) return undefined;
96
+ if (prop in target) return target[prop as keyof typeof target];
97
+ return (globalThis as Record<PropertyKey, unknown>)[prop];
98
+ },
99
+ set(target, prop, value) {
100
+ target[prop as keyof typeof target] = value;
101
+ return true;
102
+ },
103
+ });
104
+
105
+ export const evaluateAkanConsoleInput = async (source: string, context: Record<string, unknown>) => {
106
+ const trimmed = source.trim();
107
+ if (!trimmed) return undefined;
108
+ const scope = createScope(context);
109
+
110
+ try {
111
+ return await new AsyncFunction("scope", `with (scope) { return await (${trimmed}); }`)(scope);
112
+ } catch (error) {
113
+ if (!(error instanceof SyntaxError)) throw error;
114
+ return await new AsyncFunction("scope", `with (scope) { return await (async () => {\n${trimmed}\n})(); }`)(scope);
115
+ }
116
+ };
117
+
118
+ const printHelp = (output: typeof process.stdout) => {
119
+ output.write(
120
+ [
121
+ "Akan console commands:",
122
+ " .help Show this help",
123
+ " .globals Show available global names",
124
+ " .exit Close the console",
125
+ "",
126
+ "Examples:",
127
+ " debug()",
128
+ ' methods(service("user"))',
129
+ ' await service("user").__count()',
130
+ ' userService = service("user")',
131
+ "",
132
+ ].join("\n"),
133
+ );
134
+ };
135
+
136
+ const formatValue = (value: unknown) => {
137
+ if (value === undefined) return "";
138
+ return `${inspect(value, { colors: true, depth: 5, maxArrayLength: 100 })}\n`;
139
+ };
140
+
141
+ export const startAkanConsole = async (server: AkanServer, options: AkanConsoleOptions = {}) => {
142
+ const input = options.input ?? process.stdin;
143
+ const output = options.output ?? process.stdout;
144
+ const context = createAkanConsoleContext(server, options.globals);
145
+ const prompt = options.prompt ?? `akan:${server.name}> `;
146
+ const rl = createInterface({ input, output, terminal: true });
147
+
148
+ output.write(`Akan console started for ${server.name}. Type .help for commands.\n`);
149
+ rl.setPrompt(prompt);
150
+ rl.prompt();
151
+
152
+ rl.on("SIGINT", () => {
153
+ output.write("\n");
154
+ rl.close();
155
+ });
156
+
157
+ for await (const line of rl) {
158
+ const trimmed = line.trim();
159
+ try {
160
+ if (!trimmed) {
161
+ rl.prompt();
162
+ continue;
163
+ }
164
+ if (trimmed === ".exit" || trimmed === ".quit") {
165
+ rl.close();
166
+ break;
167
+ }
168
+ if (trimmed === ".help") {
169
+ printHelp(output);
170
+ rl.prompt();
171
+ continue;
172
+ }
173
+ if (trimmed === ".globals") {
174
+ output.write(
175
+ `${Object.keys(context)
176
+ .sort((a, b) => a.localeCompare(b))
177
+ .join(", ")}\n`,
178
+ );
179
+ rl.prompt();
180
+ continue;
181
+ }
182
+
183
+ output.write(formatValue(await evaluateAkanConsoleInput(line, context)));
184
+ } catch (error) {
185
+ output.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
186
+ }
187
+ rl.prompt();
188
+ }
189
+ };
@@ -45,11 +45,11 @@ export const HMR_CLIENT_SCRIPT = `(function(){
45
45
  return;
46
46
  }
47
47
  if (msg.type === "rsc-refresh") {
48
- reloadForHmr(msg);
48
+ refreshRsc(msg);
49
49
  return;
50
50
  }
51
51
  if (msg.type === "client-refresh") {
52
- reloadForHmr(msg);
52
+ refreshClient(msg);
53
53
  return;
54
54
  }
55
55
  if (msg.type === "css-update") {
@@ -73,12 +73,6 @@ export const HMR_CLIENT_SCRIPT = `(function(){
73
73
  setTimeout(connect, delay);
74
74
  }
75
75
 
76
- function reloadForHmr(msg){
77
- try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}
78
- if (msg && msg.buildId != null) lastBuildId = msg.buildId;
79
- location.reload();
80
- }
81
-
82
76
  function ensureOverlay(){
83
77
  if (overlayEl && overlayLabelEl) return overlayEl;
84
78
  if (!overlayStyleEl) {
package/server/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./akanLib";
3
3
  export * from "./akanOption";
4
4
  export * from "./akanServer";
5
5
  export * from "./artifact";
6
+ export * from "./console";
6
7
  export * from "./decorators";
7
8
  export type { ChangeBatch, ChangeKind } from "./hmr/changeBatch";
8
9
  export * from "./processMetricsCollector";
@@ -1,11 +1,14 @@
1
1
  import { createElement, type ReactNode, startTransition, use, useLayoutEffect, useState } from "react";
2
2
  import { hydrateRoot } from "react-dom/client";
3
3
  import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
4
+ import { isRscPayloadResponse } from "./rscHttp";
5
+
6
+ type InlineRscChunk = [1, string] | [3, string];
4
7
 
5
8
  declare global {
6
- var __RSC_CHUNKS__: string[] | undefined;
9
+ var __RSC_CHUNKS__: InlineRscChunk[] | undefined;
7
10
  var __RSC_CLOSED__: boolean | undefined;
8
- var __RSC_PUSH__: ((b64: string) => void) | undefined;
11
+ var __RSC_PUSH__: ((type: InlineRscChunk[0], data: string) => void) | undefined;
9
12
  var __RSC_CLOSE__: (() => void) | undefined;
10
13
  var __AKAN_RSC_NAVIGATE__:
11
14
  | ((href: string, options?: { replace?: boolean; scrollToTop?: boolean }) => Promise<void>)
@@ -21,15 +24,20 @@ function decodeBase64(b64: string): Uint8Array {
21
24
  return bytes;
22
25
  }
23
26
 
27
+ function decodeInlineRscChunk([type, data]: InlineRscChunk): Uint8Array {
28
+ if (type === 1) return new TextEncoder().encode(data);
29
+ return decodeBase64(data);
30
+ }
31
+
24
32
  type RscThenable = Promise<ReactNode>;
25
- type RscFetchResult = { type: "rsc"; thenable: RscThenable } | { type: "redirected" };
33
+ type RscFetchResult = { type: "rsc"; thenable: RscThenable } | { type: "redirected"; status?: number };
26
34
  const MAX_RSC_CACHE_ENTRIES = 32;
27
35
 
28
36
  function createInitialRscStream(): ReadableStream<Uint8Array> {
29
37
  return new ReadableStream<Uint8Array>({
30
38
  start(controller) {
31
39
  const queued = globalThis.__RSC_CHUNKS__ ?? [];
32
- for (const b64 of queued) controller.enqueue(decodeBase64(b64));
40
+ for (const chunk of queued) controller.enqueue(decodeInlineRscChunk(chunk));
33
41
  globalThis.__RSC_CHUNKS__ = [];
34
42
 
35
43
  if (globalThis.__RSC_CLOSED__) {
@@ -37,7 +45,7 @@ function createInitialRscStream(): ReadableStream<Uint8Array> {
37
45
  return;
38
46
  }
39
47
 
40
- globalThis.__RSC_PUSH__ = (b64: string) => controller.enqueue(decodeBase64(b64));
48
+ globalThis.__RSC_PUSH__ = (type, data) => controller.enqueue(decodeInlineRscChunk([type, data]));
41
49
  globalThis.__RSC_CLOSE__ = () => controller.close();
42
50
  },
43
51
  });
@@ -63,10 +71,12 @@ async function fetchRsc(href: string, options: { buildId?: number } = {}): Promi
63
71
  const redirect = res.headers.get("X-Akan-Redirect");
64
72
  if (redirect) {
65
73
  const method = res.headers.get("X-Akan-Redirect-Method");
74
+ const statusHeader = res.headers.get("X-Akan-Redirect-Status");
75
+ const status = statusHeader ? Number(statusHeader) : undefined;
66
76
  await globalThis.__AKAN_RSC_NAVIGATE__?.(redirect, { replace: method !== "push", scrollToTop: true });
67
- return { type: "redirected" };
77
+ return { type: "redirected", status };
68
78
  }
69
- if (!res.ok || !res.body) throw new Error(`[rscClient] RSC fetch failed ${res.status} ${res.statusText}`);
79
+ if (!isRscPayloadResponse(res)) throw new Error(`[rscClient] RSC fetch failed ${res.status} ${res.statusText}`);
70
80
 
71
81
  const buffer = await res.arrayBuffer();
72
82
  const completeStream = new ReadableStream<Uint8Array>({