akanjs 2.2.11 → 2.2.13-rc.0

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.
@@ -16,6 +16,14 @@ export interface SsrChunkRegistryStats {
16
16
  ssrChunkRegistrySize: number;
17
17
  ssrChunkLoadCount: number;
18
18
  ssrChunkCacheHitCount: number;
19
+ ssrChunkEvictionCount: number;
20
+ }
21
+
22
+ export interface SsrLateRedirect {
23
+ type: "redirect";
24
+ location: string;
25
+ method: "replace" | "push";
26
+ status: 303 | 307 | 308;
19
27
  }
20
28
 
21
29
  export interface SsrFromRscInput {
@@ -44,4 +52,6 @@ export interface SsrFromRscInput {
44
52
  importmap?: Record<string, string>;
45
53
  theme?: AkanTheme;
46
54
  injectThemeInitScript?: boolean;
55
+ lateControl?: Promise<SsrLateRedirect | null>;
56
+ onCancel?: (reason?: unknown) => void;
47
57
  }
@@ -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, type RscRenderResult, 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,99 @@ 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
+
60
+ export function cacheHtmlWhileStreaming(
61
+ stream: ReadableStream<Uint8Array>,
62
+ onComplete: (html: string) => void,
63
+ ): ReadableStream<Uint8Array> {
64
+ const chunks: Uint8Array[] = [];
65
+ let byteLength = 0;
66
+ const decoder = new TextDecoder();
67
+
68
+ return stream.pipeThrough(
69
+ new TransformStream<Uint8Array, Uint8Array>({
70
+ transform(chunk, controller) {
71
+ chunks.push(chunk.slice());
72
+ byteLength += chunk.byteLength;
73
+ controller.enqueue(chunk);
74
+ },
75
+ flush() {
76
+ const body = new Uint8Array(byteLength);
77
+ let offset = 0;
78
+ for (const chunk of chunks) {
79
+ body.set(chunk, offset);
80
+ offset += chunk.byteLength;
81
+ }
82
+ try {
83
+ onComplete(decoder.decode(body));
84
+ } catch {
85
+ }
86
+ },
87
+ }),
88
+ );
89
+ }
90
+
91
+ export function cancelStreamForHeadResponse(stream: ReadableStream<Uint8Array>, reason: unknown): void {
92
+ void stream.cancel(reason).catch(() => {
93
+ });
94
+ }
95
+
96
+ export async function createRscNavigationStreamResponse(
97
+ result: Extract<RscRenderResult, { type: "stream" }>,
98
+ ): Promise<Response> {
99
+
100
+ const chunks: Uint8Array[] = [];
101
+ let byteLength = 0;
102
+ const reader = result.stream.getReader();
103
+ try {
104
+ while (true) {
105
+ const { value, done } = await reader.read();
106
+ if (done) break;
107
+ chunks.push(value);
108
+ byteLength += value.byteLength;
109
+ }
110
+ } finally {
111
+ reader.releaseLock();
112
+ }
113
+
114
+ const lateControl = await result.lateControl;
115
+ if (lateControl?.type === "redirect")
116
+ return createRscRedirectResponse(lateControl.location, lateControl.method, lateControl.status);
117
+ const body = new Uint8Array(byteLength);
118
+ let offset = 0;
119
+ for (const chunk of chunks) {
120
+ body.set(chunk, offset);
121
+ offset += chunk.byteLength;
122
+ }
123
+ return createRscStreamResponse(body, result.status ?? 200);
124
+ }
125
+
33
126
  export function normalizeRscTargetUrlForHostBasePath(
34
127
  targetUrl: URL,
35
128
  options: {
@@ -261,18 +354,14 @@ export class WebRouter {
261
354
  });
262
355
  const result = await this.#rsc.renderWithMeta(rscReq, {
263
356
  clientManifest: manifest.clientManifest,
357
+ signal: req.signal,
264
358
  });
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");
359
+ if (result.type === "redirect")
360
+ return createRscRedirectResponse(result.location, result.method, result.status);
361
+ if (result.type === "not-found") return WebRouter.#rscNotFoundResponse();
268
362
  if (result.status && result.status >= 500)
