UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

104 lines (103 loc) 3.03 kB
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 };