diff --git a/.changeset/calm-files-cache.md b/.changeset/calm-files-cache.md new file mode 100644 index 0000000000..82cb7c6f52 --- /dev/null +++ b/.changeset/calm-files-cache.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Cache web assets between page loads while revalidating the application shell. diff --git a/packages/kap-server/src/routes/webAssets.ts b/packages/kap-server/src/routes/webAssets.ts index 7b7ad01760..f1b8195503 100644 --- a/packages/kap-server/src/routes/webAssets.ts +++ b/packages/kap-server/src/routes/webAssets.ts @@ -56,10 +56,18 @@ async function serveWebAsset( return reply .type(mimeType(filePath)) + .header('Cache-Control', cacheControl(assetsDir, filePath)) .header('Content-Length', String(fileInfo.size)) .send(createReadStream(filePath)); } +function cacheControl(assetsDir: string, filePath: string): string { + const assetsRoot = resolve(assetsDir, 'assets'); + return filePath.startsWith(`${assetsRoot}${sep}`) + ? 'public, max-age=31536000, immutable' + : 'no-cache'; +} + async function resolveStaticFile( assetsDir: string, pathname: string, diff --git a/packages/kap-server/test/webAssets.test.ts b/packages/kap-server/test/webAssets.test.ts new file mode 100644 index 0000000000..f840dc7c4e --- /dev/null +++ b/packages/kap-server/test/webAssets.test.ts @@ -0,0 +1,52 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import Fastify, { type FastifyInstance } from 'fastify'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { registerWebAssetRoutes } from '../src/routes/webAssets'; + +describe('web asset cache policy', () => { + let app: FastifyInstance; + let assetsDir: string; + + beforeEach(async () => { + assetsDir = await mkdtemp(join(tmpdir(), 'kimi-web-assets-')); + await mkdir(join(assetsDir, 'assets')); + await writeFile(join(assetsDir, 'index.html'), '
Kimi
'); + await writeFile(join(assetsDir, 'assets', 'index-Dy7xs5tu.js'), 'export {};'); + + app = Fastify(); + await registerWebAssetRoutes(app, assetsDir); + await app.ready(); + }); + + afterEach(async () => { + await app.close(); + await rm(assetsDir, { recursive: true, force: true }); + }); + + it('caches fingerprinted Vite assets as immutable', async () => { + const response = await app.inject({ + method: 'GET', + url: '/assets/index-Dy7xs5tu.js', + }); + + expect(response.statusCode).toBe(200); + expect(response.headers['cache-control']).toBe( + 'public, max-age=31536000, immutable', + ); + }); + + it.each(['/', '/index.html', '/sessions/current', '/assets/preview'])( + 'revalidates HTML responses for %s', + async (url) => { + const response = await app.inject({ method: 'GET', url }); + + expect(response.statusCode).toBe(200); + expect(response.headers['cache-control']).toBe('no-cache'); + expect(response.headers['content-type']).toBe('text/html; charset=utf-8'); + }, + ); +});