269
363
  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
- });
364
+ return createRscNavigationStreamResponse(result);
276
365
  } catch (err) {
277
366
  return this.#renderRscErrorResponse("__rsc", err);
278
367
  }
@@ -367,8 +456,10 @@ export class WebRouter {
367
456
  }
368
457
  const rscResult = await this.#rsc.renderWithMeta(req, {
369
458
  clientManifest: manifest.clientManifest,
459
+ signal: req.signal,
370
460
  });
371
- if (rscResult.type === "redirect") return Response.redirect(new URL(rscResult.location, url.origin), 307);
461
+ if (rscResult.type === "redirect")
462
+ return Response.redirect(new URL(rscResult.location, url.origin), rscResult.status);
372
463
  if (rscResult.type === "not-found") return this.#renderNotFoundResponse(req, url);
373
464
  const themeCookieExists = WebRouter.#hasCookie(req, "theme");
374
465
  const htmlStream = await new SsrFromRscRenderer().render({
@@ -379,20 +470,33 @@ export class WebRouter {
379
470
  extraBootstrapInline: !this.#prodMode ? HMR_CLIENT_SCRIPT : undefined,
380
471
  importmap: this.#artifact.vendorMap,
381
472
  theme: themeCookieExists ? undefined : (rscResult.theme ?? "system"),
473
+ lateControl: rscResult.lateControl,
474
+ onCancel: (reason) => {
475
+ rscResult.cancel(reason);
476
+ },
382
477
  });
383
478
  const responseStatus = rscResult.status ?? 200;
384
479
  const responseHeaders = WebRouter.#htmlResponseHeaders(responseStatus);
480
+ if (req.method === "HEAD") {
481
+ const headers = new Headers(responseHeaders);
482
+ if (htmlCacheKey && responseStatus === 200) headers.set("X-Akan-Cache", "MISS");
483
+ cancelStreamForHeadResponse(htmlStream, new Error("HEAD response does not consume body"));
484
+ return new Response(null, { status: responseStatus, headers });
485
+ }
385
486
  if (htmlCacheKey && responseStatus === 200) {
386
- const html = await new Response(htmlStream).text();
387
- this.#setCachedHtml(htmlCacheKey, html);
388
- return new Response(html, {
389
- headers: {
390
- "Content-Type": "text/html; charset=utf-8",
391
- "X-Akan-Cache": "MISS",
487
+ const headers = new Headers(responseHeaders);
488
+ headers.set("X-Akan-Cache", "MISS");
489
+ return new Response(
490
+ cacheHtmlWhileStreaming(htmlStream, (html) => {
491
+ this.#setCachedHtml(htmlCacheKey, html);
492
+ }),
493
+ {
494
+ status: responseStatus,
495
+ headers,
392
496
  },
393
- });
497
+ );
394
498
  }
395
- return new Response(req.method === "HEAD" ? null : htmlStream, {
499
+ return new Response(htmlStream, {
396
500
  status: responseStatus,
397
501
  headers: responseHeaders,
398
502
  });
@@ -417,6 +521,7 @@ export class WebRouter {
417
521
  ssrChunkRegistrySize: ssrStats.ssrChunkRegistrySize,
418
522
  ssrChunkLoadCount: ssrStats.ssrChunkLoadCount,
419
523
  ssrChunkCacheHitCount: ssrStats.ssrChunkCacheHitCount,
524
+ ssrChunkEvictionCount: ssrStats.ssrChunkEvictionCount,
420
525
  httpFullSsrCount: this.#requestStats.fullSsr,
421
526
  httpRscNavigationCount: this.#requestStats.rscNavigation,
422
527
  httpStaticAssetCount: this.#requestStats.staticAsset,
@@ -650,15 +755,10 @@ export class WebRouter {
650
755
  if (!last || last.index === undefined) return `${html}\n${snippet}`;
651
756
  return `${html.slice(0, last.index)}${snippet}\n${html.slice(last.index)}`;
652
757
  }
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
- },
758
+ static #rscNotFoundResponse(): Response {
759
+ return new Response("Not Found", {
760
+ status: 404,
761
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
662
762
  });
663
763
  }
664
764
  #getProductionRouteCache() {
@@ -57,6 +57,7 @@ export interface AkanMetricsReport {
57
57
  rscWorkerLastRecycleReason?: string;
58
58
  rscPendingRenderCount?: number;
59
59
  rscQueuedSendCount?: number;
60
+ rscHostPendingChunkOverflowCount?: number;
60
61
  rscRenderCount?: number;
61
62
  rscInFlightRenderCount?: number;
62
63
  rscLastRenderedPath?: string;
@@ -85,6 +86,7 @@ export interface AkanMetricsReport {
85
86
  ssrChunkRegistrySize?: number;
86
87
  ssrChunkLoadCount?: number;
87
88
  ssrChunkCacheHitCount?: number;
89
+ ssrChunkEvictionCount?: number;
88
90
  httpFullSsrCount?: number;
89
91
  httpRscNavigationCount?: number;
90
92
  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 +1 @@
1
- export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], string, never, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">;
1
+ export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], string, never, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">;
@@ -1,11 +1,11 @@
1
1
  import type { BaseEndpoint } from "akanjs/signal";
2
2
  export declare const dictionary: {
3
- base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, never>;
3
+ base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, never>;
4
4
  };
5
- export declare const Err: import("./trans.d.ts").ErrConstructor<never>, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, data?: import("./trans.d.ts").TranslationData) => string, msg: {
6
- info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
7
- success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
8
- error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
9
- warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
10
- loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
11
- }, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, __Error_Key__: never;
5
+ export declare const Err: import("./trans.d.ts").ErrConstructor<never>, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, data?: import("./trans.d.ts").TranslationData) => string, msg: {
6
+ info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
7
+ success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
8
+ error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
9
+ warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
10
+ loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
11
+ }, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, __Error_Key__: never;
@@ -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;
@@ -2,20 +2,50 @@ import { type AkanI18nConfig } from "akanjs/common";
2
2
  import type { AkanTheme } from "akanjs/fetch";
3
3
  import type { AkanMetricsReport } from "akanjs/service";
4
4
  import type { ClientManifest } from "./artifact.d.ts";
5
+ import type { SsrLateRedirect } from "./ssrTypes.d.ts";
5
6
  import type { BaseBuildArtifact, CssAsset } from "./types.d.ts";
7
+ export interface RscPending {
8
+ onChunk: (data: Uint8Array) => void;
9
+ onEnd: () => void;
10
+ onError: (message: string) => void;
11
+ onMeta?: (meta: {
12
+ theme?: AkanTheme;
13
+ status?: number;
14
+ }) => void;
15
+ onRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
16
+ onLateRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
17
+ onNotFound?: () => void;
18
+ }
6
19
  export type RscRedirectMethod = "replace" | "push";
20
+ export type RscRedirectStatus = 303 | 307 | 308;
7
21
  export type RscRenderResult = {
8
22
  type: "stream";
9
23
  stream: ReadableStream<Uint8Array>;
10
24
  theme?: AkanTheme;
11
25
  status?: number;
26
+ lateControl: Promise<SsrLateRedirect | null>;
27
+ cancel: (reason?: unknown) => void;
12
28
  } | {
13
29
  type: "redirect";
14
30
  location: string;
15
31
  method: RscRedirectMethod;
32
+ status: RscRedirectStatus;
16
33
  } | {
17
34
  type: "not-found";
18
35
  };
36
+ export declare function getRscHostMaxPendingChunks(value?: string | undefined): number;
37
+ export declare function nextRscHostPendingChunkCount(currentPendingChunks: number, desiredSize: number | null): number;
38
+ export declare function isRscHostPendingChunkOverflow(pendingChunks: number, maxPendingChunks: number): boolean;
39
+ export declare function createIdempotentRscRenderCancel(onCancel: (reason?: unknown) => void): (reason?: unknown) => void;
40
+ export declare function createRscHostRenderStream(input: {
41
+ setPending: (pending: RscPending) => void;
42
+ deletePending: () => void;
43
+ sendRenderOrQueue: () => void;
44
+ cancelRender: (reason?: unknown) => void;
45
+ maxPendingChunks?: number;
46
+ signal?: AbortSignal;
47
+ onPendingChunkOverflow?: () => void;
48
+ }): Promise<RscRenderResult>;
19
49
  export interface RscWorkerReloadInput {
20
50
  clientManifest: ClientManifest;
21
51
  cssAssets?: Record<string, CssAsset>;
@@ -62,6 +92,7 @@ export declare class RscWorker {
62
92
  render(req: Request): ReadableStream<Uint8Array>;
63
93
  renderWithMeta(req: Request, options?: {
64
94
  clientManifest?: ClientManifest;
95
+ signal?: AbortSignal;
65
96
  }): Promise<RscRenderResult>;
66
97
  kill(): void;
67
98
  getMetrics(): AkanMetricsReport;
@@ -0,0 +1,23 @@
1
+ export type CachedRscReplayMessage = {
2
+ type: "meta";
3
+ requestId: string;
4
+ theme?: string;
5
+ status?: number;
6
+ } | {
7
+ type: "chunk";
8
+ requestId: string;
9
+ data: Uint8Array;
10
+ } | {
11
+ type: "end";
12
+ requestId: string;
13
+ };
14
+ export declare function replayCachedRscResult(input: {
15
+ requestId: string;
16
+ chunks: readonly Uint8Array[];
17
+ theme?: string;
18
+ status?: number;
19
+ send: (message: CachedRscReplayMessage) => void;
20
+ isCancelled: () => boolean;
21
+ yieldEveryChunks?: number;
22
+ yieldToHost?: () => Promise<void>;
23
+ }): Promise<boolean>;
@@ -1,4 +1,34 @@
1
- import type { SsrChunkRegistryStats, SsrFromRscInput } from "./ssrTypes.d.ts";
1
+ import type { SsrChunkRegistryStats, SsrFromRscInput, SsrLateRedirect } 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;
15
+ export declare function createSoftRedirectScript(redirect: SsrLateRedirect): string;
16
+ export declare function sanitizeFlightForClientStream(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array>;
17
+ export declare class ExpectedLateRedirectStderrSuppressor {
18
+ #private;
19
+ private constructor();
20
+ static start(lateControl?: Promise<SsrLateRedirect | null>): ExpectedLateRedirectStderrSuppressor | null;
21
+ stop(): void;
22
+ }
23
+ export declare function interleaveRscScriptsWithHtml(htmlStream: ReadableStream<Uint8Array>, rscClientStream: ReadableStream<Uint8Array>, options?: {
24
+ bootstrapModuleScripts?: string;
25
+ lateControl?: Promise<SsrLateRedirect | null>;
26
+ maxPendingRscScripts?: number;
27
+ onPendingRscScriptsSize?: (size: number) => void;
28
+ onComplete?: () => void;
29
+ onCancel?: (reason?: unknown) => void;
30
+ request?: Request;
31
+ }): ReadableStream<Uint8Array>;
2
32
  export declare class SsrFromRscRenderer {
3
33
  #private;
4
34
  static getChunkRegistryStats(): SsrChunkRegistryStats;
@@ -16,6 +16,13 @@ export interface SsrChunkRegistryStats {
16
16
  ssrChunkRegistrySize: number;
17
17
  ssrChunkLoadCount: number;
18
18
  ssrChunkCacheHitCount: number;
19
+ ssrChunkEvictionCount: number;
20
+ }
21
+ export interface SsrLateRedirect {
22
+ type: "redirect";
23
+ location: string;
24
+ method: "replace" | "push";
25
+ status: 303 | 307 | 308;
19
26
  }
20
27
  export interface SsrFromRscInput {
21
28
  request?: Request;
@@ -43,4 +50,6 @@ export interface SsrFromRscInput {
43
50
  importmap?: Record<string, string>;
44
51
  theme?: AkanTheme;
45
52
  injectThemeInitScript?: boolean;
53
+ lateControl?: Promise<SsrLateRedirect | null>;
54
+ onCancel?: (reason?: unknown) => void;
46
55
  }