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.
- package/CHANGELOG.md +7 -0
- package/client/cookie.ts +3 -13
- package/client/router.ts +11 -3
- package/fetch/requestStorage.ts +106 -39
- package/package.json +1 -1
- package/server/akanApp.ts +55 -0
- package/server/hmr/clientScript.ts +2 -8
- package/server/rscClient.tsx +17 -7
- package/server/rscHttp.ts +7 -0
- package/server/rscWorker.tsx +183 -46
- package/server/rscWorkerHost.ts +242 -70
- package/server/rscWorkerReplay.ts +35 -0
- package/server/ssrFromRscRenderer.tsx +562 -81
- package/server/ssrTypes.ts +10 -0
- package/server/webRouter.ts +128 -28
- package/service/ipcTypes.ts +2 -0
- package/types/client/router.d.ts +8 -2
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/fetch/requestStorage.d.ts +31 -6
- package/types/server/hmr/clientScript.d.ts +1 -1
- package/types/server/rscClient.d.ts +3 -2
- package/types/server/rscHttp.d.ts +2 -0
- package/types/server/rscWorkerHost.d.ts +31 -0
- package/types/server/rscWorkerReplay.d.ts +23 -0
- package/types/server/ssrFromRscRenderer.d.ts +31 -1
- package/types/server/ssrTypes.d.ts +9 -0
- package/types/server/webRouter.d.ts +8 -1
- package/types/service/ipcTypes.d.ts +2 -0
- package/ui/Link/SsrLink.tsx +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# akanjs
|
|
2
2
|
|
|
3
|
+
## 2.2.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 666e46c: Improve SSR hydration payload handling, redirect status propagation, and restore dev HMR incremental refresh behavior.
|
|
8
|
+
- 666e46c: Align RSC not-found responses with HTTP 404 semantics and add request-scoped policy tracking for future cache decisions.
|
|
9
|
+
|
|
3
10
|
## 2.2.11
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/client/cookie.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getEnv } from "akanjs/base";
|
|
2
2
|
import { decodeJwtPayload, Logger } from "akanjs/common";
|
|
3
3
|
import type { Account } from "akanjs/fetch";
|
|
4
|
-
import {
|
|
4
|
+
import { cookies as serverCookies, headers as serverHeaders } from "akanjs/fetch";
|
|
5
5
|
import { loadCapacitorCore } from "./capacitor";
|
|
6
6
|
import { storage } from "./storage";
|
|
7
7
|
import { fetch } from "./useClient";
|
|
@@ -30,11 +30,7 @@ function parseCookieHeader(cookieHeader: string): Map<string, { name: string; va
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export const cookies = (): Map<string, { name: string; value: string }> => {
|
|
33
|
-
if (getEnv().side === "server")
|
|
34
|
-
const req = requestStorage?.getStore();
|
|
35
|
-
if (!req) return new Map();
|
|
36
|
-
return parseCookieHeader(req.headers.get("cookie") ?? "");
|
|
37
|
-
}
|
|
33
|
+
if (getEnv().side === "server") return serverCookies();
|
|
38
34
|
return parseCookieHeader(document.cookie);
|
|
39
35
|
};
|
|
40
36
|
|
|
@@ -75,13 +71,7 @@ export const removeCookie = (key: string, options: { path: string } = { path: "/
|
|
|
75
71
|
};
|
|
76
72
|
export const headers = (): Map<string, string> => {
|
|
77
73
|
if (getEnv().side !== "server") return new Map();
|
|
78
|
-
|
|
79
|
-
if (!req) return new Map();
|
|
80
|
-
const map = new Map<string, string>();
|
|
81
|
-
req.headers.forEach((value, key) => {
|
|
82
|
-
map.set(key, value);
|
|
83
|
-
});
|
|
84
|
-
return map;
|
|
74
|
+
return serverHeaders();
|
|
85
75
|
};
|
|
86
76
|
|
|
87
77
|
export const getHeader = (key: string): string | undefined => {
|
package/client/router.ts
CHANGED
|
@@ -35,12 +35,18 @@ interface CSRClientRouterOption extends RouterOptions {
|
|
|
35
35
|
router: RouterInstance;
|
|
36
36
|
}
|
|
37
37
|
export type RedirectMethod = "replace" | "push";
|
|
38
|
+
export type RedirectStatus = 303 | 307 | 308;
|
|
39
|
+
export interface RedirectOptions {
|
|
40
|
+
method?: RedirectMethod;
|
|
41
|
+
status?: RedirectStatus;
|
|
42
|
+
}
|
|
38
43
|
|
|
39
44
|
export class AkanRedirectError extends Error {
|
|
40
45
|
readonly digest = "AKAN_REDIRECT";
|
|
41
46
|
constructor(
|
|
42
47
|
readonly location: string,
|
|
43
48
|
readonly method: RedirectMethod = "replace",
|
|
49
|
+
readonly status: RedirectStatus = 307,
|
|
44
50
|
) {
|
|
45
51
|
super(`Redirect to ${location}`);
|
|
46
52
|
this.name = "AkanRedirectError";
|
|
@@ -246,7 +252,9 @@ class Router {
|
|
|
246
252
|
this.#instance.refresh();
|
|
247
253
|
return undefined as never;
|
|
248
254
|
}
|
|
249
|
-
redirect(href: string): never {
|
|
255
|
+
redirect(href: string, options: RedirectOptions = {}): never {
|
|
256
|
+
const method = options.method ?? "replace";
|
|
257
|
+
const status = options.status ?? 307;
|
|
250
258
|
if (getEnv().side === "server") {
|
|
251
259
|
const { getRequest, headers: requestHeaders } = getServerRequestContext();
|
|
252
260
|
const h = requestHeaders();
|
|
@@ -257,9 +265,9 @@ class Router {
|
|
|
257
265
|
const basePath = getServerBasePath(reqPathname, lang, h.get("x-base-path") ?? undefined, this.#prefix);
|
|
258
266
|
const { pathname, href: fullHref } = getPathInfo(href, lang, shouldExposeBasePath() ? basePath : "");
|
|
259
267
|
Logger.log(`redirect to:${pathname}`);
|
|
260
|
-
throw new AkanRedirectError(fullHref,
|
|
268
|
+
throw new AkanRedirectError(fullHref, method, status);
|
|
261
269
|
} else {
|
|
262
|
-
this.#instance
|
|
270
|
+
this.#instance[method](href);
|
|
263
271
|
}
|
|
264
272
|
return undefined as never;
|
|
265
273
|
}
|
package/fetch/requestStorage.ts
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export type AkanTheme = "css" | "system" | (string & {});
|
|
2
|
+
|
|
3
|
+
export interface AkanRequestPolicy {
|
|
4
|
+
routeId?: string;
|
|
5
|
+
rscCache?: "public" | false;
|
|
6
|
+
rscCacheTtl?: number;
|
|
7
|
+
cacheable?: boolean;
|
|
8
|
+
revalidate?: number | false;
|
|
9
|
+
tags: Set<string>;
|
|
4
10
|
}
|
|
5
11
|
|
|
6
|
-
export
|
|
12
|
+
export interface AkanDynamicUsage {
|
|
13
|
+
headers: boolean;
|
|
14
|
+
cookies: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AkanRequestStore {
|
|
18
|
+
request: Request;
|
|
19
|
+
theme?: AkanTheme;
|
|
20
|
+
queryCache: Map<string, Promise<unknown>>;
|
|
21
|
+
policy: AkanRequestPolicy;
|
|
22
|
+
dynamicUsage: AkanDynamicUsage;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RequestStorage {
|
|
26
|
+
run<T>(store: Request | AkanRequestStore, callback: () => T): T;
|
|
27
|
+
getStore(): AkanRequestStore | undefined;
|
|
28
|
+
}
|
|
7
29
|
|
|
8
30
|
declare global {
|
|
9
31
|
var __AKAN_REQUEST_STORAGE__: RequestStorage | undefined;
|
|
10
|
-
var
|
|
11
|
-
var __AKAN_REQUEST_QUERY_CACHE__: WeakMap<Request, Map<string, Promise<unknown>>> | undefined;
|
|
12
|
-
var __AKAN_REQUEST_FALLBACK_STACK__: Request[] | undefined;
|
|
32
|
+
var __AKAN_REQUEST_FALLBACK_STACK__: AkanRequestStore[] | undefined;
|
|
13
33
|
}
|
|
14
34
|
|
|
15
35
|
let _requestStorage: RequestStorage | null = null;
|
|
@@ -17,74 +37,120 @@ if (typeof window === "undefined") {
|
|
|
17
37
|
try {
|
|
18
38
|
|
|
19
39
|
const { AsyncLocalStorage } = require("node:async_hooks") as typeof import("node:async_hooks");
|
|
20
|
-
|
|
40
|
+
const als = new AsyncLocalStorage<AkanRequestStore>();
|
|
41
|
+
globalThis.__AKAN_REQUEST_STORAGE__ ??= {
|
|
42
|
+
run<T>(store: Request | AkanRequestStore, callback: () => T): T {
|
|
43
|
+
return als.run(normalizeRequestStore(store), callback);
|
|
44
|
+
},
|
|
45
|
+
getStore(): AkanRequestStore | undefined {
|
|
46
|
+
return als.getStore();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
21
49
|
_requestStorage = globalThis.__AKAN_REQUEST_STORAGE__;
|
|
22
50
|
} catch {}
|
|
23
51
|
}
|
|
24
52
|
|
|
25
53
|
export const requestStorage: RequestStorage | null = _requestStorage;
|
|
26
54
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
function createRequestPolicy(): AkanRequestPolicy {
|
|
56
|
+
return { tags: new Set() };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createRequestStore(
|
|
60
|
+
request: Request,
|
|
61
|
+
policy: Partial<Omit<AkanRequestPolicy, "tags">> = {},
|
|
62
|
+
): AkanRequestStore {
|
|
63
|
+
return {
|
|
64
|
+
request,
|
|
65
|
+
queryCache: new Map(),
|
|
66
|
+
policy: { ...createRequestPolicy(), ...policy },
|
|
67
|
+
dynamicUsage: { headers: false, cookies: false },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isRequestStore(store: Request | AkanRequestStore | undefined): store is AkanRequestStore {
|
|
72
|
+
return Boolean(store && typeof store === "object" && "request" in store && store.request instanceof Request);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeRequestStore(store: Request | AkanRequestStore): AkanRequestStore {
|
|
76
|
+
return isRequestStore(store) ? store : createRequestStore(store);
|
|
30
77
|
}
|
|
31
78
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
return
|
|
79
|
+
function getActiveRequestStore(): AkanRequestStore | undefined {
|
|
80
|
+
const store = requestStorage?.getStore() as Request | AkanRequestStore | undefined;
|
|
81
|
+
if (store) return isRequestStore(store) ? store : createRequestStore(store);
|
|
82
|
+
return globalThis.__AKAN_REQUEST_FALLBACK_STACK__?.at(-1);
|
|
35
83
|
}
|
|
36
84
|
|
|
37
85
|
/** Stores theme preference on the active request when server rendering. */
|
|
38
86
|
export function setRequestTheme(theme: AkanTheme | undefined): void {
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
|
|
87
|
+
const store = getRequestStore();
|
|
88
|
+
if (!store || theme === undefined) return;
|
|
89
|
+
store.theme = theme;
|
|
42
90
|
}
|
|
43
91
|
|
|
44
92
|
export function getRequestTheme(): AkanTheme | undefined {
|
|
45
|
-
|
|
46
|
-
if (!req) return undefined;
|
|
47
|
-
return requestThemeMap().get(req);
|
|
93
|
+
return getRequestStore()?.theme;
|
|
48
94
|
}
|
|
49
95
|
|
|
50
96
|
export function pushRequestFallback(req: Request): () => void {
|
|
51
97
|
globalThis.__AKAN_REQUEST_FALLBACK_STACK__ ??= [];
|
|
52
98
|
const stack = globalThis.__AKAN_REQUEST_FALLBACK_STACK__;
|
|
53
|
-
|
|
99
|
+
const store = createRequestStore(req);
|
|
100
|
+
stack.push(store);
|
|
54
101
|
return () => {
|
|
55
|
-
const index = stack.lastIndexOf(
|
|
102
|
+
const index = stack.lastIndexOf(store);
|
|
56
103
|
if (index >= 0) stack.splice(index, 1);
|
|
57
104
|
};
|
|
58
105
|
}
|
|
59
106
|
|
|
107
|
+
/** Returns the active server request store from AsyncLocalStorage or the fallback stack. */
|
|
108
|
+
export function getRequestStore(): AkanRequestStore | undefined {
|
|
109
|
+
return getActiveRequestStore();
|
|
110
|
+
}
|
|
111
|
+
|
|
60
112
|
/** Returns the active server request from AsyncLocalStorage or the fallback stack. */
|
|
61
113
|
export function getRequest(): Request | undefined {
|
|
62
|
-
return
|
|
114
|
+
return getRequestStore()?.request;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getRequestPolicy(): AkanRequestPolicy | undefined {
|
|
118
|
+
return getRequestStore()?.policy;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function updateRequestPolicy(
|
|
122
|
+
patch: Partial<Omit<AkanRequestPolicy, "tags">> & { tags?: Iterable<string> },
|
|
123
|
+
): AkanRequestPolicy | undefined {
|
|
124
|
+
const policy = getRequestPolicy();
|
|
125
|
+
if (!policy) return undefined;
|
|
126
|
+
const { tags, ...rest } = patch;
|
|
127
|
+
Object.assign(policy, rest);
|
|
128
|
+
if (tags) for (const tag of tags) policy.tags.add(tag);
|
|
129
|
+
return policy;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getRequestDynamicUsage(): AkanDynamicUsage | undefined {
|
|
133
|
+
return getRequestStore()?.dynamicUsage;
|
|
63
134
|
}
|
|
64
135
|
|
|
65
136
|
/** Deduplicates a promise-producing query within the active request. */
|
|
66
137
|
export function memoizeRequestQuery<T>(key: string, factory: () => Promise<T>): Promise<T> {
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
69
|
-
const
|
|
70
|
-
let cache = cacheMap.get(req);
|
|
71
|
-
if (!cache) {
|
|
72
|
-
cache = new Map<string, Promise<unknown>>();
|
|
73
|
-
cacheMap.set(req, cache);
|
|
74
|
-
}
|
|
75
|
-
const existing = cache.get(key);
|
|
138
|
+
const store = getRequestStore();
|
|
139
|
+
if (!store) return factory();
|
|
140
|
+
const existing = store.queryCache.get(key);
|
|
76
141
|
if (existing) return existing as Promise<T>;
|
|
77
142
|
const promise = factory();
|
|
78
|
-
|
|
143
|
+
store.queryCache.set(key, promise);
|
|
79
144
|
return promise;
|
|
80
145
|
}
|
|
81
146
|
|
|
82
147
|
/** Returns current request headers as a Map, or an empty Map outside a request. */
|
|
83
148
|
export function headers(): Map<string, string> {
|
|
84
|
-
const
|
|
149
|
+
const store = getRequestStore();
|
|
85
150
|
const map = new Map<string, string>();
|
|
86
|
-
if (!
|
|
87
|
-
|
|
151
|
+
if (!store) return map;
|
|
152
|
+
store.dynamicUsage.headers = true;
|
|
153
|
+
store.request.headers.forEach((value, key) => {
|
|
88
154
|
map.set(key, value);
|
|
89
155
|
});
|
|
90
156
|
return map;
|
|
@@ -121,7 +187,8 @@ export function parseCookieHeader(cookieHeader: string): Map<string, CookieEntry
|
|
|
121
187
|
|
|
122
188
|
/** Returns parsed cookies from the current request, or an empty Map outside a request. */
|
|
123
189
|
export function cookies(): Map<string, CookieEntry> {
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
126
|
-
|
|
190
|
+
const store = getRequestStore();
|
|
191
|
+
if (!store) return new Map();
|
|
192
|
+
store.dynamicUsage.cookies = true;
|
|
193
|
+
return parseCookieHeader(store.request.headers.get("cookie") ?? "");
|
|
127
194
|
}
|
package/package.json
CHANGED
package/server/akanApp.ts
CHANGED
|
@@ -84,6 +84,8 @@ export class AkanApp {
|
|
|
84
84
|
#logWriter: RotatingLogWriter | null = null;
|
|
85
85
|
#removeLogSink: (() => void) | null = null;
|
|
86
86
|
readonly #childOutputBuffers = new Map<string, string>();
|
|
87
|
+
readonly #childStderrBlockBuffers = new Map<string, string[]>();
|
|
88
|
+
readonly #childStderrBlockTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
87
89
|
static readonly #ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
88
90
|
#gatewayMetrics: AkanMetricsReport = {};
|
|
89
91
|
#proxyHopCount = 0;
|
|
@@ -1004,6 +1006,7 @@ export class AkanApp {
|
|
|
1004
1006
|
if (remaining) this.#writeChildOutput(idx, role, type, bufferKey, remaining);
|
|
1005
1007
|
} finally {
|
|
1006
1008
|
this.#flushChildOutput(idx, role, type, bufferKey);
|
|
1009
|
+
if (type === "stderr") this.#flushChildStderrBlock(idx, role, AkanApp.#childStderrBlockKey(idx, role));
|
|
1007
1010
|
}
|
|
1008
1011
|
}
|
|
1009
1012
|
|
|
@@ -1028,11 +1031,63 @@ export class AkanApp {
|
|
|
1028
1031
|
}
|
|
1029
1032
|
|
|
1030
1033
|
#writeChildOutputLine(idx: number, role: AkanChildRole, type: "stdout" | "stderr", line: string) {
|
|
1034
|
+
if (type === "stderr" && this.#bufferChildStderrLine(idx, role, line)) return;
|
|
1035
|
+
this.#writeChildOutputLineRaw(idx, role, type, line);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
#writeChildOutputLineRaw(idx: number, role: AkanChildRole, type: "stdout" | "stderr", line: string) {
|
|
1031
1039
|
const prefixedLine = `[child:${idx} ${role}] [${type}] ${line}`;
|
|
1032
1040
|
process[type].write(prefixedLine);
|
|
1033
1041
|
this.#logWriter?.write(`${idx}-${role}`, AkanApp.#stripAnsi(prefixedLine));
|
|
1034
1042
|
}
|
|
1035
1043
|
|
|
1044
|
+
#bufferChildStderrLine(idx: number, role: AkanChildRole, line: string): boolean {
|
|
1045
|
+
const key = AkanApp.#childStderrBlockKey(idx, role);
|
|
1046
|
+
const block = this.#childStderrBlockBuffers.get(key) ?? [];
|
|
1047
|
+
block.push(line);
|
|
1048
|
+
this.#childStderrBlockBuffers.set(key, block);
|
|
1049
|
+
|
|
1050
|
+
const existingTimer = this.#childStderrBlockTimers.get(key);
|
|
1051
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
1052
|
+
|
|
1053
|
+
if (line.trim() === "" || block.length >= 64) {
|
|
1054
|
+
this.#flushChildStderrBlock(idx, role, key);
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
this.#childStderrBlockTimers.set(
|
|
1059
|
+
key,
|
|
1060
|
+
setTimeout(() => this.#flushChildStderrBlock(idx, role, key), 50),
|
|
1061
|
+
);
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
#flushChildStderrBlock(idx: number, role: AkanChildRole, key: string) {
|
|
1066
|
+
const timer = this.#childStderrBlockTimers.get(key);
|
|
1067
|
+
if (timer) clearTimeout(timer);
|
|
1068
|
+
this.#childStderrBlockTimers.delete(key);
|
|
1069
|
+
|
|
1070
|
+
const block = this.#childStderrBlockBuffers.get(key);
|
|
1071
|
+
if (!block?.length) return;
|
|
1072
|
+
this.#childStderrBlockBuffers.delete(key);
|
|
1073
|
+
|
|
1074
|
+
const text = block.join("");
|
|
1075
|
+
if (AkanApp.#isBenignRsdwConnectionClosedBlock(text)) return;
|
|
1076
|
+
for (const blockLine of block) this.#writeChildOutputLineRaw(idx, role, "stderr", blockLine);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
static #childStderrBlockKey(idx: number, role: AkanChildRole): string {
|
|
1080
|
+
return `${idx}:${role}:stderr`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
static #isBenignRsdwConnectionClosedBlock(text: string): boolean {
|
|
1084
|
+
return (
|
|
1085
|
+
text.includes('reportGlobalError(weakResponse, Error("Connection closed."))') &&
|
|
1086
|
+
text.includes("error: Connection closed.") &&
|
|
1087
|
+
text.includes("react-server-dom-webpack")
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1036
1091
|
static #stripAnsi(msg: string) {
|
|
1037
1092
|
return msg.replace(AkanApp.#ansiPattern, "");
|
|
1038
1093
|
}
|
|
@@ -45,11 +45,11 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
if (msg.type === "rsc-refresh") {
|
|
48
|
-
|
|
48
|
+
refreshRsc(msg);
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
if (msg.type === "client-refresh") {
|
|
52
|
-
|
|
52
|
+
refreshClient(msg);
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
if (msg.type === "css-update") {
|
|
@@ -73,12 +73,6 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
73
73
|
setTimeout(connect, delay);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function reloadForHmr(msg){
|
|
77
|
-
try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}
|
|
78
|
-
if (msg && msg.buildId != null) lastBuildId = msg.buildId;
|
|
79
|
-
location.reload();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
76
|
function ensureOverlay(){
|
|
83
77
|
if (overlayEl && overlayLabelEl) return overlayEl;
|
|
84
78
|
if (!overlayStyleEl) {
|
package/server/rscClient.tsx
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createElement, type ReactNode, startTransition, use, useLayoutEffect, useState } from "react";
|
|
2
2
|
import { hydrateRoot } from "react-dom/client";
|
|
3
3
|
import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
|
|
4
|
+
import { isRscPayloadResponse } from "./rscHttp";
|
|
5
|
+
|
|
6
|
+
type InlineRscChunk = [1, string] | [3, string];
|
|
4
7
|
|
|
5
8
|
declare global {
|
|
6
|
-
var __RSC_CHUNKS__:
|
|
9
|
+
var __RSC_CHUNKS__: InlineRscChunk[] | undefined;
|
|
7
10
|
var __RSC_CLOSED__: boolean | undefined;
|
|
8
|
-
var __RSC_PUSH__: ((
|
|
11
|
+
var __RSC_PUSH__: ((type: InlineRscChunk[0], data: string) => void) | undefined;
|
|
9
12
|
var __RSC_CLOSE__: (() => void) | undefined;
|
|
10
13
|
var __AKAN_RSC_NAVIGATE__:
|
|
11
14
|
| ((href: string, options?: { replace?: boolean; scrollToTop?: boolean }) => Promise<void>)
|
|
@@ -21,15 +24,20 @@ function decodeBase64(b64: string): Uint8Array {
|
|
|
21
24
|
return bytes;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
function decodeInlineRscChunk([type, data]: InlineRscChunk): Uint8Array {
|
|
28
|
+
if (type === 1) return new TextEncoder().encode(data);
|
|
29
|
+
return decodeBase64(data);
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
type RscThenable = Promise<ReactNode>;
|
|
25
|
-
type RscFetchResult = { type: "rsc"; thenable: RscThenable } | { type: "redirected" };
|
|
33
|
+
type RscFetchResult = { type: "rsc"; thenable: RscThenable } | { type: "redirected"; status?: number };
|
|
26
34
|
const MAX_RSC_CACHE_ENTRIES = 32;
|
|
27
35
|
|
|
28
36
|
function createInitialRscStream(): ReadableStream<Uint8Array> {
|
|
29
37
|
return new ReadableStream<Uint8Array>({
|
|
30
38
|
start(controller) {
|
|
31
39
|
const queued = globalThis.__RSC_CHUNKS__ ?? [];
|
|
32
|
-
for (const
|
|
40
|
+
for (const chunk of queued) controller.enqueue(decodeInlineRscChunk(chunk));
|
|
33
41
|
globalThis.__RSC_CHUNKS__ = [];
|
|
34
42
|
|
|
35
43
|
if (globalThis.__RSC_CLOSED__) {
|
|
@@ -37,7 +45,7 @@ function createInitialRscStream(): ReadableStream<Uint8Array> {
|
|
|
37
45
|
return;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
globalThis.__RSC_PUSH__ = (
|
|
48
|
+
globalThis.__RSC_PUSH__ = (type, data) => controller.enqueue(decodeInlineRscChunk([type, data]));
|
|
41
49
|
globalThis.__RSC_CLOSE__ = () => controller.close();
|
|
42
50
|
},
|
|
43
51
|
});
|
|
@@ -63,10 +71,12 @@ async function fetchRsc(href: string, options: { buildId?: number } = {}): Promi
|
|
|
63
71
|
const redirect = res.headers.get("X-Akan-Redirect");
|
|
64
72
|
if (redirect) {
|
|
65
73
|
const method = res.headers.get("X-Akan-Redirect-Method");
|
|
74
|
+
const statusHeader = res.headers.get("X-Akan-Redirect-Status");
|
|
75
|
+
const status = statusHeader ? Number(statusHeader) : undefined;
|
|
66
76
|
await globalThis.__AKAN_RSC_NAVIGATE__?.(redirect, { replace: method !== "push", scrollToTop: true });
|
|
67
|
-
return { type: "redirected" };
|
|
77
|
+
return { type: "redirected", status };
|
|
68
78
|
}
|
|
69
|
-
if (!res
|
|
79
|
+
if (!isRscPayloadResponse(res)) throw new Error(`[rscClient] RSC fetch failed ${res.status} ${res.statusText}`);
|
|
70
80
|
|
|
71
81
|
const buffer = await res.arrayBuffer();
|
|
72
82
|
const completeStream = new ReadableStream<Uint8Array>({
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const RSC_CONTENT_TYPE = "text/x-component; charset=utf-8";
|
|
2
|
+
|
|
3
|
+
export function isRscPayloadResponse(res: Response): boolean {
|
|
4
|
+
if (!res.body) return false;
|
|
5
|
+
if (res.ok) return true;
|
|
6
|
+
return res.status === 404 && (res.headers.get("Content-Type") ?? "").toLowerCase().startsWith("text/x-component");
|
|
7
|
+
}
|