@lucidcms/core
Version:
The core of the Lucid CMS. It's responsible for spinning up the API and serving the CMS.
1 lines • 7.56 kB
Source Map (JSON)
{"version":3,"file":"cache.mjs","names":[],"sources":["../../../../src/libs/http/middleware/cache.ts"],"sourcesContent":["import { createMiddleware } from \"hono/factory\";\nimport { hasher } from \"node-object-hash\";\nimport type { LucidHonoContext } from \"../../../types/hono.js\";\nimport cacheKeys, { type HttpStaticValues } from \"../../kv/cache-keys.js\";\nimport { getHttpCacheNamespaceTokens } from \"../../kv/http-cache.js\";\nimport createServiceContext from \"../utils/create-service-context.js\";\n\nconst hashInstance = hasher({ sort: true, coerce: true });\n\ntype CacheOptions = {\n\t/** The time-to-live (TTL) for the cached response in seconds. */\n\tttl: number;\n\t/**\n\t * The mode for generating the cache key.\n\t *\n\t * - \"path-only\": only the path is used for the cache key, unless you specify includeHeaders\n\t * - \"include-query\": the query parameters are included in the cache key\n\t * - \"static\": a static key is used for the cache key\n\t */\n\tmode: \"path-only\" | \"include-query\" | \"static\";\n\t/** The headers to include in the cache key. */\n\tincludeHeaders?: string[];\n\t/** The tags to add the cache key to. */\n\ttags?: string[] | ((c: LucidHonoContext) => string[]);\n\t/** Bypasses hash generation and uses a simple string key. Useful for endpoints with no variations. */\n\tstaticKey?:\n\t\t| HttpStaticValues\n\t\t| ((c: LucidHonoContext) => HttpStaticValues | null);\n\t/**\n\t * Extra request context to include in the cache key, ie. the resolved tenant.\n\t * Return an empty object to leave the key unchanged. Not supported alongside staticKey.\n\t */\n\tkeyContext?: (c: LucidHonoContext) => Record<string, unknown>;\n\t/** Skip both cache reads and writes for request-specific responses. */\n\tbypass?: (c: LucidHonoContext) => boolean;\n};\n\n/**\n * Generate a cache key based on the request context and options.\n */\nconst generateCacheKey = (\n\tc: LucidHonoContext,\n\toptions: CacheOptions,\n\tnamespaceTokens?: Record<string, string>,\n) => {\n\tif (options.staticKey) {\n\t\tconst staticKey =\n\t\t\ttypeof options.staticKey === \"function\"\n\t\t\t\t? options.staticKey(c)\n\t\t\t\t: options.staticKey;\n\n\t\tif (!namespaceTokens || Object.keys(namespaceTokens).length === 0) {\n\t\t\treturn staticKey;\n\t\t}\n\n\t\treturn cacheKeys.http.response(\n\t\t\thashInstance.hash({\n\t\t\t\tstaticKey,\n\t\t\t\tnamespaceTokens,\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst { mode = \"include-query\", includeHeaders = [] } = options;\n\n\tconst cacheObject: Record<string, unknown> = {\n\t\tpath: c.req.path,\n\t};\n\n\tif (mode === \"include-query\") {\n\t\tconst url = new URL(c.req.url);\n\t\tcacheObject.query = Object.fromEntries(url.searchParams.entries());\n\t}\n\n\tif (includeHeaders.length > 0) {\n\t\tcacheObject.headers = {};\n\t\tfor (const headerName of includeHeaders) {\n\t\t\tconst headerValue = c.req.header(headerName);\n\t\t\tif (headerValue) {\n\t\t\t\t// @ts-expect-error\n\t\t\t\tcacheObject.headers[headerName] = headerValue;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (options.keyContext) {\n\t\tconst keyContext = options.keyContext(c);\n\t\tif (Object.keys(keyContext).length > 0) {\n\t\t\tcacheObject.keyContext = keyContext;\n\t\t}\n\t}\n\n\tif (namespaceTokens && Object.keys(namespaceTokens).length > 0) {\n\t\tcacheObject.namespaceTokens = namespaceTokens;\n\t}\n\n\treturn cacheKeys.http.response(hashInstance.hash(cacheObject));\n};\n\n/**\n * Check if the request should bypass the cache.\n */\nconst shouldBypassCache = (c: LucidHonoContext): boolean => {\n\tconst cacheControl = c.req.header(\"Cache-Control\");\n\tconst pragma = c.req.header(\"Pragma\");\n\n\tif (\n\t\tcacheControl?.includes(\"no-cache\") ||\n\t\tcacheControl?.includes(\"no-store\")\n\t) {\n\t\treturn true;\n\t}\n\n\tif (pragma === \"no-cache\") {\n\t\treturn true;\n\t}\n\n\treturn false;\n};\n\n/**\n * Middleware to cache responses based on the request context and options.\n */\nconst cache = (options: CacheOptions) =>\n\tcreateMiddleware(async (c: LucidHonoContext, next) => {\n\t\tif (options.bypass?.(c)) {\n\t\t\tawait next();\n\t\t\tc.header(\"Cache-Control\", \"private, no-store\");\n\t\t\tc.header(\"Pragma\", \"no-cache\");\n\t\t\treturn;\n\t\t}\n\t\tif (shouldBypassCache(c)) {\n\t\t\treturn await next();\n\t\t}\n\n\t\tconst context = createServiceContext(c);\n\t\tconst kv = context.kv;\n\n\t\tconst tags =\n\t\t\ttypeof options.tags === \"function\" ? options.tags(c) : options.tags;\n\t\tconst namespaceTokens =\n\t\t\ttags && tags.length > 0\n\t\t\t\t? await getHttpCacheNamespaceTokens(context, tags)\n\t\t\t\t: undefined;\n\t\tconst cacheKey = generateCacheKey(c, options, namespaceTokens);\n\n\t\tif (cacheKey === null) return await next();\n\n\t\tconst cached = await kv.get<{ data: unknown; cachedAt: number }>(context, {\n\t\t\tkey: cacheKey,\n\t\t\thash: true,\n\t\t});\n\t\tif (cached !== null) {\n\t\t\tconst age = Math.floor((Date.now() - cached.cachedAt) / 1000);\n\t\t\tc.header(\"X-Cache\", \"HIT\");\n\t\t\tc.header(\"Age\", age.toString());\n\t\t\treturn c.json(cached.data);\n\t\t}\n\n\t\tawait next();\n\n\t\tconst response = c.res.clone();\n\t\tif (\n\t\t\tresponse.ok &&\n\t\t\tresponse.headers.get(\"content-type\")?.includes(\"application/json\")\n\t\t) {\n\t\t\tconst data = await response.json();\n\t\t\tawait kv.set(context, {\n\t\t\t\tkey: cacheKey,\n\t\t\t\tvalue: {\n\t\t\t\t\tdata: data,\n\t\t\t\t\tcachedAt: Date.now(),\n\t\t\t\t},\n\t\t\t\texpirationTtl: options.ttl,\n\t\t\t\thash: true,\n\t\t\t});\n\t\t}\n\n\t\tc.header(\"X-Cache\", \"MISS\");\n\t});\n\nexport default cache;\n"],"mappings":"0PAOA,MAAM,EAAe,EAAO,CAAE,KAAM,GAAM,OAAQ,EAAK,CAAC,EAiClD,GACL,EACA,EACA,IACI,CACJ,GAAI,EAAQ,UAAW,CACtB,IAAM,EACL,OAAO,EAAQ,WAAc,WAC1B,EAAQ,UAAU,CAAC,EACnB,EAAQ,UAMZ,MAJI,CAAC,GAAmB,OAAO,KAAK,CAAe,CAAC,CAAC,SAAW,EACxD,EAGD,EAAU,KAAK,SACrB,EAAa,KAAK,CACjB,YACA,iBACD,CAAC,CACF,CACD,CAEA,GAAM,CAAE,OAAO,gBAAiB,iBAAiB,CAAC,GAAM,EAElD,EAAuC,CAC5C,KAAM,EAAE,IAAI,IACb,EAEA,GAAI,IAAS,gBAAiB,CAC7B,IAAM,EAAM,IAAI,IAAI,EAAE,IAAI,GAAG,EAC7B,EAAY,MAAQ,OAAO,YAAY,EAAI,aAAa,QAAQ,CAAC,CAClE,CAEA,GAAI,EAAe,OAAS,EAAG,CAC9B,EAAY,QAAU,CAAC,EACvB,IAAK,IAAM,KAAc,EAAgB,CACxC,IAAM,EAAc,EAAE,IAAI,OAAO,CAAU,EACvC,IAEH,EAAY,QAAQ,GAAc,EAEpC,CACD,CAEA,GAAI,EAAQ,WAAY,CACvB,IAAM,EAAa,EAAQ,WAAW,CAAC,EACnC,OAAO,KAAK,CAAU,CAAC,CAAC,OAAS,IACpC,EAAY,WAAa,EAE3B,CAMA,OAJI,GAAmB,OAAO,KAAK,CAAe,CAAC,CAAC,OAAS,IAC5D,EAAY,gBAAkB,GAGxB,EAAU,KAAK,SAAS,EAAa,KAAK,CAAW,CAAC,CAC9D,EAKM,EAAqB,GAAiC,CAC3D,IAAM,EAAe,EAAE,IAAI,OAAO,eAAe,EAC3C,EAAS,EAAE,IAAI,OAAO,QAAQ,EAapC,MAJA,GANC,GAAc,SAAS,UAAU,GACjC,GAAc,SAAS,UAAU,GAK9B,IAAW,WAKhB,EAKM,EAAS,GACd,EAAiB,MAAO,EAAqB,IAAS,CACrD,GAAI,EAAQ,SAAS,CAAC,EAAG,CACxB,MAAM,EAAK,EACX,EAAE,OAAO,gBAAiB,mBAAmB,EAC7C,EAAE,OAAO,SAAU,UAAU,EAC7B,MACD,CACA,GAAI,EAAkB,CAAC,EACtB,OAAO,MAAM,EAAK,EAGnB,IAAM,EAAU,EAAqB,CAAC,EAChC,EAAK,EAAQ,GAEb,EACL,OAAO,EAAQ,MAAS,WAAa,EAAQ,KAAK,CAAC,EAAI,EAAQ,KAC1D,EACL,GAAQ,EAAK,OAAS,EACnB,MAAM,EAA4B,EAAS,CAAI,EAC/C,IAAA,GACE,EAAW,EAAiB,EAAG,EAAS,CAAe,EAE7D,GAAI,IAAa,KAAM,OAAO,MAAM,EAAK,EAEzC,IAAM,EAAS,MAAM,EAAG,IAAyC,EAAS,CACzE,IAAK,EACL,KAAM,EACP,CAAC,EACD,GAAI,IAAW,KAAM,CACpB,IAAM,EAAM,KAAK,OAAO,KAAK,IAAI,EAAI,EAAO,UAAY,GAAI,EAG5D,OAFA,EAAE,OAAO,UAAW,KAAK,EACzB,EAAE,OAAO,MAAO,EAAI,SAAS,CAAC,EACvB,EAAE,KAAK,EAAO,IAAI,CAC1B,CAEA,MAAM,EAAK,EAEX,IAAM,EAAW,EAAE,IAAI,MAAM,EAC7B,GACC,EAAS,IACT,EAAS,QAAQ,IAAI,cAAc,CAAC,EAAE,SAAS,kBAAkB,EAChE,CACD,IAAM,EAAO,MAAM,EAAS,KAAK,EACjC,MAAM,EAAG,IAAI,EAAS,CACrB,IAAK,EACL,MAAO,CACA,OACN,SAAU,KAAK,IAAI,CACpB,EACA,cAAe,EAAQ,IACvB,KAAM,EACP,CAAC,CACF,CAEA,EAAE,OAAO,UAAW,MAAM,CAC3B,CAAC"}