Skip to content

Commit

Permalink
fix: ssr cache should return response header
Browse files Browse the repository at this point in the history
  • Loading branch information
GiveMe-A-Name committed Mar 18, 2024
1 parent a6a3159 commit 76948c3
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 40 deletions.
1 change: 1 addition & 0 deletions packages/server/core/src/base/adapters/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export {
registerMockHandlers,
injectServerManifest,
injectTemplates,
getHtmlTemplates,
} from './middlewares';
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ServerRoute } from '@modern-js/types';
import { fileReader } from '@modern-js/runtime-utils/fileReader';
import { HonoMiddleware, ServerEnv } from '../../../../core/server';

async function getHtmlTemplates(pwd: string, routes: ServerRoute[]) {
export async function getHtmlTemplates(pwd: string, routes: ServerRoute[]) {
const htmls = await Promise.all(
routes.map(async route => {
let html: string | undefined;
Expand Down
2 changes: 2 additions & 0 deletions packages/server/core/src/base/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export enum ServerReportTimings {
SERVER_HOOK_AFTER_RENDER = 'server-hook-after-render',
SERVER_HOOK_AFTER_MATCH = 'server-hook-after-match',
}

export const X_RENDER_CACHE = 'x-render-cache';
40 changes: 28 additions & 12 deletions packages/server/core/src/base/middlewares/renderHandler/ssrCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ interface CacheMod {
cacheOption?: CacheOption;
}

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

export class CacheManager {
private container: Container<string, string>;

constructor(container: Container<string, string>) {
Expand All @@ -34,7 +40,7 @@ class CacheManager {
cacheControl: CacheControl,
render: ServerRender,
ssrContext: SSRServerContext,
): Promise<string | ReadableStream> {
): Promise<CacheResult> {
const key = this.computedKey(req, cacheControl);

const value = await this.container.get(key);
Expand All @@ -48,20 +54,23 @@ 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 @@ -70,19 +79,20 @@ class CacheManager {
render: ServerRender,
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 {
const body =
// TODO: remove node:stream, move it to ssr entry.
Expand All @@ -107,7 +117,10 @@ class CacheManager {

body.pipeThrough(stream);

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

Expand Down Expand Up @@ -205,12 +218,12 @@ class ServerCache {
}
}

getCache(
async getCache(
req: Request,
cacheControl: CacheControl,
render: ServerRender,
ssrContext: SSRServerContext,
) {
): Promise<CacheResult> {
if (this.cacheManger) {
return this.cacheManger.getCacheResult(
req,
Expand All @@ -219,7 +232,10 @@ class ServerCache {
ssrContext,
);
} else {
return render(ssrContext);
const renderResult = await render(ssrContext);
return {
data: renderResult,
};
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
ServerManifest,
ServerRender,
} from '../../../core/server';
import { REPLACE_REG } from '../../constants';
import { REPLACE_REG, X_RENDER_CACHE } from '../../constants';
import type * as streamPolyfills from '../../adapters/node/polyfills/stream';
import type * as ssrCaheModule from './ssrCache';
import { ServerTiming } from './serverTiming';
Expand Down Expand Up @@ -124,6 +124,7 @@ export async function ssrRender(
const runtimeEnv = getRuntimeEnv();

let ssrResult: Awaited<ReturnType<ServerRender>>;
let cacheStatus: ssrCaheModule.CacheStatus | undefined;
const render: ServerRender = renderBundle[SERVER_RENDER_FUNCTION_NAME];

if (runtimeEnv === 'node') {
Expand All @@ -139,12 +140,15 @@ export async function ssrRender(
);

if (cacheControl) {
ssrResult = await ssrCache.getCache(
const { data, status } = await ssrCache.getCache(
request,
cacheControl,
render,
ssrContext,
);

ssrResult = data;
cacheStatus = status;
} else {
ssrResult = await render(ssrContext);
}
Expand All @@ -154,6 +158,11 @@ export async function ssrRender(

const { redirection } = ssrContext;

// set ssr cacheStatus
if (cacheStatus) {
responseProxy.headers.set(X_RENDER_CACHE, cacheStatus);
}

if (redirection.url) {
const { headers } = responseProxy;
headers.set('Location', redirection.url);
Expand Down
2 changes: 1 addition & 1 deletion packages/server/core/tests/base/adapters/loadEnv.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'path';
import { loadServerEnv } from '../../../src/base';
import { loadServerEnv } from '../../../src/base/adapters/node';

describe('test load serve env file', () => {
const pwd = path.resolve(__dirname, '../fixtures', 'serverEnv');
Expand Down
157 changes: 157 additions & 0 deletions packages/server/core/tests/base/middlewares/ssrCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Container, CacheControl } from '@modern-js/types';
import { CacheManager } from '../../../src/base/middlewares/renderHandler/ssrCache';
import type { SSRServerContext } from '../../../src/core/server';

function sleep(timeout: number) {
return new Promise(resolve => {
setTimeout(() => {
resolve(null);
}, timeout);
});
}

class MyContainer implements Container {
cache: Map<string, string> = new Map();

async get(key: string) {
return this.cache.get(key);
}

async set(key: string, value: string) {
this.cache.set(key, value);

return this;
}

async has(key: string) {
return this.cache.has(key);
}

async delete(key: string) {
return this.cache.delete(key);
}
}

const container = new MyContainer();

const cacheManager = new CacheManager(container);

const ssrContext: SSRServerContext = {} as any;

describe('test cacheManager', () => {
it('should return cache', async () => {
let counter = 0;
const cacheControl: CacheControl = {
maxAge: 120,
staleWhileRevalidate: 100,
};
const req = new Request('http://localhost:8080/a', {
headers: { ua: 'mock_ua' },
});

const render = async () => `Hello_${counter++}`;

const result1 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);

expect(result1.data).toEqual(`Hello_0`);

await sleep(50);
const result2 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);
expect(result2.data).toEqual(`Hello_0`);

await sleep(100);
const result3 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);
expect(result3.data).toEqual(`Hello_0`);
});

it('should revalidate the cache', async () => {
let counter = 0;
const cacheControl: CacheControl = {
maxAge: 100,
staleWhileRevalidate: 100,
};
const req = new Request('http://localhost:8080/b', {
headers: { ua: 'mock_ua' },
});
const render = async () => `Hello_${counter++}`;

const result1 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);
expect(result1.data).toEqual(`Hello_0`);

await sleep(150);
const result2 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);
expect(result2.data).toEqual(`Hello_0`);

await sleep(50);
const result3 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);
expect(result3.data).toEqual(`Hello_1`);
});

it('should invalidate the ssr, then render on next req', async () => {
let counter = 0;
const cacheControl: CacheControl = {
maxAge: 500,
staleWhileRevalidate: 20,
};
const req = new Request('http://localhost:8080/c', {
headers: { ua: 'mock_ua' },
});
const render = async () => `Hello_${counter++}`;

const result1 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);
expect(result1.data).toEqual(`Hello_0`);

await sleep(600);
const result2 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);
expect(result2.data).toEqual(`Hello_1`);

await sleep(600);
const result3 = await cacheManager.getCacheResult(
req,
cacheControl,
render,
ssrContext,
);
expect(result3.data).toEqual(`Hello_2`);
});
});
2 changes: 1 addition & 1 deletion packages/server/core/tests/base/utils/serverConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as path from 'path';
import { DEFAULT_SERVER_CONFIG } from '@modern-js/utils';
import mergeDeep from 'merge-deep';
import { getServerConfigPath } from '../../../src/base/utils';
import { getServerConfigPath } from '../../../src/base/utils/serverConfig';

describe('test loadConfig', () => {
test('should merge CliConfig and ServerConfig correctly', () => {
Expand Down
26 changes: 4 additions & 22 deletions packages/server/core/tests/core/loadPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,16 @@ import { loadPlugins } from '../../src/core/loadPlugins';
const modulePath = path.join(__dirname, './fixtures/load-plugins');
describe('test load plugin', () => {
it('should load string plugin correctly', () => {
const loaded = loadPlugins(modulePath, [], {
internalPlugins: {
'test-a': 'test-a',
},
const loaded = loadPlugins(modulePath, {
'test-a': 'test-a',
});
expect(loaded[0].name).toBe('test-a');
});

it('should load plugin instance correctly', () => {
const loaded = loadPlugins(
modulePath,
[
{
name: 'modern',
},
] as any,
{},
);

expect(loaded[0].name).toBe('modern');
});

it('should throw error if plugin not found', () => {
try {
loadPlugins(modulePath, [], {
internalPlugins: {
'test-b': 'test-b',
},
loadPlugins(modulePath, {
'test-b': 'test-b',
});
} catch (e: any) {
expect(e.message).toMatch('Can not find module test-b.');
Expand Down
2 changes: 1 addition & 1 deletion packages/server/core/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { installGlobals } from '../src/base/adapters/node';
import { installGlobals } from '../src/base/adapters/node/polyfills';

installGlobals();

0 comments on commit 76948c3

Please sign in to comment.