akanjs 2.2.11 → 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,12 @@
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
+
3
10
  ## 2.2.11
4
11
 
5
12
  ### 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.11",
3
+ "version": "2.2.12",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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) {
@@ -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>({
@@ -0,0 +1,7 @@
1
+ export const RSC_CONTENT_TYPE = "text/x-component; charset=utf-8";
2
+
3
+ export function isRscPayloadResponse(res: Response): boolean {
4
+ if (!res.body) return false;
5
+ if (res.ok) return true;
6
+ return res.status === 404 && (res.headers.get("Content-Type") ?? "").toLowerCase().startsWith("text/x-component");
7
+ }
@@ -1,6 +1,12 @@
1
- import type { AkanNotFoundError, AkanRedirectError, LayoutFallbackRoute, PathRoute } from "akanjs/client";
1
+ import type {
2
+ AkanNotFoundError,
3
+ AkanRedirectError,
4
+ LayoutFallbackRoute,
5
+ PathRoute,
6
+ RedirectStatus,
7
+ } from "akanjs/client";
2
8
  import { type AkanI18nConfig, DEFAULT_AKAN_I18N, getBasePathFromPathname, Logger } from "akanjs/common";
3
- import { cookies, getRequest, getRequestTheme, requestStorage } from "akanjs/fetch";
9
+ import { cookies, getRequest, getRequestTheme, requestStorage, updateRequestPolicy } from "akanjs/fetch";
4
10
  import type { ReactNode } from "react";
5
11
  import { renderToReadableStream } from "react-server-dom-webpack/server.node";
6
12
  import type { ClientManifest } from "./artifact";
@@ -40,7 +46,7 @@ interface UpdateCssAssetsMsg {
40
46
  }
41
47
  type InMsg = InitMsg | RenderMsg | ReloadMsg | UpdateCssAssetsMsg;
42
48
  type RenderControl =
43
- | { type: "redirect"; location: string; method: "replace" | "push" }
49
+ | { type: "redirect"; location: string; method: "replace" | "push"; status: RedirectStatus }
44
50
  | { type: "not-found" }
45
51
  | { type: "error"; error: unknown };
46
52
 
@@ -82,7 +88,9 @@ export function isAkanRedirectError(error: unknown): error is AkanRedirectError
82
88
  "digest" in error &&
83
89
  (error as { digest?: unknown }).digest === "AKAN_REDIRECT" &&
84
90
  "location" in error &&
85
- typeof (error as { location?: unknown }).location === "string"
91
+ typeof (error as { location?: unknown }).location === "string" &&
92
+ "status" in error &&
93
+ typeof (error as { status?: unknown }).status === "number"
86
94
  );
87
95
  }
88
96
 
@@ -272,6 +280,7 @@ class RscRenderer {
272
280
  const match = RouteTreeBuilder.match(urlObj.pathname, this.#pathRoutes);
273
281
  activeRoute.match = match;
274
282
  const routeId = match?.pathRoute.path ?? "__not_found__";
283
+ updateRequestPolicy({ routeId });
275
284
  this.#stats.lastRenderRouteId = routeId;
276
285
  this.#stats.lastRenderKind = match ? "route" : "not-found";
277
286
  if (match)
@@ -365,7 +374,13 @@ class RscRenderer {
365
374
  if (isAkanRedirectError(error)) {
366
375
  this.#stats.lastRenderKind = "redirect";
367
376
  this.#logger.verbose(`render[${requestId}] redirect ${error.location}`);
368
- this.#send({ type: "redirect", requestId, location: error.location, method: error.method });
377
+ this.#send({
378
+ type: "redirect",
379
+ requestId,
380
+ location: error.location,
381
+ method: error.method,
382
+ status: error.status,
383
+ });
369
384
  return;
370
385
  }
371
386
  if (isAkanNotFoundError(error)) {
@@ -474,7 +489,12 @@ class RscRenderer {
474
489
  const stream = await renderToReadableStream(element, clientManifest, {
475
490
  onError: (error) => {
476
491
  if (isAkanRedirectError(error)) {
477
- controlRef.current = { type: "redirect", location: error.location, method: error.method };
492
+ controlRef.current = {
493
+ type: "redirect",
494
+ location: error.location,
495
+ method: error.method,
496
+ status: error.status,
497
+ };
478
498
  return error.digest;
479
499
  }
480
500
  if (isAkanNotFoundError(error)) {
@@ -558,7 +578,13 @@ class RscRenderer {
558
578
  #sendRenderControl(requestId: string, control: RenderControl): void {
559
579
  if (control.type === "redirect") {
560
580
  this.#logger.verbose(`render[${requestId}] redirect ${control.location}`);
561
- this.#send({ type: "redirect", requestId, location: control.location, method: control.method });
581
+ this.#send({
582
+ type: "redirect",
583
+ requestId,
584
+ location: control.location,
585
+ method: control.method,
586
+ status: control.status,
587
+ });
562
588
  return;
563
589
  }
564
590
  if (control.type === "error") {
@@ -594,6 +620,11 @@ class RscRenderer {
594
620
  async #getResultCacheKey(request: Request, url: URL, pathRoute: PathRoute): Promise<string | null> {
595
621
  const config = await pathRoute.renderPage.getPageConfig?.();
596
622
  const ttl = RscRenderer.#normalizeCacheTtl(config?.rscCacheTtl);
623
+ updateRequestPolicy({
624
+ rscCache: config?.rscCache,
625
+ rscCacheTtl: config?.rscCacheTtl,
626
+ cacheable: config?.rscCache === "public" || ttl !== null,
627
+ });
597
628
  if (config?.rscCache !== "public" && ttl === null) {
598
629
  this.#resultCacheBypass += 1;
599
630
  return null;
@@ -12,15 +12,16 @@ interface RscPending {
12
12
  onEnd: () => void;
13
13
  onError: (message: string) => void;
14
14
  onMeta?: (meta: { theme?: AkanTheme; status?: number }) => void;
15
- onRedirect?: (location: string, method: RscRedirectMethod) => void;
15
+ onRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
16
16
  onNotFound?: () => void;
17
17
  }
18
18
 
19
19
  export type RscRedirectMethod = "replace" | "push";
20
+ export type RscRedirectStatus = 303 | 307 | 308;
20
21
 
21
22
  export type RscRenderResult =
22
23
  | { type: "stream"; stream: ReadableStream<Uint8Array>; theme?: AkanTheme; status?: number }
23
- | { type: "redirect"; location: string; method: RscRedirectMethod }
24
+ | { type: "redirect"; location: string; method: RscRedirectMethod; status: RscRedirectStatus }
24
25
  | { type: "not-found" };
25
26
 
26
27
  type RscInMsg =
@@ -30,7 +31,7 @@ type RscInMsg =
30
31
  | { type: "meta"; requestId: string; theme?: AkanTheme; status?: number }
31
32
  | { type: "chunk"; requestId: string; data: Uint8Array }
32
33
  | { type: "end"; requestId: string }
33
- | { type: "redirect"; requestId: string; location: string; method?: RscRedirectMethod }
34
+ | { type: "redirect"; requestId: string; location: string; method?: RscRedirectMethod; status?: RscRedirectStatus }
34
35
  | { type: "not-found"; requestId: string }
35
36
  | { type: "metrics"; metrics: AkanMetricsReport }
36
37
  | { type: "error"; requestId: string; message: string; buildId?: number };
@@ -211,10 +212,10 @@ export class RscWorker {
211
212
  }
212
213
  controller.error(new Error(msg));
213
214
  },
214
- onRedirect: (location, method) => {
215
+ onRedirect: (location, method, status) => {
215
216
  if (!settled) {
216
217
  settled = true;
217
- resolve({ type: "redirect", location, method });
218
+ resolve({ type: "redirect", location, method, status });
218
219
  controller.close();
219
220
  return;
220
221
  }
@@ -406,7 +407,9 @@ export class RscWorker {
406
407
  this.#resolvePending(message.requestId, (p) => p.onEnd());
407
408
  return;
408
409
  case "redirect":
409
- this.#resolvePending(message.requestId, (p) => p.onRedirect?.(message.location, message.method ?? "replace"));
410
+ this.#resolvePending(message.requestId, (p) =>
411
+ p.onRedirect?.(message.location, message.method ?? "replace", message.status ?? 307),
412
+ );
410
413
  return;
411
414
  case "not-found":
412
415
  this.#resolvePending(message.requestId, (p) => p.onNotFound?.());
@@ -5,11 +5,115 @@ import { renderToReadableStream } from "react-dom/server.browser";
5
5
  import { createFromNodeStream } from "react-server-dom-webpack/client.node";
6
6
  import type { SsrChunkRegistryStats, SsrFromRscInput } from "./ssrTypes";
7
7
 
8
+ const DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES = 1024;
9
+
10
+ interface SsrChunkRegistryEntry<T> {
11
+ keys: Set<string>;
12
+ lruKey: string;
13
+ value: T;
14
+ }
15
+
16
+ export class SsrChunkRegistry<T> {
17
+ readonly #entriesByKey = new Map<string, SsrChunkRegistryEntry<T>>();
18
+ readonly #lru = new Map<string, SsrChunkRegistryEntry<T>>();
19
+ #evictionCount = 0;
20
+
21
+ constructor(readonly maxEntries = DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES) {}
22
+
23
+ get size(): number {
24
+ return this.#entriesByKey.size;
25
+ }
26
+
27
+ get evictionCount(): number {
28
+ return this.#evictionCount;
29
+ }
30
+
31
+ get(key: string): T | undefined {
32
+ const entry = this.#entriesByKey.get(key);
33
+ if (!entry) return undefined;
34
+ this.#touch(entry);
35
+ return entry.value;
36
+ }
37
+
38
+ set(keys: string[], value: T): void {
39
+ const uniqueKeys = [...new Set(keys)].filter(Boolean);
40
+ if (uniqueKeys.length === 0) return;
41
+
42
+ let entry = uniqueKeys
43
+ .map((key) => this.#entriesByKey.get(key))
44
+ .find((item): item is SsrChunkRegistryEntry<T> => Boolean(item));
45
+ if (!entry) {
46
+ entry = { keys: new Set(), lruKey: uniqueKeys[0] as string, value };
47
+ }
48
+ entry.value = value;
49
+
50
+ for (const key of uniqueKeys) {
51
+ const existing = this.#entriesByKey.get(key);
52
+ if (existing && existing !== entry) {
53
+ existing.keys.delete(key);
54
+ if (existing.keys.size === 0) this.#lru.delete(existing.lruKey);
55
+ }
56
+ entry.keys.add(key);
57
+ this.#entriesByKey.set(key, entry);
58
+ }
59
+
60
+ this.#touch(entry);
61
+ this.#evict(entry);
62
+ }
63
+
64
+ #touch(entry: SsrChunkRegistryEntry<T>): void {
65
+ this.#lru.delete(entry.lruKey);
66
+ this.#lru.set(entry.lruKey, entry);
67
+ }
68
+
69
+ #evict(protectedEntry: SsrChunkRegistryEntry<T>): void {
70
+ const maxEntries = this.maxEntries > 0 ? this.maxEntries : DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES;
71
+ while (this.#entriesByKey.size > maxEntries) {
72
+ const oldest = this.#lru.entries().next().value as [string, SsrChunkRegistryEntry<T>] | undefined;
73
+ if (!oldest) return;
74
+ const [lruKey, entry] = oldest;
75
+ if (entry === protectedEntry && this.#lru.size === 1) return;
76
+ if (entry === protectedEntry) {
77
+ this.#touch(entry);
78
+ continue;
79
+ }
80
+ this.#lru.delete(lruKey);
81
+ for (const key of entry.keys) this.#entriesByKey.delete(key);
82
+ this.#evictionCount += 1;
83
+ }
84
+ }
85
+ }
86
+
87
+ export type InlineRscChunk = readonly [1, string] | readonly [3, string];
88
+
89
+ export function encodeInlineRscChunk(chunk: Uint8Array): InlineRscChunk {
90
+ try {
91
+ return [1, new TextDecoder("utf-8", { fatal: true }).decode(chunk)];
92
+ } catch {
93
+ return [3, Buffer.from(chunk).toString("base64")];
94
+ }
95
+ }
96
+
97
+ export function htmlEscapeJsonString(value: string): string {
98
+ return JSON.stringify(value)
99
+ .replace(/</g, "\\u003c")
100
+ .replace(/>/g, "\\u003e")
101
+ .replace(/&/g, "\\u0026")
102
+ .replace(/\u2028/g, "\\u2028")
103
+ .replace(/\u2029/g, "\\u2029");
104
+ }
105
+
106
+ export function createInlineRscScript(chunk: Uint8Array): string {
107
+ const [type, data] = encodeInlineRscChunk(chunk);
108
+ return `<script>self.__RSC_PUSH__(${type},${htmlEscapeJsonString(data)})</script>`;
109
+ }
110
+
8
111
  export class SsrFromRscRenderer {
9
112
  static readonly #chunkRegistryStats: SsrChunkRegistryStats = {
10
113
  ssrChunkRegistrySize: 0,
11
114
  ssrChunkLoadCount: 0,
12
115
  ssrChunkCacheHitCount: 0,
116
+ ssrChunkEvictionCount: 0,
13
117
  };
14
118
 
15
119
  static readonly #clientBootstrap = `(function(){
@@ -31,7 +135,7 @@ export class SsrFromRscRenderer {
31
135
  self.__webpack_get_script_filename__ = function(chunkId) { return chunkId; };
32
136
  self.__RSC_CHUNKS__ = [];
33
137
  self.__RSC_CLOSED__ = false;
34
- self.__RSC_PUSH__ = function(b64){ self.__RSC_CHUNKS__.push(b64); };
138
+ self.__RSC_PUSH__ = function(type,data){ self.__RSC_CHUNKS__.push([type,data]); };
35
139
  self.__RSC_CLOSE__ = function(){ self.__RSC_CLOSED__ = true; };
36
140
  })();`;
37
141
 
@@ -98,18 +202,18 @@ export class SsrFromRscRenderer {
98
202
  if (g.__rsc_ssr_shims_installed__) return;
99
203
  g.__rsc_ssr_shims_installed__ = true;
100
204
 
101
- const registry = new Map<string, Record<string, unknown>>();
205
+ const registry = new SsrChunkRegistry<Record<string, unknown>>(SsrFromRscRenderer.#getSsrChunkRegistryMaxEntries());
102
206
  g.__webpack_chunk_load__ = async (chunkId: string) => {
103
- if (registry.has(chunkId)) {
207
+ if (registry.get(chunkId)) {
104
208
  SsrFromRscRenderer.#chunkRegistryStats.ssrChunkCacheHitCount += 1;
105
209
  return;
106
210
  }
107
211
  const mod = (await import(chunkId)) as Record<string, unknown>;
108
- registry.set(chunkId, mod);
109
212
  const canonical = chunkId.replace(/\?v=\d+$/, "");
110
- registry.set(canonical, mod);
213
+ registry.set([chunkId, canonical], mod);
111
214
  SsrFromRscRenderer.#chunkRegistryStats.ssrChunkLoadCount += 1;
112
215
  SsrFromRscRenderer.#chunkRegistryStats.ssrChunkRegistrySize = registry.size;
216
+ SsrFromRscRenderer.#chunkRegistryStats.ssrChunkEvictionCount = registry.evictionCount;
113
217
  };
114
218
  g.__webpack_require__ = (id: string) => {
115
219
  const mod = registry.get(id);
@@ -120,6 +224,11 @@ export class SsrFromRscRenderer {
120
224
  };
121
225
  }
122
226
 
227
+ static #getSsrChunkRegistryMaxEntries(): number {
228
+ const parsed = Number.parseInt(process.env.AKAN_SSR_CHUNK_REGISTRY_MAX_ENTRIES ?? "", 10);
229
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES;
230
+ }
231
+
123
232
  /**
124
233
  * Splice bootstrap-only head scripts immediately after the `<head>` opening
125
234
  * tag in the outgoing HTML stream.
@@ -281,8 +390,7 @@ export class SsrFromRscRenderer {
281
390
  const { value, done } = await rscReader.read();
282
391
  if (done) break;
283
392
  if (errored) return;
284
- const b64 = Buffer.from(value).toString("base64");
285
- controller.enqueue(encoder.encode(`<script>self.__RSC_PUSH__(${JSON.stringify(b64)})</script>`));
393
+ controller.enqueue(encoder.encode(createInlineRscScript(value)));
286
394
  }
287
395
  } finally {
288
396
  rscReader.releaseLock();
@@ -16,6 +16,7 @@ export interface SsrChunkRegistryStats {
16
16
  ssrChunkRegistrySize: number;
17
17
  ssrChunkLoadCount: number;
18
18
  ssrChunkCacheHitCount: number;
19
+ ssrChunkEvictionCount: number;
19
20
  }
20
21
 
21
22
  export interface SsrFromRscInput {
@@ -22,7 +22,7 @@ import { HMR_CLIENT_SCRIPT } from "./hmr/clientScript";
22
22
  import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
23
23
  import { ImageOptimizer } from "./imageOptimizer";
24
24
  import { createDefaultRobotsTxt } from "./robots";
25
- import { RscWorker } from "./rscWorkerHost";
25
+ import { type RscRedirectMethod, type RscRedirectStatus, RscWorker } from "./rscWorkerHost";
26
26
  import { createDefaultSitemapXml, getSitemapBasePath } from "./sitemap";
27
27
  import { SsrFromRscRenderer } from "./ssrFromRscRenderer";
28
28
  import { createSystemPageResponse, getSystemPageHomeHref } from "./systemPages";
@@ -30,6 +30,33 @@ import type { BaseBuildArtifact, HttpRoutes, RenderState } from "./types";
30
30
 
31
31
  const RESERVED_BASE_PATHS = new Set(["admin"]);
32
32
 
33
+ export function createRscRedirectResponse(
34
+ location: string,
35
+ method: RscRedirectMethod,
36
+ status: RscRedirectStatus = 307,
37
+ ): Response {
38
+ return new Response(JSON.stringify({ type: "redirect", location, method, status }), {
39
+ status: 200,
40
+ headers: {
41
+ "Content-Type": "application/json; charset=utf-8",
42
+ "Cache-Control": "no-store",
43
+ "X-Akan-Redirect": location,
44
+ "X-Akan-Redirect-Method": method,
45
+ "X-Akan-Redirect-Status": String(status),
46
+ },
47
+ });
48
+ }
49
+
50
+ export function createRscStreamResponse(stream: BodyInit, status = 200): Response {
51
+ return new Response(stream, {
52
+ status,
53
+ headers: {
54
+ "Content-Type": "text/x-component; charset=utf-8",
55
+ "Cache-Control": "no-store",
56
+ },
57
+ });
58
+ }
59
+
33
60
  export function normalizeRscTargetUrlForHostBasePath(
34
61
  targetUrl: URL,
35
62
  options: {
@@ -262,17 +289,12 @@ export class WebRouter {
262
289
  const result = await this.#rsc.renderWithMeta(rscReq, {
263
290
  clientManifest: manifest.clientManifest,
264
291
  });
265
- if (result.type === "redirect") return WebRouter.#rscRedirectResponse(result.location, result.method);
266
- if (result.type === "not-found") return WebRouter.#rscRedirectResponse("/404", "replace");
267
- if (result.status === 404) return WebRouter.#rscRedirectResponse("/404", "replace");
292
+ if (result.type === "redirect")
293
+ return createRscRedirectResponse(result.location, result.method, result.status);
294
+ if (result.type === "not-found") return WebRouter.#rscNotFoundResponse();
268
295
  if (result.status && result.status >= 500)
269
296
  return this.#renderRscErrorResponse("__rsc", "Internal Server Error");
270
- return new Response(result.stream, {
271
- headers: {
272
- "Content-Type": "text/x-component; charset=utf-8",
273
- "Cache-Control": "no-store",
274
- },
275
- });
297
+ return createRscStreamResponse(result.stream, result.status ?? 200);
276
298
  } catch (err) {
277
299
  return this.#renderRscErrorResponse("__rsc", err);
278
300
  }
@@ -368,7 +390,8 @@ export class WebRouter {
368
390
  const rscResult = await this.#rsc.renderWithMeta(req, {
369
391
  clientManifest: manifest.clientManifest,
370
392
  });
371
- if (rscResult.type === "redirect") return Response.redirect(new URL(rscResult.location, url.origin), 307);
393
+ if (rscResult.type === "redirect")
394
+ return Response.redirect(new URL(rscResult.location, url.origin), rscResult.status);
372
395
  if (rscResult.type === "not-found") return this.#renderNotFoundResponse(req, url);
373
396
  const themeCookieExists = WebRouter.#hasCookie(req, "theme");
374
397
  const htmlStream = await new SsrFromRscRenderer().render({
@@ -417,6 +440,7 @@ export class WebRouter {
417
440
  ssrChunkRegistrySize: ssrStats.ssrChunkRegistrySize,
418
441
  ssrChunkLoadCount: ssrStats.ssrChunkLoadCount,
419
442
  ssrChunkCacheHitCount: ssrStats.ssrChunkCacheHitCount,
443
+ ssrChunkEvictionCount: ssrStats.ssrChunkEvictionCount,
420
444
  httpFullSsrCount: this.#requestStats.fullSsr,
421
445
  httpRscNavigationCount: this.#requestStats.rscNavigation,
422
446
  httpStaticAssetCount: this.#requestStats.staticAsset,
@@ -650,15 +674,10 @@ export class WebRouter {
650
674
  if (!last || last.index === undefined) return `${html}\n${snippet}`;
651
675
  return `${html.slice(0, last.index)}${snippet}\n${html.slice(last.index)}`;
652
676
  }
653
- static #rscRedirectResponse(location: string, method: "replace" | "push") {
654
- return new Response(JSON.stringify({ type: "redirect", location, method }), {
655
- status: 200,
656
- headers: {
657
- "Content-Type": "application/json; charset=utf-8",
658
- "Cache-Control": "no-store",
659
- "X-Akan-Redirect": location,
660
- "X-Akan-Redirect-Method": method,
661
- },
677
+ static #rscNotFoundResponse(): Response {
678
+ return new Response("Not Found", {
679
+ status: 404,
680
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
662
681
  });
663
682
  }
664
683
  #getProductionRouteCache() {
@@ -85,6 +85,7 @@ export interface AkanMetricsReport {
85
85
  ssrChunkRegistrySize?: number;
86
86
  ssrChunkLoadCount?: number;
87
87
  ssrChunkCacheHitCount?: number;
88
+ ssrChunkEvictionCount?: number;
88
89
  httpFullSsrCount?: number;
89
90
  httpRscNavigationCount?: number;
90
91
  httpStaticAssetCount?: number;
@@ -25,11 +25,17 @@ interface CSRClientRouterOption extends RouterOptions {
25
25
  router: RouterInstance;
26
26
  }
27
27
  export type RedirectMethod = "replace" | "push";
28
+ export type RedirectStatus = 303 | 307 | 308;
29
+ export interface RedirectOptions {
30
+ method?: RedirectMethod;
31
+ status?: RedirectStatus;
32
+ }
28
33
  export declare class AkanRedirectError extends Error {
29
34
  readonly location: string;
30
35
  readonly method: RedirectMethod;
36
+ readonly status: RedirectStatus;
31
37
  readonly digest = "AKAN_REDIRECT";
32
- constructor(location: string, method?: RedirectMethod);
38
+ constructor(location: string, method?: RedirectMethod, status?: RedirectStatus);
33
39
  }
34
40
  export declare class AkanNotFoundError extends Error {
35
41
  readonly digest = "AKAN_NOT_FOUND";
@@ -53,7 +59,7 @@ declare class Router {
53
59
  replace(href: string, routeOptions?: RouteOptions): never;
54
60
  back(routeOptions?: RouteOptions): never;
55
61
  refresh(): never;
56
- redirect(href: string): never;
62
+ redirect(href: string, options?: RedirectOptions): never;
57
63
  notFound(): never;
58
64
  setLang(lang: string): never;
59
65
  getPath(pathname?: string): string;
@@ -1,21 +1,46 @@
1
+ export type AkanTheme = "css" | "system" | (string & {});
2
+ export interface AkanRequestPolicy {
3
+ routeId?: string;
4
+ rscCache?: "public" | false;
5
+ rscCacheTtl?: number;
6
+ cacheable?: boolean;
7
+ revalidate?: number | false;
8
+ tags: Set<string>;
9
+ }
10
+ export interface AkanDynamicUsage {
11
+ headers: boolean;
12
+ cookies: boolean;
13
+ }
14
+ export interface AkanRequestStore {
15
+ request: Request;
16
+ theme?: AkanTheme;
17
+ queryCache: Map<string, Promise<unknown>>;
18
+ policy: AkanRequestPolicy;
19
+ dynamicUsage: AkanDynamicUsage;
20
+ }
1
21
  export interface RequestStorage {
2
- run<T>(store: Request, callback: () => T): T;
3
- getStore(): Request | undefined;
22
+ run<T>(store: Request | AkanRequestStore, callback: () => T): T;
23
+ getStore(): AkanRequestStore | undefined;
4
24
  }
5
- export type AkanTheme = "css" | "system" | (string & {});
6
25
  declare global {
7
26
  var __AKAN_REQUEST_STORAGE__: RequestStorage | undefined;
8
- var __AKAN_REQUEST_THEME__: WeakMap<Request, AkanTheme> | undefined;
9
- var __AKAN_REQUEST_QUERY_CACHE__: WeakMap<Request, Map<string, Promise<unknown>>> | undefined;
10
- var __AKAN_REQUEST_FALLBACK_STACK__: Request[] | undefined;
27
+ var __AKAN_REQUEST_FALLBACK_STACK__: AkanRequestStore[] | undefined;
11
28
  }
12
29
  export declare const requestStorage: RequestStorage | null;
30
+ export declare function createRequestStore(request: Request, policy?: Partial<Omit<AkanRequestPolicy, "tags">>): AkanRequestStore;
13
31
  /** Stores theme preference on the active request when server rendering. */
14
32
  export declare function setRequestTheme(theme: AkanTheme | undefined): void;
15
33
  export declare function getRequestTheme(): AkanTheme | undefined;
16
34
  export declare function pushRequestFallback(req: Request): () => void;
35
+ /** Returns the active server request store from AsyncLocalStorage or the fallback stack. */
36
+ export declare function getRequestStore(): AkanRequestStore | undefined;
17
37
  /** Returns the active server request from AsyncLocalStorage or the fallback stack. */
18
38
  export declare function getRequest(): Request | undefined;
39
+ export declare function getRequestPolicy(): AkanRequestPolicy | undefined;
40
+ export declare function updateRequestPolicy(patch: Partial<Omit<AkanRequestPolicy, "tags">> & {
41
+ tags?: Iterable<string>;
42
+ }): AkanRequestPolicy | undefined;
43
+ export declare function getRequestDynamicUsage(): AkanDynamicUsage | undefined;
19
44
  /** Deduplicates a promise-producing query within the active request. */
20
45
  export declare function memoizeRequestQuery<T>(key: string, factory: () => Promise<T>): Promise<T>;
21
46
  /** Returns current request headers as a Map, or an empty Map outside a request. */
@@ -1 +1 @@
1
- export declare const HMR_CLIENT_SCRIPT = "(function(){\n if (self.__AKAN_HMR_INSTALLED__) return;\n self.__AKAN_HMR_INSTALLED__ = true;\n var proto = location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n var url = proto + \"//\" + location.host + \"/_akan/hmr\";\n var attempts = 0;\n var socket = null;\n var lastBuildId = null;\n var refreshRuntimePromise = null;\n var refreshQueue = Promise.resolve();\n var overlayEl = null;\n var overlayLabelEl = null;\n var overlayStyleEl = null;\n var overlayTimer = null;\n var overlayHideTimer = null;\n var overlayNextToken = 1;\n var overlayJobs = {};\n self.__AKAN_HMR_PHASE__ = null;\n\n // Bun's React Fast Refresh transform can emit top-level calls to these globals\n // even when we fall back to full reload instead of applying React Refresh.\n self.$RefreshReg$ = self.$RefreshReg$ || function(){};\n self.$RefreshSig$ = self.$RefreshSig$ || function(){ return function(type){ return type; }; };\n\n function connect(){\n try { socket = new WebSocket(url); }\n catch(e){ console.error(\"[akan-hmr] ws init failed\", e); schedule(); return; }\n socket.addEventListener(\"open\", function(){ attempts = 0; });\n socket.addEventListener(\"message\", function(ev){\n var msg;\n try { msg = JSON.parse(ev.data); } catch (e){ return; }\n if (!msg || typeof msg.type !== \"string\") return;\n if (msg.type === \"hello\") {\n if (lastBuildId !== null && msg.buildId !== lastBuildId) {\n location.reload();\n return;\n }\n lastBuildId = msg.buildId;\n return;\n }\n if (msg.type === \"reload\") {\n beginHmrOverlay(\"Reloading...\", true);\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n if (msg.type === \"rsc-refresh\") {\n reloadForHmr(msg);\n return;\n }\n if (msg.type === \"client-refresh\") {\n reloadForHmr(msg);\n return;\n }\n if (msg.type === \"css-update\") {\n var cssUrl = selectCssUrl(msg.cssAssets);\n if (cssUrl) swapCss(cssUrl);\n else {\n beginHmrOverlay(\"Reloading...\", true);\n location.reload();\n }\n return;\n }\n if (msg.type === \"error\") { console.error(\"[akan-hmr]\", msg.message); return; }\n });\n socket.addEventListener(\"close\", function(){ socket = null; schedule(); });\n socket.addEventListener(\"error\", function(){ try { socket && socket.close(); } catch(e){} });\n }\n\n function schedule(){\n attempts = Math.min(attempts + 1, 6);\n var delay = Math.min(30000, 250 * Math.pow(2, attempts - 1));\n setTimeout(connect, delay);\n }\n\n // goseoghyeon: CSR route registry keeps stale module refs, so JS/RSC HMR uses full reload for now.\n function reloadForHmr(msg){\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n if (msg && msg.buildId != null) lastBuildId = msg.buildId;\n location.reload();\n }\n\n function ensureOverlay(){\n if (overlayEl && overlayLabelEl) return overlayEl;\n if (!overlayStyleEl) {\n overlayStyleEl = document.createElement(\"style\");\n overlayStyleEl.textContent =\n \"@keyframes akan-hmr-spin{to{transform:rotate(360deg)}}\" +\n \".__akan_hmr_overlay{position:fixed;left:16px;bottom:16px;z-index:2147483647;display:flex;align-items:center;gap:9px;padding:10px 12px;border-radius:999px;background:rgba(17,24,39,.94);color:#fff;font:500 13px/1.2 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;box-shadow:0 10px 28px rgba(0,0,0,.28);pointer-events:none;opacity:0;transform:translateY(6px);transition:opacity .15s ease,transform .15s ease;backdrop-filter:blur(8px)}\" +\n \".__akan_hmr_overlay[data-show=true]{opacity:1;transform:translateY(0)}\" +\n \".__akan_hmr_spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,.32);border-top-color:#fff;border-radius:999px;animation:akan-hmr-spin .75s linear infinite;flex:none}\" +\n \"@media (prefers-reduced-motion:reduce){.__akan_hmr_overlay{transition:none}.__akan_hmr_spinner{animation:none}}\";\n document.head.appendChild(overlayStyleEl);\n }\n overlayEl = document.createElement(\"div\");\n overlayEl.className = \"__akan_hmr_overlay\";\n overlayEl.setAttribute(\"role\", \"status\");\n overlayEl.setAttribute(\"aria-live\", \"polite\");\n overlayEl.innerHTML = '<span class=\"__akan_hmr_spinner\" aria-hidden=\"true\"></span><span data-akan-hmr-label>Updating...</span>';\n overlayLabelEl = overlayEl.querySelector(\"[data-akan-hmr-label]\");\n (document.body || document.documentElement).appendChild(overlayEl);\n return overlayEl;\n }\n\n function activeOverlayTokens(){\n return Object.keys(overlayJobs);\n }\n\n function latestOverlayLabel(){\n var keys = activeOverlayTokens();\n if (keys.length === 0) return \"Updating...\";\n return overlayJobs[keys[keys.length - 1]] || \"Updating...\";\n }\n\n function showOverlayNow(){\n overlayTimer = null;\n if (activeOverlayTokens().length === 0) return;\n var el = ensureOverlay();\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n }\n\n function beginHmrOverlay(label, immediate){\n var token = overlayNextToken++;\n overlayJobs[token] = label || \"Updating...\";\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (immediate) {\n if (overlayTimer) clearTimeout(overlayTimer);\n showOverlayNow();\n } else if (!overlayTimer && (!overlayEl || overlayEl.getAttribute(\"data-show\") !== \"true\")) {\n overlayTimer = setTimeout(showOverlayNow, 120);\n }\n return token;\n }\n\n function setHmrOverlayLabel(token, label){\n if (!overlayJobs[token]) return;\n overlayJobs[token] = label || \"Updating...\";\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n }\n\n function endHmrOverlay(token){\n delete overlayJobs[token];\n if (activeOverlayTokens().length > 0) {\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n return;\n }\n if (overlayTimer) {\n clearTimeout(overlayTimer);\n overlayTimer = null;\n }\n if (!overlayEl) return;\n overlayEl.setAttribute(\"data-show\", \"false\");\n if (overlayHideTimer) clearTimeout(overlayHideTimer);\n overlayHideTimer = setTimeout(function(){\n if (overlayEl && activeOverlayTokens().length === 0 && overlayEl.parentNode) {\n overlayEl.parentNode.removeChild(overlayEl);\n overlayEl = null;\n overlayLabelEl = null;\n }\n }, 180);\n }\n\n function refreshRsc(msg){\n var started = performance.now();\n var overlayToken = beginHmrOverlay(\"Refreshing page...\");\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n if (!self.__AKAN_RSC_REFRESH__) {\n console.warn(\"[akan-hmr] RSC refresh API unavailable, falling back to full reload\");\n setHmrOverlayLabel(overlayToken, \"Reloading...\");\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n Promise.resolve(self.__AKAN_RSC_REFRESH__({ buildId: msg.buildId })).then(function(){\n lastBuildId = msg.buildId;\n endHmrOverlay(overlayToken);\n console.debug && console.debug(\"[akan-hmr] RSC refreshed\", {\n buildId: msg.buildId,\n generation: msg.generation,\n routeIds: msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n durationMs: Math.round(performance.now() - started)\n });\n }, function(err){\n console.error(\"[akan-hmr] RSC refresh failed, falling back to full reload\", err);\n setHmrOverlayLabel(overlayToken, \"Update failed, reloading...\");\n setTimeout(function(){ location.reload(); }, 250);\n });\n }\n\n function ensureRefreshRuntime(){\n if (refreshRuntimePromise) return refreshRuntimePromise;\n refreshRuntimePromise = import(\"react-refresh/runtime\").then(function(mod){\n var runtime = mod.default || mod;\n if (!self.__AKAN_REACT_REFRESH_READY__) {\n runtime.injectIntoGlobalHook(self);\n self.$RefreshReg$ = function(type, id){ runtime.register(type, id); };\n self.$RefreshSig$ = runtime.createSignatureFunctionForTransform;\n self.__AKAN_REACT_REFRESH_READY__ = true;\n self.__AKAN_REACT_REFRESH_RUNTIME__ = runtime;\n }\n return runtime;\n });\n return refreshRuntimePromise;\n }\n\n function refreshClient(msg){\n refreshQueue = refreshQueue.then(function(){ return doRefreshClient(msg); }, function(){ return doRefreshClient(msg); });\n }\n\n function setHmrPhase(phase){\n self.__AKAN_HMR_PHASE__ = phase;\n }\n\n function doRefreshClient(msg){\n var started = performance.now();\n var metadataAt = started;\n var importAt = started;\n var refreshAt = started;\n var overlayToken = beginHmrOverlay(\"Updating...\");\n var fallbackToRsc = false;\n return ensureRefreshRuntime().then(function(runtime){\n setHmrOverlayLabel(overlayToken, \"Fetching update...\");\n var endpoint = new URL(\"/_akan/hmr/client-refresh\", location.origin);\n endpoint.searchParams.set(\"url\", location.href);\n if (msg.buildId != null) endpoint.searchParams.set(\"buildId\", String(msg.buildId));\n return fetch(endpoint, { credentials: \"same-origin\", cache: \"no-store\" })\n .then(function(res){\n if (!res.ok) throw new Error(\"client-refresh metadata failed \" + res.status + \" \" + res.statusText);\n return res.json();\n })\n .then(function(info){\n metadataAt = performance.now();\n var chunks = Array.isArray(info.chunks) ? info.chunks : [];\n if (chunks.length === 0) throw new Error(\"no client chunks returned\");\n setHmrPhase(\"refresh-import\");\n setHmrOverlayLabel(overlayToken, \"Importing update...\");\n return Promise.all(chunks.map(function(chunk){ return import(chunk); })).then(function(){\n importAt = performance.now();\n setHmrPhase(\"react-refresh\");\n setHmrOverlayLabel(overlayToken, \"Applying update...\");\n try {\n runtime.performReactRefresh();\n } finally {\n setHmrPhase(null);\n }\n refreshAt = performance.now();\n lastBuildId = msg.buildId;\n console.debug && console.debug(\"[akan-hmr] React Fast Refresh applied\", {\n buildId: msg.buildId,\n generation: msg.generation,\n chunks: chunks.length,\n routeIds: info.routeIds || msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n metadataMs: Math.round(metadataAt - started),\n importMs: Math.round(importAt - metadataAt),\n refreshMs: Math.round(refreshAt - importAt),\n durationMs: Math.round(refreshAt - started)\n });\n endHmrOverlay(overlayToken);\n }, function(err){\n setHmrPhase(null);\n throw err;\n });\n });\n }).catch(function(err){\n console.warn(\"[akan-hmr] React Fast Refresh failed, falling back to RSC refresh\", err);\n fallbackToRsc = true;\n endHmrOverlay(overlayToken);\n refreshRsc(msg);\n }).finally(function(){\n if (!fallbackToRsc) endHmrOverlay(overlayToken);\n });\n }\n\n function swapCss(href){\n var overlayToken = beginHmrOverlay(\"Updating styles...\");\n var link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = href;\n link.setAttribute(\"data-akan-css\", \"pending\");\n link.addEventListener(\"load\", function(){\n var prev = document.querySelectorAll(\"link[data-akan-css=active]\");\n for (var i = 0; i < prev.length; i++) prev[i].parentNode && prev[i].parentNode.removeChild(prev[i]);\n link.setAttribute(\"data-akan-css\", \"active\");\n endHmrOverlay(overlayToken);\n });\n link.addEventListener(\"error\", function(){\n if (link.parentNode) link.parentNode.removeChild(link);\n endHmrOverlay(overlayToken);\n });\n document.head.appendChild(link);\n }\n\n function selectCssUrl(cssAssets){\n if (!cssAssets || typeof cssAssets !== \"object\") return null;\n var parts = location.pathname.split(\"/\").filter(Boolean);\n for (var i = 0; i < parts.length; i++) {\n var asset = cssAssets[parts[i]];\n if (asset && asset.cssUrl) return asset.cssUrl;\n }\n return cssAssets[\"\"] && cssAssets[\"\"].cssUrl ? cssAssets[\"\"].cssUrl : null;\n }\n\n connect();\n})();";
1
+ export declare const HMR_CLIENT_SCRIPT = "(function(){\n if (self.__AKAN_HMR_INSTALLED__) return;\n self.__AKAN_HMR_INSTALLED__ = true;\n var proto = location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n var url = proto + \"//\" + location.host + \"/_akan/hmr\";\n var attempts = 0;\n var socket = null;\n var lastBuildId = null;\n var refreshRuntimePromise = null;\n var refreshQueue = Promise.resolve();\n var overlayEl = null;\n var overlayLabelEl = null;\n var overlayStyleEl = null;\n var overlayTimer = null;\n var overlayHideTimer = null;\n var overlayNextToken = 1;\n var overlayJobs = {};\n self.__AKAN_HMR_PHASE__ = null;\n\n // Bun's React Fast Refresh transform can emit top-level calls to these globals\n // even when we fall back to full reload instead of applying React Refresh.\n self.$RefreshReg$ = self.$RefreshReg$ || function(){};\n self.$RefreshSig$ = self.$RefreshSig$ || function(){ return function(type){ return type; }; };\n\n function connect(){\n try { socket = new WebSocket(url); }\n catch(e){ console.error(\"[akan-hmr] ws init failed\", e); schedule(); return; }\n socket.addEventListener(\"open\", function(){ attempts = 0; });\n socket.addEventListener(\"message\", function(ev){\n var msg;\n try { msg = JSON.parse(ev.data); } catch (e){ return; }\n if (!msg || typeof msg.type !== \"string\") return;\n if (msg.type === \"hello\") {\n if (lastBuildId !== null && msg.buildId !== lastBuildId) {\n location.reload();\n return;\n }\n lastBuildId = msg.buildId;\n return;\n }\n if (msg.type === \"reload\") {\n beginHmrOverlay(\"Reloading...\", true);\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n if (msg.type === \"rsc-refresh\") {\n refreshRsc(msg);\n return;\n }\n if (msg.type === \"client-refresh\") {\n refreshClient(msg);\n return;\n }\n if (msg.type === \"css-update\") {\n var cssUrl = selectCssUrl(msg.cssAssets);\n if (cssUrl) swapCss(cssUrl);\n else {\n beginHmrOverlay(\"Reloading...\", true);\n location.reload();\n }\n return;\n }\n if (msg.type === \"error\") { console.error(\"[akan-hmr]\", msg.message); return; }\n });\n socket.addEventListener(\"close\", function(){ socket = null; schedule(); });\n socket.addEventListener(\"error\", function(){ try { socket && socket.close(); } catch(e){} });\n }\n\n function schedule(){\n attempts = Math.min(attempts + 1, 6);\n var delay = Math.min(30000, 250 * Math.pow(2, attempts - 1));\n setTimeout(connect, delay);\n }\n\n function ensureOverlay(){\n if (overlayEl && overlayLabelEl) return overlayEl;\n if (!overlayStyleEl) {\n overlayStyleEl = document.createElement(\"style\");\n overlayStyleEl.textContent =\n \"@keyframes akan-hmr-spin{to{transform:rotate(360deg)}}\" +\n \".__akan_hmr_overlay{position:fixed;left:16px;bottom:16px;z-index:2147483647;display:flex;align-items:center;gap:9px;padding:10px 12px;border-radius:999px;background:rgba(17,24,39,.94);color:#fff;font:500 13px/1.2 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;box-shadow:0 10px 28px rgba(0,0,0,.28);pointer-events:none;opacity:0;transform:translateY(6px);transition:opacity .15s ease,transform .15s ease;backdrop-filter:blur(8px)}\" +\n \".__akan_hmr_overlay[data-show=true]{opacity:1;transform:translateY(0)}\" +\n \".__akan_hmr_spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,.32);border-top-color:#fff;border-radius:999px;animation:akan-hmr-spin .75s linear infinite;flex:none}\" +\n \"@media (prefers-reduced-motion:reduce){.__akan_hmr_overlay{transition:none}.__akan_hmr_spinner{animation:none}}\";\n document.head.appendChild(overlayStyleEl);\n }\n overlayEl = document.createElement(\"div\");\n overlayEl.className = \"__akan_hmr_overlay\";\n overlayEl.setAttribute(\"role\", \"status\");\n overlayEl.setAttribute(\"aria-live\", \"polite\");\n overlayEl.innerHTML = '<span class=\"__akan_hmr_spinner\" aria-hidden=\"true\"></span><span data-akan-hmr-label>Updating...</span>';\n overlayLabelEl = overlayEl.querySelector(\"[data-akan-hmr-label]\");\n (document.body || document.documentElement).appendChild(overlayEl);\n return overlayEl;\n }\n\n function activeOverlayTokens(){\n return Object.keys(overlayJobs);\n }\n\n function latestOverlayLabel(){\n var keys = activeOverlayTokens();\n if (keys.length === 0) return \"Updating...\";\n return overlayJobs[keys[keys.length - 1]] || \"Updating...\";\n }\n\n function showOverlayNow(){\n overlayTimer = null;\n if (activeOverlayTokens().length === 0) return;\n var el = ensureOverlay();\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n }\n\n function beginHmrOverlay(label, immediate){\n var token = overlayNextToken++;\n overlayJobs[token] = label || \"Updating...\";\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (immediate) {\n if (overlayTimer) clearTimeout(overlayTimer);\n showOverlayNow();\n } else if (!overlayTimer && (!overlayEl || overlayEl.getAttribute(\"data-show\") !== \"true\")) {\n overlayTimer = setTimeout(showOverlayNow, 120);\n }\n return token;\n }\n\n function setHmrOverlayLabel(token, label){\n if (!overlayJobs[token]) return;\n overlayJobs[token] = label || \"Updating...\";\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n }\n\n function endHmrOverlay(token){\n delete overlayJobs[token];\n if (activeOverlayTokens().length > 0) {\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n return;\n }\n if (overlayTimer) {\n clearTimeout(overlayTimer);\n overlayTimer = null;\n }\n if (!overlayEl) return;\n overlayEl.setAttribute(\"data-show\", \"false\");\n if (overlayHideTimer) clearTimeout(overlayHideTimer);\n overlayHideTimer = setTimeout(function(){\n if (overlayEl && activeOverlayTokens().length === 0 && overlayEl.parentNode) {\n overlayEl.parentNode.removeChild(overlayEl);\n overlayEl = null;\n overlayLabelEl = null;\n }\n }, 180);\n }\n\n function refreshRsc(msg){\n var started = performance.now();\n var overlayToken = beginHmrOverlay(\"Refreshing page...\");\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n if (!self.__AKAN_RSC_REFRESH__) {\n console.warn(\"[akan-hmr] RSC refresh API unavailable, falling back to full reload\");\n setHmrOverlayLabel(overlayToken, \"Reloading...\");\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n Promise.resolve(self.__AKAN_RSC_REFRESH__({ buildId: msg.buildId })).then(function(){\n lastBuildId = msg.buildId;\n endHmrOverlay(overlayToken);\n console.debug && console.debug(\"[akan-hmr] RSC refreshed\", {\n buildId: msg.buildId,\n generation: msg.generation,\n routeIds: msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n durationMs: Math.round(performance.now() - started)\n });\n }, function(err){\n console.error(\"[akan-hmr] RSC refresh failed, falling back to full reload\", err);\n setHmrOverlayLabel(overlayToken, \"Update failed, reloading...\");\n setTimeout(function(){ location.reload(); }, 250);\n });\n }\n\n function ensureRefreshRuntime(){\n if (refreshRuntimePromise) return refreshRuntimePromise;\n refreshRuntimePromise = import(\"react-refresh/runtime\").then(function(mod){\n var runtime = mod.default || mod;\n if (!self.__AKAN_REACT_REFRESH_READY__) {\n runtime.injectIntoGlobalHook(self);\n self.$RefreshReg$ = function(type, id){ runtime.register(type, id); };\n self.$RefreshSig$ = runtime.createSignatureFunctionForTransform;\n self.__AKAN_REACT_REFRESH_READY__ = true;\n self.__AKAN_REACT_REFRESH_RUNTIME__ = runtime;\n }\n return runtime;\n });\n return refreshRuntimePromise;\n }\n\n function refreshClient(msg){\n refreshQueue = refreshQueue.then(function(){ return doRefreshClient(msg); }, function(){ return doRefreshClient(msg); });\n }\n\n function setHmrPhase(phase){\n self.__AKAN_HMR_PHASE__ = phase;\n }\n\n function doRefreshClient(msg){\n var started = performance.now();\n var metadataAt = started;\n var importAt = started;\n var refreshAt = started;\n var overlayToken = beginHmrOverlay(\"Updating...\");\n var fallbackToRsc = false;\n return ensureRefreshRuntime().then(function(runtime){\n setHmrOverlayLabel(overlayToken, \"Fetching update...\");\n var endpoint = new URL(\"/_akan/hmr/client-refresh\", location.origin);\n endpoint.searchParams.set(\"url\", location.href);\n if (msg.buildId != null) endpoint.searchParams.set(\"buildId\", String(msg.buildId));\n return fetch(endpoint, { credentials: \"same-origin\", cache: \"no-store\" })\n .then(function(res){\n if (!res.ok) throw new Error(\"client-refresh metadata failed \" + res.status + \" \" + res.statusText);\n return res.json();\n })\n .then(function(info){\n metadataAt = performance.now();\n var chunks = Array.isArray(info.chunks) ? info.chunks : [];\n if (chunks.length === 0) throw new Error(\"no client chunks returned\");\n setHmrPhase(\"refresh-import\");\n setHmrOverlayLabel(overlayToken, \"Importing update...\");\n return Promise.all(chunks.map(function(chunk){ return import(chunk); })).then(function(){\n importAt = performance.now();\n setHmrPhase(\"react-refresh\");\n setHmrOverlayLabel(overlayToken, \"Applying update...\");\n try {\n runtime.performReactRefresh();\n } finally {\n setHmrPhase(null);\n }\n refreshAt = performance.now();\n lastBuildId = msg.buildId;\n console.debug && console.debug(\"[akan-hmr] React Fast Refresh applied\", {\n buildId: msg.buildId,\n generation: msg.generation,\n chunks: chunks.length,\n routeIds: info.routeIds || msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n metadataMs: Math.round(metadataAt - started),\n importMs: Math.round(importAt - metadataAt),\n refreshMs: Math.round(refreshAt - importAt),\n durationMs: Math.round(refreshAt - started)\n });\n endHmrOverlay(overlayToken);\n }, function(err){\n setHmrPhase(null);\n throw err;\n });\n });\n }).catch(function(err){\n console.warn(\"[akan-hmr] React Fast Refresh failed, falling back to RSC refresh\", err);\n fallbackToRsc = true;\n endHmrOverlay(overlayToken);\n refreshRsc(msg);\n }).finally(function(){\n if (!fallbackToRsc) endHmrOverlay(overlayToken);\n });\n }\n\n function swapCss(href){\n var overlayToken = beginHmrOverlay(\"Updating styles...\");\n var link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = href;\n link.setAttribute(\"data-akan-css\", \"pending\");\n link.addEventListener(\"load\", function(){\n var prev = document.querySelectorAll(\"link[data-akan-css=active]\");\n for (var i = 0; i < prev.length; i++) prev[i].parentNode && prev[i].parentNode.removeChild(prev[i]);\n link.setAttribute(\"data-akan-css\", \"active\");\n endHmrOverlay(overlayToken);\n });\n link.addEventListener(\"error\", function(){\n if (link.parentNode) link.parentNode.removeChild(link);\n endHmrOverlay(overlayToken);\n });\n document.head.appendChild(link);\n }\n\n function selectCssUrl(cssAssets){\n if (!cssAssets || typeof cssAssets !== \"object\") return null;\n var parts = location.pathname.split(\"/\").filter(Boolean);\n for (var i = 0; i < parts.length; i++) {\n var asset = cssAssets[parts[i]];\n if (asset && asset.cssUrl) return asset.cssUrl;\n }\n return cssAssets[\"\"] && cssAssets[\"\"].cssUrl ? cssAssets[\"\"].cssUrl : null;\n }\n\n connect();\n})();";
@@ -1,7 +1,8 @@
1
+ type InlineRscChunk = [1, string] | [3, string];
1
2
  declare global {
2
- var __RSC_CHUNKS__: string[] | undefined;
3
+ var __RSC_CHUNKS__: InlineRscChunk[] | undefined;
3
4
  var __RSC_CLOSED__: boolean | undefined;
4
- var __RSC_PUSH__: ((b64: string) => void) | undefined;
5
+ var __RSC_PUSH__: ((type: InlineRscChunk[0], data: string) => void) | undefined;
5
6
  var __RSC_CLOSE__: (() => void) | undefined;
6
7
  var __AKAN_RSC_NAVIGATE__: ((href: string, options?: {
7
8
  replace?: boolean;
@@ -0,0 +1,2 @@
1
+ export declare const RSC_CONTENT_TYPE = "text/x-component; charset=utf-8";
2
+ export declare function isRscPayloadResponse(res: Response): boolean;
@@ -4,6 +4,7 @@ import type { AkanMetricsReport } from "akanjs/service";
4
4
  import type { ClientManifest } from "./artifact.d.ts";
5
5
  import type { BaseBuildArtifact, CssAsset } from "./types.d.ts";
6
6
  export type RscRedirectMethod = "replace" | "push";
7
+ export type RscRedirectStatus = 303 | 307 | 308;
7
8
  export type RscRenderResult = {
8
9
  type: "stream";
9
10
  stream: ReadableStream<Uint8Array>;
@@ -13,6 +14,7 @@ export type RscRenderResult = {
13
14
  type: "redirect";
14
15
  location: string;
15
16
  method: RscRedirectMethod;
17
+ status: RscRedirectStatus;
16
18
  } | {
17
19
  type: "not-found";
18
20
  };
@@ -1,4 +1,17 @@
1
1
  import type { SsrChunkRegistryStats, SsrFromRscInput } from "./ssrTypes.d.ts";
2
+ export declare class SsrChunkRegistry<T> {
3
+ #private;
4
+ readonly maxEntries: number;
5
+ constructor(maxEntries?: number);
6
+ get size(): number;
7
+ get evictionCount(): number;
8
+ get(key: string): T | undefined;
9
+ set(keys: string[], value: T): void;
10
+ }
11
+ export type InlineRscChunk = readonly [1, string] | readonly [3, string];
12
+ export declare function encodeInlineRscChunk(chunk: Uint8Array): InlineRscChunk;
13
+ export declare function htmlEscapeJsonString(value: string): string;
14
+ export declare function createInlineRscScript(chunk: Uint8Array): string;
2
15
  export declare class SsrFromRscRenderer {
3
16
  #private;
4
17
  static getChunkRegistryStats(): SsrChunkRegistryStats;
@@ -16,6 +16,7 @@ export interface SsrChunkRegistryStats {
16
16
  ssrChunkRegistrySize: number;
17
17
  ssrChunkLoadCount: number;
18
18
  ssrChunkCacheHitCount: number;
19
+ ssrChunkEvictionCount: number;
19
20
  }
20
21
  export interface SsrFromRscInput {
21
22
  request?: Request;
@@ -2,8 +2,10 @@ import { type AkanI18nConfig } from "akanjs/common";
2
2
  import type { AkanMetricsReport } from "akanjs/service";
3
3
  import { type BuilderRpc, type RouteSeedIndex } from "./artifact.d.ts";
4
4
  import type { HmrWsData, HmrWsHub } from "./hmr/wsHub.d.ts";
5
- import { RscWorker } from "./rscWorkerHost.d.ts";
5
+ import { type RscRedirectMethod, type RscRedirectStatus, RscWorker } from "./rscWorkerHost.d.ts";
6
6
  import type { BaseBuildArtifact, HttpRoutes, RenderState } from "./types.d.ts";
7
+ export declare function createRscRedirectResponse(location: string, method: RscRedirectMethod, status?: RscRedirectStatus): Response;
8
+ export declare function createRscStreamResponse(stream: BodyInit, status?: number): Response;
7
9
  export declare function normalizeRscTargetUrlForHostBasePath(targetUrl: URL, options: {
8
10
  basePath: string | null;
9
11
  basePaths?: readonly string[];
@@ -99,6 +99,7 @@ export interface AkanMetricsReport {
99
99
  ssrChunkRegistrySize?: number;
100
100
  ssrChunkLoadCount?: number;
101
101
  ssrChunkCacheHitCount?: number;
102
+ ssrChunkEvictionCount?: number;
102
103
  httpFullSsrCount?: number;
103
104
  httpRscNavigationCount?: number;
104
105
  httpStaticAssetCount?: number;