Skip to content

Commit c1e51c4

Browse files
feat: use kv for caching (#326)
* feat: use kv for caching * fix: add kv to env.dev and env.prod namespace * feat: changes to deploy with kv --------- Co-authored-by: Daniel Lipniacki <[email protected]>
1 parent a3789b6 commit c1e51c4

File tree

5 files changed

+102
-9
lines changed

5 files changed

+102
-9
lines changed

Jenkinsfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ pipeline {
1313
timestamps()
1414
}
1515

16+
environment {
17+
KV_DEV = credentials('cf-kv-dev')
18+
KV_PROD = credentials('cf-kv-prod')
19+
}
20+
1621
stages {
1722
stage("Deploy main") {
1823
when {
@@ -34,6 +39,8 @@ pipeline {
3439
usernamePassword(credentialsId: 'cf-workers-creds', usernameVariable: 'CLOUDFLARE_ACCOUNT_ID', passwordVariable: 'CLOUDFLARE_API_TOKEN'),
3540
]) {
3641
sh """
42+
sed -i "s/<kv_dev_namespace_id>/$KV_DEV/g" apps/blog-bff/wrangler.toml
43+
sed -i "s/<kv_prod_namespace_id>/$KV_PROD/g" apps/blog-bff/wrangler.toml
3744
npx wrangler deploy --config apps/blog-bff/wrangler.toml --env dev
3845
npx wrangler deploy --config apps/blog-bff/wrangler.toml --env prod
3946
"""

apps/blog-bff/wrangler.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ logpush = true
99
name = "blog-bff"
1010
vars = { IS_PROD = "true" }
1111

12+
[[env.prod.kv_namespaces]]
13+
binding = "CACHE_KV"
14+
id = "<kv_prod_namespace_id>"
15+
1216
[env.dev]
1317
name = "blog-bff-dev"
1418
vars = { IS_PROD = "false" }
19+
20+
[[env.dev.kv_namespaces]]
21+
binding = "CACHE_KV"
22+
id = "<kv_dev_namespace_id>"
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import { env } from 'hono/adapter';
2-
import { cache } from 'hono/cache';
32
import { createMiddleware } from 'hono/factory';
43

4+
import { kvCache } from './kv-cache';
55
import { getWpLang } from './lang';
66

7-
export const appCache = createMiddleware((c, next) => {
8-
if (env<{ DISABLE_CACHE: string }>(c)['DISABLE_CACHE']) {
7+
type Env = {
8+
Bindings: {
9+
DISABLE_CACHE?: string;
10+
CACHE_KV: KVNamespace;
11+
};
12+
};
13+
14+
export const appCache = createMiddleware<Env>((c, next) => {
15+
if (env(c)['DISABLE_CACHE']) {
916
return next();
1017
}
11-
return cache({
12-
wait: false,
13-
cacheName: 'al-bff',
14-
cacheControl: 'max-age=3600',
18+
return kvCache({
19+
kvNamespace: env(c).CACHE_KV,
1520
keyGenerator: (c) => `${c.req.url}_${getWpLang(c, 'en')}`,
16-
vary: 'x-al-lang',
1721
})(c, next);
1822
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { Context, MiddlewareHandler } from 'hono';
2+
3+
/**
4+
* KV Cache Middleware based on `hono/cache` built for Cache API.
5+
*
6+
* @param {Object} options - The options for the KV cache middleware.
7+
* @param {KVNamespace} options.kvNamespace - The KV namespace to use for caching.
8+
* @param {string} [options.cacheKeyPrefix] - A prefix to add to cache keys.
9+
* @param {Function} [options.keyGenerator] - A function to generate cache keys.
10+
* @param {number} [options.ttl=3600] - Time-to-live for cached items in seconds.
11+
* @returns {MiddlewareHandler} The middleware handler function.
12+
*/
13+
export const kvCache = (options: {
14+
kvNamespace: KVNamespace;
15+
cacheKeyPrefix?: string;
16+
keyGenerator?: (c: Context) => Promise<string> | string;
17+
ttl?: number; // TTL in seconds
18+
}): MiddlewareHandler => {
19+
return async function kvCache(c, next) {
20+
let key = c.req.url;
21+
if (options.keyGenerator) {
22+
key = await options.keyGenerator(c);
23+
}
24+
25+
if (options.cacheKeyPrefix) {
26+
key = options.cacheKeyPrefix + key;
27+
}
28+
29+
// Attempt to retrieve the cached response
30+
const cachedResponseBody = await options.kvNamespace.get(
31+
key,
32+
'arrayBuffer',
33+
);
34+
if (cachedResponseBody) {
35+
// Retrieve stored headers
36+
const cachedHeadersJson = await options.kvNamespace.get(key + ':headers');
37+
const headers = new Headers();
38+
if (cachedHeadersJson) {
39+
const headersObj = JSON.parse(cachedHeadersJson);
40+
for (const [k, v] of Object.entries(headersObj)) {
41+
headers.set(k, v as string);
42+
}
43+
}
44+
return new Response(cachedResponseBody, { headers });
45+
}
46+
47+
// Proceed to the next middleware or handler
48+
await next();
49+
50+
// Cache the response if it's successful
51+
if (!c.res.ok) {
52+
return;
53+
}
54+
55+
// Clone the response to read its body
56+
const resClone = c.res.clone();
57+
const resBody = await resClone.arrayBuffer();
58+
59+
// Store the response body and headers in KV
60+
const ttl = options.ttl || 3600; // Default TTL is 1 hour
61+
await options.kvNamespace.put(key, resBody, { expirationTtl: ttl });
62+
63+
const headersObj: Record<string, string> = {};
64+
for (const [k, v] of resClone.headers.entries()) {
65+
headersObj[k] = v;
66+
}
67+
await options.kvNamespace.put(
68+
key + ':headers',
69+
JSON.stringify(headersObj),
70+
{ expirationTtl: ttl },
71+
);
72+
};
73+
};

libs/blog-bff/shared/util-middleware/tsconfig.lib.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
"extends": "./tsconfig.json",
33
"compilerOptions": {
44
"module": "commonjs",
5+
"lib": ["ESNext"],
56
"outDir": "../../../../dist/out-tsc",
67
"declaration": true,
7-
"types": ["node"]
8+
"types": ["node", "@cloudflare/workers-types"]
89
},
910
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
1011
"include": ["src/**/*.ts"]

0 commit comments

Comments
 (0)