akanjs 2.2.10 → 2.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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. */
@@ -8,6 +8,18 @@ export interface AkanServerProps extends AkanLibProps {
8
8
  prefix?: string;
9
9
  websocketPrefix?: string;
10
10
  }
11
+ export interface AkanServerConsoleInfo {
12
+ name: string;
13
+ status: AkanServer["status"];
14
+ serverMode: AkanServer["serverMode"];
15
+ env: Pick<BaseEnv, "appName" | "environment" | "operationMode" | "repoName" | "serveDomain" | "databaseMode">;
16
+ services: string[];
17
+ signals: string[];
18
+ adaptors: string[];
19
+ uses: string[];
20
+ serviceStages: string[][];
21
+ adaptorStages: string[][];
22
+ }
11
23
  export declare class AkanServer {
12
24
  #private;
13
25
  status: "stopped" | "initializing" | "initialized" | "starting" | "running" | "stopping";
@@ -34,6 +46,7 @@ export declare class AkanServer {
34
46
  getService<T = Service>(refName: string): T;
35
47
  getSignal<T = ServerSignal>(refName: string): T;
36
48
  getAdaptor<T = Adaptor>(refName: string): T;
49
+ inspectConsole(): AkanServerConsoleInfo;
37
50
  init({ routes: initRoutes, web }?: {
38
51
  routes?: boolean;
39
52
  web?: boolean;
@@ -0,0 +1,25 @@
1
+ import type { BaseEnv } from "akanjs/base";
2
+ import type { Adaptor, Service } from "akanjs/service";
3
+ import type { ServerSignal } from "akanjs/signal";
4
+ import type { AkanServer } from "./akanServer.d.ts";
5
+ export interface AkanConsoleOptions {
6
+ prompt?: string;
7
+ globals?: Record<string, unknown>;
8
+ input?: typeof process.stdin;
9
+ output?: typeof process.stdout;
10
+ }
11
+ export interface AkanConsoleContext extends Record<string, unknown> {
12
+ server: AkanServer;
13
+ env: AkanServer["env"];
14
+ get: AkanServer["get"];
15
+ service: <T = Service>(refName: string) => T;
16
+ signal: <T = ServerSignal>(refName: string) => T;
17
+ adaptor: <T = Adaptor>(refName: string) => T;
18
+ methods: (value: unknown) => string[];
19
+ debug: () => ReturnType<AkanServer["inspectConsole"]>;
20
+ }
21
+ export declare const assertAkanConsoleAllowed: (env?: Pick<BaseEnv, "environment" | "operationMode">) => void;
22
+ export declare const getAkanConsoleMethods: (value: unknown) => string[];
23
+ export declare const createAkanConsoleContext: (server: AkanServer, globals?: Record<string, unknown>) => AkanConsoleContext;
24
+ export declare const evaluateAkanConsoleInput: (source: string, context: Record<string, unknown>) => Promise<unknown>;
25
+ export declare const startAkanConsole: (server: AkanServer, options?: AkanConsoleOptions) => Promise<void>;
@@ -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})();";
@@ -3,6 +3,7 @@ export * from "./akanLib.d.ts";
3
3
  export * from "./akanOption.d.ts";
4
4
  export * from "./akanServer.d.ts";
5
5
  export * from "./artifact.d.ts";
6
+ export * from "./console.d.ts";
6
7
  export * from "./decorators.d.ts";
7
8
  export type { ChangeBatch, ChangeKind } from "./hmr/changeBatch.d.ts";
8
9
  export * from "./processMetricsCollector.d.ts";