@fedify/fedify
Version:
An ActivityPub server framework
104 lines (103 loc) • 3.03 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import { URLPattern } from "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { preloadedContexts } from "@fedify/vocab-runtime";
import { getLogger } from "@logtape/logtape";
//#region src/utils/kv-cache.ts
const logger = getLogger([
"fedify",
"utils",
"kv-cache"
]);
/**
* A mock implementation of a key–value store for testing purposes.
*/
var MockKvStore = class {
#values = {};
get(key) {
return Promise.resolve(this.#values[JSON.stringify(key)]);
}
set(key, value, _options) {
this.#values[JSON.stringify(key)] = value;
return Promise.resolve();
}
async delete(_) {}
cas(..._) {
return Promise.resolve(false);
}
async *list(prefix) {
for (const [encodedKey, value] of Object.entries(this.#values)) {
const key = JSON.parse(encodedKey);
if (prefix != null) {
if (key.length < prefix.length) continue;
if (!prefix.every((p, i) => key[i] === p)) continue;
}
yield {
key,
value
};
}
}
};
/**
* Decorates a {@link DocumentLoader} with a cache backed by a {@link KvStore}.
* @param parameters The parameters for the cache.
* @returns The decorated document loader which is cache-enabled.
*/
function kvCache({ loader, kv, prefix, rules }) {
const keyPrefix = prefix ?? ["_fedify", "remoteDocument"];
rules ??= [[new URLPattern({}), Temporal.Duration.from({ minutes: 5 })]];
for (const [p, duration] of rules) if (Temporal.Duration.compare(duration, { days: 30 }) > 0) throw new TypeError("The maximum cache duration is 30 days: " + (p instanceof URLPattern ? `${p.protocol}://${p.username}:${p.password}@${p.hostname}:${p.port}/${p.pathname}?${p.search}#${p.hash}` : p.toString()));
return async (url, options) => {
if (url in preloadedContexts) {
logger.debug("Using preloaded context: {url}.", { url });
return {
contextUrl: null,
document: preloadedContexts[url],
documentUrl: url
};
}
const match = matchRule(url, rules);
if (match == null) return await loader(url, options);
const key = [...keyPrefix, url];
let cache = void 0;
try {
cache = await kv.get(key);
} catch (error) {
if (error instanceof Error) logger.warn("Failed to get the document of {url} from the KV cache: {error}", {
url,
error
});
}
if (cache == null) {
const remoteDoc = await loader(url, options);
try {
await kv.set(key, remoteDoc, { ttl: match });
} catch (error) {
logger.warn("Failed to save the document of {url} to the KV cache: {error}", {
url,
error
});
}
return remoteDoc;
}
return cache;
};
}
function matchRule(url, rules) {
for (const [pattern, d] of rules) {
const duration = Temporal.Duration.from(d);
if (typeof pattern === "string") {
if (url === pattern) return duration;
continue;
}
if (pattern instanceof URL) {
if (pattern.href == url) return duration;
continue;
}
if (pattern.test(url)) return duration;
}
return null;
}
//#endregion
export { kvCache as n, MockKvStore as t };