mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-06 08:18:18 +00:00
Add CouchDB cache compatibility controls and native revalidation
This commit is contained in:
1 parent
7166de3291
commit
667d6bed1f
9 files changed
+283
-11
No files matched your search
@@ -82,6 +82,7 @@ export function getCouchDBReplicatorConfigurationIdentity(settings: RemoteDBSett
|
||||
authentication,
|
||||
projectHeaders(settings.couchDB_CustomHeaders),
|
||||
settings.useRequestAPI,
|
||||
settings.useCouchDBCacheControlHeadersOnly === true,
|
||||
settings.disableRequestURI,
|
||||
projectRemoteSecurity(settings),
|
||||
usesEncryptedInternalMetadata(settings),
|
||||
|
||||
@@ -7,6 +7,17 @@ import {
|
||||
} from "./replicatorConfigurationIdentity";
|
||||
|
||||
describe("active Replicator configuration identity", () => {
|
||||
it("treats an absent headers-only CouchDB cache switch as off and leaves Object Storage unaffected", () => {
|
||||
const absent = configuredSettings();
|
||||
Reflect.deleteProperty(absent, "useCouchDBCacheControlHeadersOnly");
|
||||
expect(getCouchDBReplicatorConfigurationIdentity({ ...absent, useCouchDBCacheControlHeadersOnly: false })).toBe(
|
||||
getCouchDBReplicatorConfigurationIdentity(absent)
|
||||
);
|
||||
expect(getObjectStorageReplicatorConfigurationIdentity({ ...absent, useCouchDBCacheControlHeadersOnly: true })).toBe(
|
||||
getObjectStorageReplicatorConfigurationIdentity(absent)
|
||||
);
|
||||
});
|
||||
|
||||
it("replaces a connection when the path identity mode changes", () => {
|
||||
const previous = configuredSettings({ pathNormalizationVersion: 0 });
|
||||
const next = { ...previous, pathNormalizationVersion: 1 as const };
|
||||
@@ -73,6 +84,7 @@ describe("active Replicator configuration identity", () => {
|
||||
["couchDB_PASSWORD", "secret-b"],
|
||||
["couchDB_CustomHeaders", "X-First: changed"],
|
||||
["useRequestAPI", true],
|
||||
["useCouchDBCacheControlHeadersOnly", true],
|
||||
["disableRequestURI", true],
|
||||
["encrypt", false],
|
||||
["passphrase", "encryption-b"],
|
||||
|
||||
@@ -28,6 +28,7 @@ export function panePowerUsers(
|
||||
onUpdate: this.onlyOnCouchDB,
|
||||
});
|
||||
new Setting(paneEl).autoWireToggle("useTimeouts", { onUpdate: this.onlyOnCouchDB });
|
||||
new Setting(paneEl).autoWireToggle("useCouchDBCacheControlHeadersOnly", { onUpdate: this.onlyOnCouchDB });
|
||||
});
|
||||
void addPanel(paneEl, $msg("Configuration Encryption")).then((paneEl) => {
|
||||
const passphrase_options: Record<ConfigPassphraseStore, string> = {
|
||||
|
||||
@@ -220,8 +220,9 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
|
||||
delete transformedHeaders["content-length"];
|
||||
delete transformedHeaders["Content-Length"];
|
||||
const cacheMode = opts?.cache ?? (req instanceof Request ? req.cache : undefined);
|
||||
if (cacheMode === "no-store" && ["GET", "HEAD"].includes(method.toUpperCase())) {
|
||||
if (["no-cache", "no-store"].includes(cacheMode ?? "") && ["GET", "HEAD"].includes(method.toUpperCase())) {
|
||||
// requestUrl has no cache option; no-store alone can reuse an existing cached response.
|
||||
const enforceNoStore = cacheMode === "no-store";
|
||||
const cacheControlKeys = Object.keys(transformedHeaders).filter(
|
||||
(key) => key.toLowerCase() === "cache-control"
|
||||
);
|
||||
@@ -229,11 +230,19 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
|
||||
.flatMap((key) => splitHeaderDirectives(transformedHeaders[key]))
|
||||
.filter((directive) => {
|
||||
const name = getDirectiveName(directive);
|
||||
return !cacheReuseDirectives.has(name) && name !== "no-cache" && name !== "no-store";
|
||||
return (
|
||||
!cacheReuseDirectives.has(name) &&
|
||||
name !== "no-cache" &&
|
||||
(!enforceNoStore || name !== "no-store")
|
||||
);
|
||||
});
|
||||
const cacheControlKey = cacheControlKeys[0] ?? "cache-control";
|
||||
for (const key of cacheControlKeys) delete transformedHeaders[key];
|
||||
transformedHeaders[cacheControlKey] = [...cacheControlDirectives, "no-cache", "no-store"].join(", ");
|
||||
transformedHeaders[cacheControlKey] = [
|
||||
...cacheControlDirectives,
|
||||
"no-cache",
|
||||
...(enforceNoStore ? ["no-store"] : []),
|
||||
].join(", ");
|
||||
|
||||
const pragmaKeys = Object.keys(transformedHeaders).filter((key) => key.toLowerCase() === "pragma");
|
||||
const pragmaDirectives = pragmaKeys
|
||||
|
||||
@@ -101,6 +101,35 @@ describe("ObsidianAPIService.nativeFetch cache control", () => {
|
||||
expect(mocks.requestUrl.mock.calls[0][0]).toMatchObject({ url, method });
|
||||
});
|
||||
|
||||
it.each(["GET", "HEAD", "get", "head"])("revalidates an explicit no-cache %s request", async (method) => {
|
||||
const headers = {
|
||||
"Cache-Control": 'public, max-age=3600, no-transform, custom="a,b"',
|
||||
"cAcHe-CoNtRoL": 'max-stale=30, min-fresh=5, only-if-cached, custom-two="max-age=1, only-if-cached"',
|
||||
Pragma: "cache",
|
||||
};
|
||||
const originalHeaders = { ...headers };
|
||||
|
||||
await createService({}).nativeFetch(url, { method, cache: "no-cache", headers });
|
||||
|
||||
expect(sentHeaders().get("cache-control")).toBe(
|
||||
'public, no-transform, custom="a,b", custom-two="max-age=1, only-if-cached", no-cache'
|
||||
);
|
||||
expect(sentHeaders().get("pragma")).toBe("cache, no-cache");
|
||||
expect(headers).toEqual(originalHeaders);
|
||||
expect(mocks.requestUrl.mock.calls[0][0]).toMatchObject({ url, method });
|
||||
});
|
||||
|
||||
it("preserves an explicit no-store directive when enforcing no-cache", async () => {
|
||||
const headers = { "Cache-Control": "no-store, max-age=3600", Pragma: "legacy" };
|
||||
const originalHeaders = { ...headers };
|
||||
|
||||
await createService({}).nativeFetch(url, { cache: "no-cache", headers });
|
||||
|
||||
expect(sentHeaders().get("cache-control")).toBe("no-store, no-cache");
|
||||
expect(sentHeaders().get("pragma")).toBe("legacy, no-cache");
|
||||
expect(headers).toEqual(originalHeaders);
|
||||
});
|
||||
|
||||
it("preserves non-conflicting cache directives when enforcing no-store", async () => {
|
||||
const headers = {
|
||||
"Cache-Control": 'max-age=3600, no-transform, custom="a,b"',
|
||||
@@ -140,6 +169,30 @@ describe("ObsidianAPIService.nativeFetch cache control", () => {
|
||||
expect(request.headers.has("cache-control")).toBe(false);
|
||||
});
|
||||
|
||||
it("uses a Request no-cache mode when the options do not override it", async () => {
|
||||
const request = new Request(url, {
|
||||
method: "HEAD",
|
||||
cache: "no-cache",
|
||||
headers: { "Cache-Control": "max-age=60", "X-Fixture": "request" },
|
||||
});
|
||||
const headers = new Headers({ "X-Other": "options" });
|
||||
|
||||
await createService({}).nativeFetch(request, { headers });
|
||||
|
||||
expect(sentHeaders().get("cache-control")).toBe("no-cache");
|
||||
expect(sentHeaders().get("x-fixture")).toBe("request");
|
||||
expect(sentHeaders().get("x-other")).toBe("options");
|
||||
expect(request.headers.get("cache-control")).toBe("max-age=60");
|
||||
expect(headers.has("cache-control")).toBe(false);
|
||||
});
|
||||
|
||||
it("lets an explicit default cache mode override a no-cache Request", async () => {
|
||||
await createService({}).nativeFetch(new Request(url, { cache: "no-cache" }), { cache: "default" });
|
||||
|
||||
expect(sentHeaders().has("cache-control")).toBe(false);
|
||||
expect(sentHeaders().has("pragma")).toBe(false);
|
||||
});
|
||||
|
||||
it("lets the options request no-store for an otherwise cacheable Request", async () => {
|
||||
await createService({}).nativeFetch(new Request(url, { cache: "force-cache" }), { cache: "no-store" });
|
||||
|
||||
@@ -153,7 +206,7 @@ describe("ObsidianAPIService.nativeFetch cache control", () => {
|
||||
expect(sentHeaders().has("pragma")).toBe(false);
|
||||
});
|
||||
|
||||
it.each([undefined, "default", "no-cache", "reload", "force-cache"] as const)(
|
||||
it.each([undefined, "default", "reload", "force-cache"] as const)(
|
||||
"preserves caller headers for cache mode %s",
|
||||
async (cache) => {
|
||||
const headers = { "Cache-Control": "max-age=60", Pragma: "fixture" };
|
||||
@@ -171,6 +224,14 @@ describe("ObsidianAPIService.nativeFetch cache control", () => {
|
||||
expect(sentHeaders().has("pragma")).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["POST", "PUT", "DELETE"])("preserves a no-cache %s request", async (method) => {
|
||||
const headers = { "Cache-Control": "max-age=60, no-transform", Pragma: "fixture" };
|
||||
|
||||
await createService({}).nativeFetch(url, { method, cache: "no-cache", headers });
|
||||
|
||||
expect(mocks.requestUrl.mock.calls[0][0]).toMatchObject({ url, method, headers });
|
||||
});
|
||||
|
||||
it.each(["POST", "PUT", "DELETE"])("preserves a no-store %s request", async (method) => {
|
||||
const headers = { "Cache-Control": "no-transform" };
|
||||
|
||||
|
||||
Reference in new issue
Block a user