UNPKG

@alepha/server-cache

Version:

Adds ETag and Cache-Control headers to server responses.

192 lines (188 loc) 6.48 kB
//#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion const __alepha_cache = __toESM(require("@alepha/cache")); const __alepha_core = __toESM(require("@alepha/core")); const node_crypto = __toESM(require("node:crypto")); const __alepha_datetime = __toESM(require("@alepha/datetime")); const __alepha_logger = __toESM(require("@alepha/logger")); const __alepha_server = __toESM(require("@alepha/server")); //#region src/providers/ServerCacheProvider.ts __alepha_server.ActionDescriptor.prototype.invalidate = async function() { await this.alepha.inject(ServerCacheProvider).invalidate(this.route); }; var ServerCacheProvider = class { log = (0, __alepha_logger.$logger)(); alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha); time = (0, __alepha_core.$inject)(__alepha_datetime.DateTimeProvider); cache = (0, __alepha_cache.$cache)({ provider: "memory" }); generateETag(content) { return `"${(0, node_crypto.createHash)("md5").update(content).digest("hex")}"`; } async invalidate(route) { const cache = route.cache; if (!cache) return; await this.cache.invalidate(this.createCacheKey(route)); } onActionRequest = (0, __alepha_core.$hook)({ on: "action:onRequest", handler: async ({ action, request }) => { const cache = action.route.cache; if (!cache) return; const key = this.createCacheKey(action.route, request); const cached = await this.cache.get(key); if (cached) { const body = cached.contentType === "application/json" ? JSON.parse(cached.body) : cached.body; this.log.trace("Cache hit for action", { key, action: action.name }); request.reply.body = body; } else this.log.trace("Cache miss for action", { key, action: action.name }); } }); onActionResponse = (0, __alepha_core.$hook)({ on: "action:onResponse", handler: async ({ action, request, response }) => { const cache = action.route.cache; if (!cache || !response) return; const key = this.createCacheKey(action.route, request); const contentType = typeof response === "string" ? "text/plain" : "application/json"; const body = contentType === "text/plain" ? response : JSON.stringify(response); const etag = this.generateETag(body); this.log.trace("Caching action", { key, action: action.name, length: body.length }); await this.cache.set(key, { body, lastModified: this.time.toISOString(), contentType, hash: etag }); } }); onRequest = (0, __alepha_core.$hook)({ on: "server:onRequest", handler: async ({ route, request }) => { const cache = route.cache; if (!cache) return; const key = this.createCacheKey(route, request); const cached = await this.cache.get(key); if (cached) { this.log.trace("Cache hit for route", { key, route: route.path }); if (request.headers["if-none-match"] === cached.hash || request.headers["if-modified-since"] === cached.lastModified) { request.reply.status = 304; return; } request.reply.body = cached.body; request.reply.status = cached.status ?? 200; if (cached.contentType) request.reply.setHeader("Content-Type", cached.contentType); } else this.log.trace("Cache miss for route", { key, route: route.path }); } }); onResponse = (0, __alepha_core.$hook)({ on: "server:onResponse", priority: "first", handler: async ({ route, request, response }) => { const cache = route.cache; if (!cache) return; const key = this.createCacheKey(route, request); if (typeof response.body === "string") { this.log.trace("Caching response", { key, route: route.path, status: response.status }); const etag = this.generateETag(response.body); await this.cache.set(key, { body: response.body, status: response.status, contentType: response.headers?.["content-type"], lastModified: this.time.toISOString(), hash: etag }); response.headers ??= {}; response.headers.etag = etag; } } }); getCacheOptions(cache) { if (typeof cache === "boolean") return { ttl: this.time.duration(5, "minutes") }; if (this.time.isDurationLike(cache)) return { ttl: cache }; return { ...cache }; } createCacheKey(route, config) { const params = []; for (const [key, value] of Object.entries(config?.params ?? {})) params.push(`${key}=${value}`); for (const [key, value] of Object.entries(config?.query ?? {})) params.push(`${key}=${value}`); return `${route.method}:${route.path.replaceAll(":", "")}:${params.join(",").replaceAll(":", "")}`; } }; //#endregion //#region src/index.ts /** * Plugin for Alepha Server that provides server-side caching capabilities. * It uses the Alepha Cache module to cache responses from server actions ($action). * It also provides a ETag-based cache invalidation mechanism. * * @example * ```ts * import { Alepha } from "alepha"; * import { $action } from "alepha/server"; * import { AlephaServerCache } from "alepha/server/cache"; * * class ApiServer { * hello = $action({ * cache: true, * handler: () => "Hello, World!", * }); * } * * const alepha = Alepha.create() * .with(AlephaServerCache) * .with(ApiServer); * * run(alepha); * ``` * * @see {@link ServerCacheProvider} * @module alepha.server.cache */ const AlephaServerCache = (0, __alepha_core.$module)({ name: "alepha.server.cache", services: [__alepha_cache.AlephaCache, ServerCacheProvider] }); //#endregion exports.AlephaServerCache = AlephaServerCache; exports.ServerCacheProvider = ServerCacheProvider; //# sourceMappingURL=index.cjs.map