@bleedingdev/modern-js-server-core 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2
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/dist/cjs/adapters/node/hono.js +25 -16
- package/dist/cjs/adapters/node/node.js +3 -1
- package/dist/cjs/adapters/node/plugins/static.js +1 -1
- package/dist/cjs/helper.js +1 -1
- package/dist/cjs/plugins/log.js +2 -1
- package/dist/cjs/plugins/render/render.js +4 -4
- package/dist/cjs/plugins/render/ssrCache.js +96 -52
- package/dist/cjs/serverBase.js +2 -2
- package/dist/cjs/utils/transformStream.js +1 -1
- package/dist/esm/adapters/node/hono.mjs +25 -16
- package/dist/esm/adapters/node/node.mjs +3 -1
- package/dist/esm/adapters/node/plugins/static.mjs +1 -1
- package/dist/esm/helper.mjs +1 -1
- package/dist/esm/plugins/log.mjs +2 -1
- package/dist/esm/plugins/render/render.mjs +5 -11
- package/dist/esm/plugins/render/ssrCache.mjs +96 -52
- package/dist/esm/serverBase.mjs +2 -2
- package/dist/esm/utils/transformStream.mjs +1 -1
- package/dist/esm-node/adapters/node/hono.mjs +25 -16
- package/dist/esm-node/adapters/node/node.mjs +3 -1
- package/dist/esm-node/adapters/node/plugins/static.mjs +1 -1
- package/dist/esm-node/helper.mjs +1 -1
- package/dist/esm-node/plugins/log.mjs +2 -1
- package/dist/esm-node/plugins/render/render.mjs +5 -11
- package/dist/esm-node/plugins/render/ssrCache.mjs +96 -52
- package/dist/esm-node/serverBase.mjs +2 -2
- package/dist/esm-node/utils/transformStream.mjs +1 -1
- package/dist/types/adapters/node/plugins/static.d.ts +2 -2
- package/dist/types/types/config/bff.d.ts +1 -1
- package/dist/types/types/config/server.d.ts +8 -3
- package/dist/types/types/config/source.d.ts +1 -1
- package/dist/types/types/server.d.ts +2 -0
- package/package.json +22 -14
- package/dist/esm/rslib-runtime~1.mjs +0 -18
- package/dist/esm-node/rslib-runtime~0.mjs +0 -19
|
@@ -1,54 +1,88 @@
|
|
|
1
1
|
import { createMemoryStorage } from "@modern-js/runtime-utils/storer";
|
|
2
2
|
import { X_RENDER_CACHE } from "../../constants.mjs";
|
|
3
3
|
import { createTransformStream, getPathname } from "../../utils/index.mjs";
|
|
4
|
+
const preventsSharedCaching = (headers)=>/(?:^|,)\s*(?:private|no-store|no-cache)\s*(?:[=,]|$)/i.test(headers.get('cache-control') || '');
|
|
5
|
+
const isCacheableResponse = (response)=>200 === response.status && !preventsSharedCaching(response.headers) && !response.headers.has('set-cookie') && !response.headers.get('vary');
|
|
4
6
|
const removeTailSlash = (s)=>s.replace(/\/+$/, '');
|
|
5
7
|
const ZERO_RENDER_LEVEL = /"renderLevel":0/;
|
|
6
8
|
const NO_SSR_CACHE = /<meta\s+[^>]*name=["']no-ssr-cache["'][^>]*>/i;
|
|
7
9
|
async function processCache({ request, key, requestHandler, requestHandlerOptions, ttl, container, cacheStatus }) {
|
|
8
10
|
const response = await requestHandler(request, requestHandlerOptions);
|
|
9
11
|
const { onError } = requestHandlerOptions;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
const deleteCache = async ()=>{
|
|
13
|
+
try {
|
|
14
|
+
await container.delete(key);
|
|
15
|
+
} catch {
|
|
16
|
+
(onError || console.error)('[render-cache] delete cache failed');
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
if (!isCacheableResponse(response) || !response.body) {
|
|
20
|
+
await deleteCache();
|
|
21
|
+
return response;
|
|
22
|
+
}
|
|
23
|
+
const headers = Object.fromEntries(response.headers);
|
|
22
24
|
const decoder = new TextDecoder();
|
|
23
25
|
if (response.body) {
|
|
24
26
|
const stream = createTransformStream();
|
|
25
27
|
const reader = response.body.getReader();
|
|
26
28
|
const writer = stream.writable.getWriter();
|
|
27
29
|
let html = '';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
let cancelled = false;
|
|
31
|
+
const cancel = async (reason)=>{
|
|
32
|
+
cancelled = true;
|
|
33
|
+
await reader.cancel(reason);
|
|
34
|
+
};
|
|
35
|
+
writer.closed.catch(cancel).catch(()=>{});
|
|
36
|
+
const onAbort = ()=>{
|
|
37
|
+
Promise.allSettled([
|
|
38
|
+
cancel(request.signal.reason),
|
|
39
|
+
writer.abort(request.signal.reason)
|
|
40
|
+
]);
|
|
41
|
+
};
|
|
42
|
+
request.signal.addEventListener('abort', onAbort, {
|
|
43
|
+
once: true
|
|
44
|
+
});
|
|
45
|
+
if (request.signal.aborted) onAbort();
|
|
46
|
+
const pumping = (async ()=>{
|
|
47
|
+
try {
|
|
48
|
+
for(;;){
|
|
49
|
+
const { done, value } = await reader.read();
|
|
50
|
+
if (cancelled) return;
|
|
51
|
+
if (done) break;
|
|
52
|
+
html += decoder.decode(value, {
|
|
53
|
+
stream: true
|
|
54
|
+
});
|
|
55
|
+
await writer.write(value);
|
|
56
|
+
}
|
|
57
|
+
html += decoder.decode();
|
|
58
|
+
await writer.close();
|
|
59
|
+
if (cancelled) return;
|
|
60
|
+
if (ZERO_RENDER_LEVEL.test(html) || NO_SSR_CACHE.test(html)) return void await deleteCache();
|
|
61
|
+
const cache = {
|
|
62
|
+
val: html,
|
|
63
|
+
cursor: Date.now(),
|
|
64
|
+
headers
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
await container.set(key, JSON.stringify(cache), {
|
|
38
68
|
ttl
|
|
39
|
-
}).catch(()=>{
|
|
40
|
-
if (onError) onError(`[render-cache] set cache failed, key: ${key}, value: ${JSON.stringify(cache)}`);
|
|
41
|
-
else console.error(`[render-cache] set cache failed, key: ${key}, value: ${JSON.stringify(cache)}`);
|
|
42
69
|
});
|
|
43
|
-
|
|
44
|
-
|
|
70
|
+
} catch (error) {
|
|
71
|
+
(onError || console.error)('[render-cache] set cache failed');
|
|
45
72
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
} catch (error) {
|
|
74
|
+
await Promise.allSettled([
|
|
75
|
+
cancel(error),
|
|
76
|
+
writer.abort(error)
|
|
77
|
+
]);
|
|
78
|
+
if (cacheStatus) try {
|
|
79
|
+
(onError || console.error)('[render-cache] response stream failed');
|
|
80
|
+
} catch {}
|
|
81
|
+
} finally{
|
|
82
|
+
request.signal.removeEventListener('abort', onAbort);
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
pumping.catch(()=>{});
|
|
52
86
|
cacheStatus && response.headers.set(X_RENDER_CACHE, cacheStatus);
|
|
53
87
|
return new Response(stream.readable, {
|
|
54
88
|
status: response.status,
|
|
@@ -63,9 +97,12 @@ function computedKey(req, cacheControl) {
|
|
|
63
97
|
const pathname = getPathname(req);
|
|
64
98
|
const { customKey } = cacheControl;
|
|
65
99
|
const defaultKey = '/' === pathname ? pathname : removeTailSlash(pathname);
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
if (customKey) if ('string' == typeof customKey) return customKey;
|
|
101
|
+
else return customKey(defaultKey);
|
|
102
|
+
else {
|
|
103
|
+
const url = new URL(req.url);
|
|
104
|
+
return `${url.origin}${defaultKey}${url.search}`;
|
|
105
|
+
}
|
|
69
106
|
}
|
|
70
107
|
function shouldUseCache(request) {
|
|
71
108
|
const url = new URL(request.url);
|
|
@@ -76,7 +113,7 @@ function shouldUseCache(request) {
|
|
|
76
113
|
}
|
|
77
114
|
function matchCacheControl(cacheOption, req) {
|
|
78
115
|
if (!cacheOption || !req) return;
|
|
79
|
-
{
|
|
116
|
+
else {
|
|
80
117
|
if (isCacheControl(cacheOption)) return cacheOption;
|
|
81
118
|
if (isCacheOptionProvider(cacheOption)) return cacheOption(req);
|
|
82
119
|
const url = req.url;
|
|
@@ -95,13 +132,15 @@ function matchCacheControl(cacheOption, req) {
|
|
|
95
132
|
async function getCacheResult(request, options) {
|
|
96
133
|
const { cacheControl, container = storage, requestHandler, requestHandlerOptions } = options;
|
|
97
134
|
const { onError } = requestHandlerOptions;
|
|
98
|
-
const
|
|
135
|
+
const hasCredentials = request.headers.has('cookie') || request.headers.has('authorization');
|
|
136
|
+
if ('GET' !== request.method || !shouldUseCache(request) || preventsSharedCaching(request.headers) || hasCredentials && !cacheControl.customKey) return requestHandler(request, requestHandlerOptions);
|
|
137
|
+
const key = `${CACHE_NAMESPACE}:v2:${computedKey(request, cacheControl)}`;
|
|
99
138
|
let value;
|
|
100
139
|
try {
|
|
101
140
|
value = await container.get(key);
|
|
102
141
|
} catch (_) {
|
|
103
|
-
if (onError) onError(
|
|
104
|
-
else console.error(
|
|
142
|
+
if (onError) onError('[render-cache] get cache failed');
|
|
143
|
+
else console.error('[render-cache] get cache failed');
|
|
105
144
|
value = void 0;
|
|
106
145
|
}
|
|
107
146
|
const { maxAge, staleWhileRevalidate } = cacheControl;
|
|
@@ -115,27 +154,27 @@ async function getCacheResult(request, options) {
|
|
|
115
154
|
container,
|
|
116
155
|
cacheStatus: 'miss'
|
|
117
156
|
});
|
|
118
|
-
{
|
|
157
|
+
else {
|
|
119
158
|
const cache = JSON.parse(value);
|
|
120
159
|
const interval = Date.now() - cache.cursor;
|
|
121
160
|
if (interval <= maxAge) {
|
|
122
161
|
const cacheStatus = 'hit';
|
|
123
162
|
return new Response(cache.val, {
|
|
124
163
|
headers: {
|
|
164
|
+
...cache.headers,
|
|
125
165
|
[X_RENDER_CACHE]: cacheStatus
|
|
126
166
|
}
|
|
127
167
|
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
{
|
|
168
|
+
} else {
|
|
169
|
+
if (!(interval <= staleWhileRevalidate + maxAge)) return processCache({
|
|
170
|
+
key,
|
|
171
|
+
request,
|
|
172
|
+
requestHandler,
|
|
173
|
+
requestHandlerOptions,
|
|
174
|
+
ttl,
|
|
175
|
+
container,
|
|
176
|
+
cacheStatus: 'expired'
|
|
177
|
+
});
|
|
139
178
|
processCache({
|
|
140
179
|
key,
|
|
141
180
|
request,
|
|
@@ -145,10 +184,15 @@ async function getCacheResult(request, options) {
|
|
|
145
184
|
container
|
|
146
185
|
}).then(async (response)=>{
|
|
147
186
|
await response.text();
|
|
187
|
+
}).catch((error)=>{
|
|
188
|
+
try {
|
|
189
|
+
(onError || console.error)('[render-cache] revalidation failed');
|
|
190
|
+
} catch {}
|
|
148
191
|
});
|
|
149
192
|
const cacheStatus = 'stale';
|
|
150
193
|
return new Response(cache.val, {
|
|
151
194
|
headers: {
|
|
195
|
+
...cache.headers,
|
|
152
196
|
[X_RENDER_CACHE]: cacheStatus
|
|
153
197
|
}
|
|
154
198
|
});
|
package/dist/esm/serverBase.mjs
CHANGED
|
@@ -146,7 +146,7 @@ function applyMiddlewares() {
|
|
|
146
146
|
if (middleware.before) {
|
|
147
147
|
const targetIndex = finalMiddlewares.findIndex((item)=>{
|
|
148
148
|
if (middleware.before?.includes(item.name)) return true;
|
|
149
|
-
return false;
|
|
149
|
+
else return false;
|
|
150
150
|
});
|
|
151
151
|
if (-1 !== targetIndex) finalMiddlewares.splice(targetIndex, 0, middleware);
|
|
152
152
|
else finalMiddlewares.push(middleware);
|
|
@@ -167,7 +167,7 @@ function applyMiddlewares() {
|
|
|
167
167
|
}
|
|
168
168
|
function handler2Handlers(handler) {
|
|
169
169
|
if (Array.isArray(handler)) return handler;
|
|
170
|
-
return [
|
|
170
|
+
else return [
|
|
171
171
|
handler
|
|
172
172
|
];
|
|
173
173
|
}
|
|
@@ -14,7 +14,7 @@ function transformResponse(response, transformCb) {
|
|
|
14
14
|
if (response.body) {
|
|
15
15
|
const stream = createTransformStream(async (before)=>{
|
|
16
16
|
if (Array.isArray(transformCb)) return transformCb.reduce(async (before, cb)=>cb(await before), Promise.resolve(before));
|
|
17
|
-
return transformCb(before);
|
|
17
|
+
else return transformCb(before);
|
|
18
18
|
});
|
|
19
19
|
response.body.pipeThrough(stream);
|
|
20
20
|
readable = stream.readable;
|
|
@@ -30,27 +30,36 @@ const httpCallBack2HonoMid = (handler)=>async (context, next)=>{
|
|
|
30
30
|
} else await next();
|
|
31
31
|
};
|
|
32
32
|
const noop = ()=>{};
|
|
33
|
-
const connectMid2HonoMid = (handler)=>async (context, next)=>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const connectMid2HonoMid = (handler)=>async (context, next)=>{
|
|
34
|
+
const { req, res: rawRes } = context.env.node;
|
|
35
|
+
const res = rawRes;
|
|
36
|
+
if (handler.length < 3) return handler(req, res, noop);
|
|
37
|
+
const continueChain = await new Promise((resolve, reject)=>{
|
|
38
|
+
const onFinish = ()=>{
|
|
39
|
+
context.res;
|
|
40
|
+
context.finalized = true;
|
|
41
|
+
resolve(false);
|
|
42
|
+
};
|
|
43
|
+
res.once?.('finish', onFinish);
|
|
44
|
+
handler(req, res, (err)=>{
|
|
45
|
+
res.removeListener?.('finish', onFinish);
|
|
37
46
|
if (err) reject(err);
|
|
38
|
-
else resolve(
|
|
47
|
+
else resolve(true);
|
|
39
48
|
});
|
|
40
49
|
});
|
|
41
|
-
|
|
50
|
+
if (continueChain) await next();
|
|
51
|
+
};
|
|
52
|
+
const connectMockMid2HonoMid = (handler)=>{
|
|
53
|
+
if (handler.length >= 3) return connectMid2HonoMid(handler);
|
|
54
|
+
return async (context, _next)=>new Promise((resolve)=>{
|
|
42
55
|
const { req, res: rawRes } = context.env.node;
|
|
43
56
|
const res = rawRes;
|
|
44
|
-
|
|
45
|
-
res
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
resolve();
|
|
49
|
-
});
|
|
50
|
-
handler(req, res, noop);
|
|
51
|
-
} else handler(req, res, (err)=>{
|
|
52
|
-
if (err) reject(err);
|
|
53
|
-
else resolve(next());
|
|
57
|
+
res.once?.('finish', ()=>{
|
|
58
|
+
context.res;
|
|
59
|
+
context.finalized = true;
|
|
60
|
+
resolve();
|
|
54
61
|
});
|
|
62
|
+
handler(req, res, noop);
|
|
55
63
|
});
|
|
64
|
+
};
|
|
56
65
|
export { connectMid2HonoMid, connectMockMid2HonoMid, httpCallBack2HonoMid };
|
|
@@ -25,7 +25,9 @@ const createWebRequest = (req, res, body)=>{
|
|
|
25
25
|
headers: headerRecord,
|
|
26
26
|
signal: controller.signal
|
|
27
27
|
};
|
|
28
|
-
res.on('close', ()=>
|
|
28
|
+
res.on('close', ()=>{
|
|
29
|
+
if (!res.writableFinished || req.aborted) controller.abort('res closed');
|
|
30
|
+
});
|
|
29
31
|
const protocol = 'encrypted' in req.socket && req.socket.encrypted ? 'https' : 'http';
|
|
30
32
|
const authority = req.headers[':authority'] ?? req.headers.host;
|
|
31
33
|
const url = `${protocol}://${authority}${req.url}`;
|
|
@@ -155,7 +155,7 @@ function createStaticMiddleware(options) {
|
|
|
155
155
|
respondAsset: options.respondAsset,
|
|
156
156
|
respondPublicFallback: options.respondPublicFallback
|
|
157
157
|
})(c, next);
|
|
158
|
-
{
|
|
158
|
+
else {
|
|
159
159
|
const filepath = path.join(pwd, pathname.replace(pathPrefix, ()=>''));
|
|
160
160
|
if (!isPathInside(filepath, pwd)) return next();
|
|
161
161
|
if (!await fs.pathExists(filepath)) return next();
|
package/dist/esm-node/helper.mjs
CHANGED
|
@@ -3,7 +3,7 @@ const LOADER_CONTEXT = 'loaderContext';
|
|
|
3
3
|
function getLoaderCtx(c) {
|
|
4
4
|
const loaderContext = c.get(LOADER_CONTEXT);
|
|
5
5
|
if (loaderContext) return loaderContext;
|
|
6
|
-
{
|
|
6
|
+
else {
|
|
7
7
|
const loaderContext = new Map();
|
|
8
8
|
c.set(LOADER_CONTEXT, loaderContext);
|
|
9
9
|
return loaderContext;
|
|
@@ -41,7 +41,8 @@ function logHandler() {
|
|
|
41
41
|
log(logFn, "<--", method, path);
|
|
42
42
|
const start = Date.now();
|
|
43
43
|
await next();
|
|
44
|
-
|
|
44
|
+
const nodeRes = c?.env?.node?.res;
|
|
45
|
+
log(logFn, "-->", method, path, nodeRes?.headersSent ? nodeRes.statusCode : c.res.status, time(start));
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
const logPlugin = ()=>({
|
|
@@ -8,14 +8,8 @@ import { dataHandler } from "./dataHandler.mjs";
|
|
|
8
8
|
import { renderRscHandler } from "./renderRscHandler.mjs";
|
|
9
9
|
import { serverActionHandler } from "./serverActionHandler.mjs";
|
|
10
10
|
import { ssrRender } from "./ssrRender.mjs";
|
|
11
|
-
import { __webpack_require__ } from "../../rslib-runtime~0.mjs";
|
|
12
11
|
import { createRequire as __rspack_createRequire } from "node:module";
|
|
13
12
|
const __rspack_createRequire_require = __rspack_createRequire(import.meta.url);
|
|
14
|
-
__webpack_require__.add({
|
|
15
|
-
"@modern-js/runtime-utils/router" (module) {
|
|
16
|
-
module.exports = __rspack_createRequire_require("@modern-js/runtime-utils/router");
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
13
|
const DYNAMIC_ROUTE_REG = /\/:./;
|
|
20
14
|
function getRouter(routes) {
|
|
21
15
|
const dynamicRoutes = [];
|
|
@@ -48,8 +42,7 @@ function matchRoute(router, pathname, entryName) {
|
|
|
48
42
|
void 0,
|
|
49
43
|
{}
|
|
50
44
|
];
|
|
51
|
-
}
|
|
52
|
-
{
|
|
45
|
+
} else {
|
|
53
46
|
const result = matchedRoutes[0];
|
|
54
47
|
if (!result) return [
|
|
55
48
|
void 0,
|
|
@@ -74,6 +67,7 @@ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig
|
|
|
74
67
|
return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, serverContext, contextForceCSR, reporter })=>{
|
|
75
68
|
const forMatchpathname = matchPathname ?? getPathname(req);
|
|
76
69
|
const [routeInfo, params] = matchRoute(router, forMatchpathname, matchEntryName);
|
|
70
|
+
serverContext?.set('renderRoute', routeInfo);
|
|
77
71
|
const framework = cutNameByHyphen(metaName || 'modern-js');
|
|
78
72
|
const fallbackHeader = `x-${framework}-ssr-fallback`;
|
|
79
73
|
let fallbackReason = null;
|
|
@@ -166,7 +160,7 @@ async function renderHandler(request, options, mode, fallbackWrapper, framework)
|
|
|
166
160
|
const { nestedRoutesJson } = serverManifest;
|
|
167
161
|
const routes = nestedRoutesJson?.[options.routeInfo.entryName];
|
|
168
162
|
if (routes) {
|
|
169
|
-
const { matchRoutes } =
|
|
163
|
+
const { matchRoutes } = __rspack_createRequire_require("@modern-js/runtime-utils/router");
|
|
170
164
|
const url = new URL(request.url);
|
|
171
165
|
const matchedRoutes = matchRoutes(routes, url.pathname, options.routeInfo.urlPath);
|
|
172
166
|
if (matchedRoutes) {
|
|
@@ -204,7 +198,7 @@ async function getRenderMode(req, fallbackHeader, isSSR, forceCSR, nodeReq, onFa
|
|
|
204
198
|
if (req.headers.get('x-rsc-action')) return 'rsc-action';
|
|
205
199
|
if (req.headers.get('x-rsc-tree')) return 'rsc-tree';
|
|
206
200
|
if (!isSSR) return 'csr';
|
|
207
|
-
{
|
|
201
|
+
else {
|
|
208
202
|
if (query.__loader) return 'data';
|
|
209
203
|
const fallbackHeaderValue = req.headers.get(fallbackHeader) || nodeReq?.headers[fallbackHeader];
|
|
210
204
|
if (forceCSR && (query.csr || fallbackHeaderValue || contextForceCSR)) {
|
|
@@ -233,6 +227,6 @@ async function csrRender(request, options) {
|
|
|
233
227
|
[X_MODERNJS_RENDER]: 'client'
|
|
234
228
|
})
|
|
235
229
|
});
|
|
236
|
-
return csrRscRender(request, options);
|
|
230
|
+
else return csrRscRender(request, options);
|
|
237
231
|
}
|
|
238
232
|
export { createRender, matchRoute };
|
|
@@ -2,54 +2,88 @@ import "node:module";
|
|
|
2
2
|
import { createMemoryStorage } from "@modern-js/runtime-utils/storer";
|
|
3
3
|
import { X_RENDER_CACHE } from "../../constants.mjs";
|
|
4
4
|
import { createTransformStream, getPathname } from "../../utils/index.mjs";
|
|
5
|
+
const preventsSharedCaching = (headers)=>/(?:^|,)\s*(?:private|no-store|no-cache)\s*(?:[=,]|$)/i.test(headers.get('cache-control') || '');
|
|
6
|
+
const isCacheableResponse = (response)=>200 === response.status && !preventsSharedCaching(response.headers) && !response.headers.has('set-cookie') && !response.headers.get('vary');
|
|
5
7
|
const removeTailSlash = (s)=>s.replace(/\/+$/, '');
|
|
6
8
|
const ZERO_RENDER_LEVEL = /"renderLevel":0/;
|
|
7
9
|
const NO_SSR_CACHE = /<meta\s+[^>]*name=["']no-ssr-cache["'][^>]*>/i;
|
|
8
10
|
async function processCache({ request, key, requestHandler, requestHandlerOptions, ttl, container, cacheStatus }) {
|
|
9
11
|
const response = await requestHandler(request, requestHandlerOptions);
|
|
10
12
|
const { onError } = requestHandlerOptions;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
const deleteCache = async ()=>{
|
|
14
|
+
try {
|
|
15
|
+
await container.delete(key);
|
|
16
|
+
} catch {
|
|
17
|
+
(onError || console.error)('[render-cache] delete cache failed');
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
if (!isCacheableResponse(response) || !response.body) {
|
|
21
|
+
await deleteCache();
|
|
22
|
+
return response;
|
|
23
|
+
}
|
|
24
|
+
const headers = Object.fromEntries(response.headers);
|
|
23
25
|
const decoder = new TextDecoder();
|
|
24
26
|
if (response.body) {
|
|
25
27
|
const stream = createTransformStream();
|
|
26
28
|
const reader = response.body.getReader();
|
|
27
29
|
const writer = stream.writable.getWriter();
|
|
28
30
|
let html = '';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
const cancel = async (reason)=>{
|
|
33
|
+
cancelled = true;
|
|
34
|
+
await reader.cancel(reason);
|
|
35
|
+
};
|
|
36
|
+
writer.closed.catch(cancel).catch(()=>{});
|
|
37
|
+
const onAbort = ()=>{
|
|
38
|
+
Promise.allSettled([
|
|
39
|
+
cancel(request.signal.reason),
|
|
40
|
+
writer.abort(request.signal.reason)
|
|
41
|
+
]);
|
|
42
|
+
};
|
|
43
|
+
request.signal.addEventListener('abort', onAbort, {
|
|
44
|
+
once: true
|
|
45
|
+
});
|
|
46
|
+
if (request.signal.aborted) onAbort();
|
|
47
|
+
const pumping = (async ()=>{
|
|
48
|
+
try {
|
|
49
|
+
for(;;){
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (cancelled) return;
|
|
52
|
+
if (done) break;
|
|
53
|
+
html += decoder.decode(value, {
|
|
54
|
+
stream: true
|
|
55
|
+
});
|
|
56
|
+
await writer.write(value);
|
|
57
|
+
}
|
|
58
|
+
html += decoder.decode();
|
|
59
|
+
await writer.close();
|
|
60
|
+
if (cancelled) return;
|
|
61
|
+
if (ZERO_RENDER_LEVEL.test(html) || NO_SSR_CACHE.test(html)) return void await deleteCache();
|
|
62
|
+
const cache = {
|
|
63
|
+
val: html,
|
|
64
|
+
cursor: Date.now(),
|
|
65
|
+
headers
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
await container.set(key, JSON.stringify(cache), {
|
|
39
69
|
ttl
|
|
40
|
-
}).catch(()=>{
|
|
41
|
-
if (onError) onError(`[render-cache] set cache failed, key: ${key}, value: ${JSON.stringify(cache)}`);
|
|
42
|
-
else console.error(`[render-cache] set cache failed, key: ${key}, value: ${JSON.stringify(cache)}`);
|
|
43
70
|
});
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
} catch (error) {
|
|
72
|
+
(onError || console.error)('[render-cache] set cache failed');
|
|
46
73
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await Promise.allSettled([
|
|
76
|
+
cancel(error),
|
|
77
|
+
writer.abort(error)
|
|
78
|
+
]);
|
|
79
|
+
if (cacheStatus) try {
|
|
80
|
+
(onError || console.error)('[render-cache] response stream failed');
|
|
81
|
+
} catch {}
|
|
82
|
+
} finally{
|
|
83
|
+
request.signal.removeEventListener('abort', onAbort);
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
pumping.catch(()=>{});
|
|
53
87
|
cacheStatus && response.headers.set(X_RENDER_CACHE, cacheStatus);
|
|
54
88
|
return new Response(stream.readable, {
|
|
55
89
|
status: response.status,
|
|
@@ -64,9 +98,12 @@ function computedKey(req, cacheControl) {
|
|
|
64
98
|
const pathname = getPathname(req);
|
|
65
99
|
const { customKey } = cacheControl;
|
|
66
100
|
const defaultKey = '/' === pathname ? pathname : removeTailSlash(pathname);
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
101
|
+
if (customKey) if ('string' == typeof customKey) return customKey;
|
|
102
|
+
else return customKey(defaultKey);
|
|
103
|
+
else {
|
|
104
|
+
const url = new URL(req.url);
|
|
105
|
+
return `${url.origin}${defaultKey}${url.search}`;
|
|
106
|
+
}
|
|
70
107
|
}
|
|
71
108
|
function shouldUseCache(request) {
|
|
72
109
|
const url = new URL(request.url);
|
|
@@ -77,7 +114,7 @@ function shouldUseCache(request) {
|
|
|
77
114
|
}
|
|
78
115
|
function matchCacheControl(cacheOption, req) {
|
|
79
116
|
if (!cacheOption || !req) return;
|
|
80
|
-
{
|
|
117
|
+
else {
|
|
81
118
|
if (isCacheControl(cacheOption)) return cacheOption;
|
|
82
119
|
if (isCacheOptionProvider(cacheOption)) return cacheOption(req);
|
|
83
120
|
const url = req.url;
|
|
@@ -96,13 +133,15 @@ function matchCacheControl(cacheOption, req) {
|
|
|
96
133
|
async function getCacheResult(request, options) {
|
|
97
134
|
const { cacheControl, container = storage, requestHandler, requestHandlerOptions } = options;
|
|
98
135
|
const { onError } = requestHandlerOptions;
|
|
99
|
-
const
|
|
136
|
+
const hasCredentials = request.headers.has('cookie') || request.headers.has('authorization');
|
|
137
|
+
if ('GET' !== request.method || !shouldUseCache(request) || preventsSharedCaching(request.headers) || hasCredentials && !cacheControl.customKey) return requestHandler(request, requestHandlerOptions);
|
|
138
|
+
const key = `${CACHE_NAMESPACE}:v2:${computedKey(request, cacheControl)}`;
|
|
100
139
|
let value;
|
|
101
140
|
try {
|
|
102
141
|
value = await container.get(key);
|
|
103
142
|
} catch (_) {
|
|
104
|
-
if (onError) onError(
|
|
105
|
-
else console.error(
|
|
143
|
+
if (onError) onError('[render-cache] get cache failed');
|
|
144
|
+
else console.error('[render-cache] get cache failed');
|
|
106
145
|
value = void 0;
|
|
107
146
|
}
|
|
108
147
|
const { maxAge, staleWhileRevalidate } = cacheControl;
|
|
@@ -116,27 +155,27 @@ async function getCacheResult(request, options) {
|
|
|
116
155
|
container,
|
|
117
156
|
cacheStatus: 'miss'
|
|
118
157
|
});
|
|
119
|
-
{
|
|
158
|
+
else {
|
|
120
159
|
const cache = JSON.parse(value);
|
|
121
160
|
const interval = Date.now() - cache.cursor;
|
|
122
161
|
if (interval <= maxAge) {
|
|
123
162
|
const cacheStatus = 'hit';
|
|
124
163
|
return new Response(cache.val, {
|
|
125
164
|
headers: {
|
|
165
|
+
...cache.headers,
|
|
126
166
|
[X_RENDER_CACHE]: cacheStatus
|
|
127
167
|
}
|
|
128
168
|
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{
|
|
169
|
+
} else {
|
|
170
|
+
if (!(interval <= staleWhileRevalidate + maxAge)) return processCache({
|
|
171
|
+
key,
|
|
172
|
+
request,
|
|
173
|
+
requestHandler,
|
|
174
|
+
requestHandlerOptions,
|
|
175
|
+
ttl,
|
|
176
|
+
container,
|
|
177
|
+
cacheStatus: 'expired'
|
|
178
|
+
});
|
|
140
179
|
processCache({
|
|
141
180
|
key,
|
|
142
181
|
request,
|
|
@@ -146,10 +185,15 @@ async function getCacheResult(request, options) {
|
|
|
146
185
|
container
|
|
147
186
|
}).then(async (response)=>{
|
|
148
187
|
await response.text();
|
|
188
|
+
}).catch((error)=>{
|
|
189
|
+
try {
|
|
190
|
+
(onError || console.error)('[render-cache] revalidation failed');
|
|
191
|
+
} catch {}
|
|
149
192
|
});
|
|
150
193
|
const cacheStatus = 'stale';
|
|
151
194
|
return new Response(cache.val, {
|
|
152
195
|
headers: {
|
|
196
|
+
...cache.headers,
|
|
153
197
|
[X_RENDER_CACHE]: cacheStatus
|
|
154
198
|
}
|
|
155
199
|
});
|
|
@@ -147,7 +147,7 @@ function applyMiddlewares() {
|
|
|
147
147
|
if (middleware.before) {
|
|
148
148
|
const targetIndex = finalMiddlewares.findIndex((item)=>{
|
|
149
149
|
if (middleware.before?.includes(item.name)) return true;
|
|
150
|
-
return false;
|
|
150
|
+
else return false;
|
|
151
151
|
});
|
|
152
152
|
if (-1 !== targetIndex) finalMiddlewares.splice(targetIndex, 0, middleware);
|
|
153
153
|
else finalMiddlewares.push(middleware);
|
|
@@ -168,7 +168,7 @@ function applyMiddlewares() {
|
|
|
168
168
|
}
|
|
169
169
|
function handler2Handlers(handler) {
|
|
170
170
|
if (Array.isArray(handler)) return handler;
|
|
171
|
-
return [
|
|
171
|
+
else return [
|
|
172
172
|
handler
|
|
173
173
|
];
|
|
174
174
|
}
|
|
@@ -15,7 +15,7 @@ function transformResponse(response, transformCb) {
|
|
|
15
15
|
if (response.body) {
|
|
16
16
|
const stream = createTransformStream(async (before)=>{
|
|
17
17
|
if (Array.isArray(transformCb)) return transformCb.reduce(async (before, cb)=>cb(await before), Promise.resolve(before));
|
|
18
|
-
return transformCb(before);
|
|
18
|
+
else return transformCb(before);
|
|
19
19
|
});
|
|
20
20
|
response.body.pipeThrough(stream);
|
|
21
21
|
readable = stream.readable;
|