Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support x-render-cache response headers when open ssr cache #5466

Merged
merged 3 commits into from Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/mighty-bears-rescue.md
@@ -0,0 +1,6 @@
---
'@modern-js/prod-server': minor
---

feat: support `x-render-cache` response headers when open ssr cache
feat: 当开启 ssr 缓存时,支持 `x-render-cache` 响应头
11 changes: 10 additions & 1 deletion packages/server/prod-server/src/libs/render/ssr.ts
Expand Up @@ -93,7 +93,11 @@ export const render = async (
const serverRender: RenderFunction =
bundleJSContent[SERVER_RENDER_FUNCTION_NAME];

const content = await ssrCache(ctx.req, serverRender, context);
const { data: content, status: cacheStatus } = await ssrCache(
ctx.req,
serverRender,
context,
);

const { url, status = 302 } = context.redirection;

Expand All @@ -106,10 +110,14 @@ export const render = async (
};
}

const headers: Record<string, string> = {};
cacheStatus && (headers['x-render-cache'] = cacheStatus);

if (typeof content === 'string') {
return {
content: injectServerData(content, ctx),
contentType: mime.contentType('html') as string,
headers,
};
} else {
let contentStream = injectServerDataStream(content, ctx);
Expand All @@ -130,6 +138,7 @@ export const render = async (
content: '',
contentStream,
contentType: mime.contentType('html') as string,
headers,
};
}
};
19 changes: 13 additions & 6 deletions packages/server/prod-server/src/libs/render/ssrCache/index.ts
@@ -1,5 +1,5 @@
import { IncomingMessage } from 'http';
import { Transform, type Readable } from 'stream';
import { Transform } from 'stream';
import {
CacheControl,
CacheOption,
Expand All @@ -8,15 +8,15 @@ import {
import { createMemoryStorage } from '@modern-js/runtime-utils/storer';
import { RenderFunction, SSRServerContext } from '../type';
import { cacheMod } from './cacheMod';
import { CacheManager } from './manager';
import { CacheManager, CacheResult } from './manager';

const cacheStorage = createMemoryStorage<string>('__ssr__cache');

export async function ssrCache(
req: IncomingMessage,
render: RenderFunction,
ssrContext: SSRServerContext,
): Promise<string | Readable> {
): Promise<CacheResult> {
const { customContainer, cacheOption } = cacheMod;
const cacheControl = await matchCacheControl(req, cacheOption);
const cacheManager = new CacheManager(
Expand All @@ -28,17 +28,24 @@ export async function ssrCache(
} else {
const renderResult = await render(ssrContext);
if (!renderResult) {
return '';
return {
data: '',
};
} else if (typeof renderResult === 'string') {
return renderResult;
return {
data: renderResult,
};
} else {
const stream = new Transform({
write(chunk, _, callback) {
this.push(chunk);
callback();
},
});
return renderResult(stream);
const data = await renderResult(stream);
return {
data,
};
}
}
}
Expand Down
28 changes: 20 additions & 8 deletions packages/server/prod-server/src/libs/render/ssrCache/manager.ts
Expand Up @@ -3,6 +3,12 @@ import { Readable, Transform } from 'stream';
import { CacheControl, Container } from '@modern-js/types';
import { RenderFunction, SSRServerContext } from '../type';

type CacheStatus = 'hit' | 'stale' | 'expired' | 'miss';
export type CacheResult = {
data: string | Readable;
status?: CacheStatus;
};

interface CacheStruct {
val: string;
cursor: number;
Expand All @@ -20,7 +26,7 @@ export class CacheManager {
cacheControl: CacheControl,
render: RenderFunction,
ssrContext: SSRServerContext,
): Promise<string | Readable> {
): Promise<CacheResult> {
const key = this.computedKey(req, cacheControl);

const value = await this.container.get(key);
Expand All @@ -34,20 +40,20 @@ export class CacheManager {

if (interval <= maxAge) {
// the cache is validate
return cache.val;
return { data: cache.val, status: 'hit' };
} else if (interval <= staleWhileRevalidate + maxAge) {
// the cache is stale while revalidate

// we shouldn't await this promise.
this.processCache(key, render, ssrContext, ttl);

return cache.val;
return { data: cache.val, status: 'stale' };
} else {
// the cache is invalidate
return this.processCache(key, render, ssrContext, ttl);
return this.processCache(key, render, ssrContext, ttl, 'expired');
}
} else {
return this.processCache(key, render, ssrContext, ttl);
return this.processCache(key, render, ssrContext, ttl, 'miss');
}
}

Expand All @@ -56,19 +62,20 @@ export class CacheManager {
render: RenderFunction,
ssrContext: SSRServerContext,
ttl: number,
status?: CacheStatus,
) {
const renderResult = await render(ssrContext);

if (!renderResult) {
return '';
return { data: '' };
} else if (typeof renderResult === 'string') {
const current = Date.now();
const cache: CacheStruct = {
val: renderResult,
cursor: current,
};
await this.container.set(key, JSON.stringify(cache), { ttl });
return renderResult;
return { data: renderResult, status };
} else {
let html = '';
const stream = new Transform({
Expand All @@ -88,7 +95,12 @@ export class CacheManager {
this.container.set(key, JSON.stringify(cache), { ttl });
});

return renderResult(stream);
const readable = await renderResult(stream);

return {
data: readable,
status,
};
}
}

Expand Down
10 changes: 9 additions & 1 deletion packages/server/prod-server/src/server/modernServer.ts
Expand Up @@ -590,7 +590,15 @@ export class ModernServer implements ModernServerInterface {
return;
}

const { contentStream: responseStream } = renderResult;
const { contentStream: responseStream, headers } = renderResult;

if (headers) {
for (const name in headers) {
const value = headers[name];
res.setHeader(name, value);
}
}

let { content: response } = renderResult;
if (route.entryName && responseStream) {
responseStream.pipe(res);
Expand Down
1 change: 1 addition & 0 deletions packages/server/prod-server/src/type.ts
Expand Up @@ -58,6 +58,7 @@ export type RenderResult = {
contentType: string;
contentStream?: Readable;
statusCode?: number;
headers?: Record<string, string>;
redirect?: boolean;
};

Expand Down
27 changes: 24 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion tests/integration/ssr/package.json
Expand Up @@ -4,6 +4,7 @@
"version": "2.9.0",
"dependencies": {
"@types/jest": "^29",
"@types/node": "^14"
"@types/node": "^14",
"axios": "^1.6.0"
}
}
8 changes: 8 additions & 0 deletions tests/integration/ssr/tests/base.test.ts
Expand Up @@ -2,6 +2,7 @@ import dns from 'node:dns';
import path, { join } from 'path';
import puppeteer, { Browser, Page } from 'puppeteer';
import { fs } from '@modern-js/utils';
import axios from 'axios';
import {
launchApp,
getPort,
Expand Down Expand Up @@ -124,4 +125,11 @@ describe('Traditional SSR', () => {
const content1 = await page.content();
expect(content1).toMatch(result);
});

test('x-render-cache http header', async () => {
const response = await axios.get(`http://localhost:${appPort}`);

const { headers } = response;
expect(Boolean(headers['x-render-cache'])).toBeTruthy();
});
});