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

[FIX] serveResources: Improve cache invalidation #688

Merged
merged 3 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions lib/middleware/serveResources.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,18 @@ function createMiddleware({resources, middlewareUtil}) {

// Enable ETag caching
const statInfo = resource.getStatInfo();
if (statInfo?.size !== undefined) {
res.setHeader("ETag", etag(statInfo));
if (statInfo?.size !== undefined && !resource.isModified()) {
let etagHeader = etag(statInfo);
if (resource.getProject()) {
// Add project version to ETag to invalidate cache when project version changes.
// This is necessary to invalidate files with ${version} placeholders.
etagHeader = etagHeader.slice(0, -1) + `-${resource.getProject().getVersion()}"`;
}
res.setHeader("ETag", etagHeader);
} else {
// Fallback to buffer if stats are not available or insufficient
// Fallback to buffer if stats are not available or insufficient or resource is modified.
// Modified resources must use the buffer for cache invalidation so that UI5 Tooling changes
// invalidate the cache even when the original resource is not modified.
res.setHeader("ETag", etag(await resource.getBuffer()));
}

Expand Down
99 changes: 99 additions & 0 deletions test/lib/server/caching.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import test from "ava";
import sinonGlobal from "sinon";
import esmock from "esmock";
import supertest from "supertest";
import {graphFromPackageDependencies} from "@ui5/project/graph";

let request;
let server;

// Start server before running tests
test.before(async (t) => {
const sinon = t.context.sinon = sinonGlobal.createSandbox();

t.context.manifestEnhancer = sinon.stub();

const {serve} = await esmock.p("../../../lib/server.js", {}, {
"@ui5/builder/processors/manifestEnhancer": t.context.manifestEnhancer,
});

const graph = await graphFromPackageDependencies({
cwd: "./test/fixtures/application.a"
});

t.context.applicationProject = graph.getProject("application.a");

server = await serve(graph, {
port: 3334
});
request = supertest("http://localhost:3334");
});

test.after.always(() => {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
});

test("serveResources: manifestEnhancer cache invalidation", async (t) => {
const {manifestEnhancer} = t.context;

manifestEnhancer.callsFake(async ({resources}) => {
for (const resource of resources) {
resource.setString(JSON.stringify({"mockedResponse": "v1"}));
}
});

const response = await request.get("/manifest.json");
if (response.error) {
throw new Error(response.error);
}
t.is(response.statusCode, 200, "Correct HTTP status code");
t.is(response.text, JSON.stringify({
"mockedResponse": "v1"
}), "Correct response");

const cachedResponse = await request.get("/manifest.json").set({"If-None-Match": response.headers.etag});
t.is(cachedResponse.statusCode, 304, "Correct HTTP status code");

// Changes to the response content should invalidate the cache
manifestEnhancer.callsFake(async ({resources}) => {
for (const resource of resources) {
resource.setString(JSON.stringify({"mockedResponse": "v2"}));
}
});

const newResponse = await request.get("/manifest.json").set({"If-None-Match": response.headers.etag});
t.is(newResponse.statusCode, 200, "Correct HTTP status code");
t.is(newResponse.text, JSON.stringify({
"mockedResponse": "v2"
}), "Correct response");
});

test("serveResources: version placeholder cache invalidation", async (t) => {
const {applicationProject} = t.context;

const response = await request.get("/versionTest.js");
if (response.error) {
throw new Error(response.error);
}
t.is(response.statusCode, 200, "Correct HTTP status code");
t.is(response.text, "console.log(`1.0.0`);\n", "Correct response");

const cachedResponse = await request.get("/versionTest.js").set({"If-None-Match": response.headers.etag});
t.is(cachedResponse.statusCode, 304, "Correct HTTP status code");

// Changes to the project version should invalidate the cache
applicationProject._version = "1.0.1-SNAPSHOT";

const newResponse = await request.get("/versionTest.js").set({"If-None-Match": response.headers.etag});
t.is(newResponse.statusCode, 200, "Correct HTTP status code");
t.is(newResponse.text, "console.log(`1.0.1-SNAPSHOT`);\n", "Correct response");
t.regex(newResponse.headers.etag, /1\.0\.1-SNAPSHOT/, "Correct updated ETag");
});
9 changes: 6 additions & 3 deletions test/lib/server/middleware/serveResources.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ test.serial("Check verbose logging", async (t) => {
return {
getVersion: () => "1.0.0"
};
}
},
isModified: () => false
};

const resources = {
Expand Down Expand Up @@ -404,7 +405,8 @@ test.serial("Check if version replacement is done", (t) => {
getVersion: () => "1.0.0"
};
},
getPathTree: () => ""
getPathTree: () => "",
isModified: () => false
};

const resources = {
Expand Down Expand Up @@ -483,7 +485,8 @@ test.serial("Check if utf8 characters are correctly processed in version replace
getVersion: () => "1.0.0"
};
},
getPathTree: () => ""
getPathTree: () => "",
isModified: () => false
};

const resources = {
Expand Down