Skip to content

Commit 2411f25

Browse files
committed
Even easier
1 parent b5073aa commit 2411f25

File tree

9 files changed

+138
-162
lines changed

9 files changed

+138
-162
lines changed

.changeset/young-bears-tease.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@ const graphqlServer = createYoga<Env & ExecutionContext>({
2020
schema: createSchema({ typeDefs, resolvers }),
2121
plugins: [
2222
useResponseCache({
23-
cache: ctx =>
24-
createKvCache({
25-
KV: ctx.GRAPHQL_RESPONSE_CACHE,
26-
waitUntil: ctx.waitUntil,
27-
keyPrefix: 'graphql' // optional
28-
}),
23+
cache: createKvCache({
24+
KVName: 'GRAPHQL_RESPONSE_CACHE',
25+
keyPrefix: 'graphql' // optional
26+
}),
2927
session: () => null,
3028
includeExtensionMetadata: true,
3129
ttl: 1000 * 10 // 10 seconds

packages/plugins/response-cache-cloudflare-kv/README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,10 @@ const graphqlServer = createYoga<Env & ExecutionContext>({
3838
schema: createSchema({ typeDefs, resolvers }),
3939
plugins: [
4040
useResponseCache({
41-
cache: ctx =>
42-
createKvCache({
43-
KV: ctx.GRAPHQL_RESPONSE_CACHE,
44-
waitUntil: ctx.waitUntil,
45-
keyPrefix: 'graphql' // optional
46-
}),
41+
cache: createKvCache({
42+
KVName: 'GRAPHQL_RESPONSE_CACHE',
43+
keyPrefix: 'graphql' // optional
44+
}),
4745
session: () => null,
4846
includeExtensionMetadata: true,
4947
ttl: 1000 * 10 // 10 seconds

packages/plugins/response-cache-cloudflare-kv/src/index.ts

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@ import { buildOperationKey } from './cache-key.js';
55
import { invalidate } from './invalidate.js';
66
import { set } from './set.js';
77

8-
export type KvCacheConfig = {
8+
export type KvCacheConfig<TKVNamespaceName extends string> = {
99
/**
10-
* The Cloudflare KV namespace that should be used to store the cache
10+
* The name of the Cloudflare KV namespace that should be used to store the cache
1111
*/
12-
KV: KVNamespace;
13-
/**
14-
* The function that should be used to wait for non-blocking actions to complete
15-
*/
16-
waitUntil: (promise: Promise<unknown>) => void;
12+
KVName: TKVNamespaceName;
1713
/**
1814
* Defines the length of time in milliseconds that a KV result is cached in the global network location it is accessed from.
1915
*
@@ -35,7 +31,14 @@ export type KvCacheConfig = {
3531
* @param config Modify the behavior of the cache as it pertains to Cloudflare KV
3632
* @returns A cache object that can be passed to envelop's `useResponseCache` plugin
3733
*/
38-
export function createKvCache(config: KvCacheConfig): Cache {
34+
export function createKvCache<
35+
TKVNamespaceName extends string,
36+
TServerContext extends {
37+
[TKey in TKVNamespaceName]: KVNamespace;
38+
} & {
39+
waitUntil(fn: Promise<unknown>): void;
40+
},
41+
>(config: KvCacheConfig<TKVNamespaceName>): (ctx: TServerContext) => Cache {
3942
if (config.cacheReadTTL && config.cacheReadTTL < 60000) {
4043
// eslint-disable-next-line no-console
4144
console.warn(
@@ -44,36 +47,37 @@ export function createKvCache(config: KvCacheConfig): Cache {
4447
}
4548
const computedTtlInSeconds = Math.max(Math.floor((config.cacheReadTTL ?? 60000) / 1000), 60); // KV TTL must be at least 60 seconds
4649

47-
const cache: Cache = {
48-
async get(id: string) {
49-
const kvResponse = await config.KV.get(buildOperationKey(id, config.keyPrefix), {
50-
type: 'text',
51-
cacheTtl: computedTtlInSeconds,
52-
});
53-
if (kvResponse) {
54-
return JSON.parse(kvResponse) as ExecutionResult;
55-
}
56-
return undefined;
57-
},
50+
return function KVCacheFactory(ctx: TServerContext) {
51+
return {
52+
async get(id: string) {
53+
const kvResponse = await ctx[config.KVName].get(buildOperationKey(id, config.keyPrefix), {
54+
type: 'text',
55+
cacheTtl: computedTtlInSeconds,
56+
});
57+
if (kvResponse) {
58+
return JSON.parse(kvResponse) as ExecutionResult;
59+
}
60+
return undefined;
61+
},
5862

59-
set(
60-
/** id/hash of the operation */
61-
id: string,
62-
/** the result that should be cached */
63-
data: ExecutionResult,
64-
/** array of entity records that were collected during execution */
65-
entities: Iterable<CacheEntityRecord>,
66-
/** how long the operation should be cached (in milliseconds) */
67-
ttl: number,
68-
) {
69-
// Do not block execution of the worker while caching the result
70-
config.waitUntil(set(id, data, entities, ttl, config));
71-
},
63+
set(
64+
/** id/hash of the operation */
65+
id: string,
66+
/** the result that should be cached */
67+
data: ExecutionResult,
68+
/** array of entity records that were collected during execution */
69+
entities: Iterable<CacheEntityRecord>,
70+
/** how long the operation should be cached (in milliseconds) */
71+
ttl: number,
72+
) {
73+
// Do not block execution of the worker while caching the result
74+
ctx.waitUntil(set(id, data, entities, ttl, ctx[config.KVName], config.keyPrefix));
75+
},
7276

73-
invalidate(entities: Iterable<CacheEntityRecord>) {
74-
// Do not block execution of the worker while invalidating the cache
75-
config.waitUntil(invalidate(entities, config));
76-
},
77+
invalidate(entities: Iterable<CacheEntityRecord>) {
78+
// Do not block execution of the worker while invalidating the cache
79+
ctx.waitUntil(invalidate(entities, ctx[config.KVName], config.keyPrefix));
80+
},
81+
};
7782
};
78-
return cache;
7983
}

packages/plugins/response-cache-cloudflare-kv/src/invalidate.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
import { KVNamespace } from '@cloudflare/workers-types';
12
import type { CacheEntityRecord } from '@envelop/response-cache';
23
import { buildEntityKey } from './cache-key.js';
3-
import type { KvCacheConfig } from './index.js';
44

55
export async function invalidate(
66
entities: Iterable<CacheEntityRecord>,
7-
config: KvCacheConfig,
7+
KV: KVNamespace,
8+
keyPrefix?: string,
89
): Promise<void> {
910
const kvPromises: Promise<unknown>[] = []; // Collecting all the KV operations so we can await them all at once
1011
const entityInvalidationPromises: Promise<unknown>[] = []; // Parallelize invalidation of each entity
1112

1213
for (const entity of entities) {
13-
entityInvalidationPromises.push(invalidateCacheEntityRecord(entity, kvPromises, config));
14+
entityInvalidationPromises.push(invalidateCacheEntityRecord(entity, kvPromises, KV, keyPrefix));
1415
}
1516
await Promise.allSettled(entityInvalidationPromises);
1617
await Promise.allSettled(kvPromises);
@@ -20,24 +21,25 @@ export async function invalidateCacheEntityRecord(
2021
entity: CacheEntityRecord,
2122
/** Collect all inner promises to batch await all async operations outside the function */
2223
kvPromiseCollection: Promise<unknown>[],
23-
config: KvCacheConfig,
24+
KV: KVNamespace,
25+
keyPrefix?: string,
2426
) {
25-
const entityKey = buildEntityKey(entity.typename, entity.id, config.keyPrefix);
27+
const entityKey = buildEntityKey(entity.typename, entity.id, keyPrefix);
2628

27-
for await (const kvKey of getAllKvKeysForPrefix(entityKey, config)) {
29+
for await (const kvKey of getAllKvKeysForPrefix(entityKey, KV)) {
2830
if (kvKey.metadata?.operationKey) {
29-
kvPromiseCollection.push(config.KV.delete(kvKey.metadata?.operationKey));
30-
kvPromiseCollection.push(config.KV.delete(kvKey.name));
31+
kvPromiseCollection.push(KV.delete(kvKey.metadata?.operationKey));
32+
kvPromiseCollection.push(KV.delete(kvKey.name));
3133
}
3234
}
3335
}
3436

35-
export async function* getAllKvKeysForPrefix(prefix: string, config: KvCacheConfig) {
37+
export async function* getAllKvKeysForPrefix(prefix: string, KV: KVNamespace) {
3638
let keyListComplete = false;
3739
let cursor: string | undefined;
3840

3941
do {
40-
const kvListResponse = await config.KV.list<{ operationKey: string }>({
42+
const kvListResponse = await KV.list<{ operationKey: string }>({
4143
prefix,
4244
cursor,
4345
});

packages/plugins/response-cache-cloudflare-kv/src/set.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ExecutionResult } from 'graphql';
2+
import { KVNamespace } from '@cloudflare/workers-types';
23
import type { CacheEntityRecord } from '@envelop/response-cache';
34
import { buildEntityKey, buildOperationKey } from './cache-key.js';
4-
import type { KvCacheConfig } from './index.js';
55

66
export async function set(
77
/** id/hash of the operation */
@@ -12,15 +12,16 @@ export async function set(
1212
entities: Iterable<CacheEntityRecord>,
1313
/** how long the operation should be cached (in milliseconds) */
1414
ttl: number,
15-
config: KvCacheConfig,
15+
KV: KVNamespace,
16+
keyPrefix?: string,
1617
): Promise<void> {
1718
const ttlInSeconds = Math.max(Math.floor(ttl / 1000), 60); // KV TTL must be at least 60 seconds
18-
const operationKey = buildOperationKey(id, config.keyPrefix);
19+
const operationKey = buildOperationKey(id, keyPrefix);
1920
const operationKeyWithoutPrefix = buildOperationKey(id);
2021
const kvPromises: Promise<unknown>[] = []; // Collecting all the KV operations so we can await them all at once
2122

2223
kvPromises.push(
23-
config.KV.put(operationKey, JSON.stringify(data), {
24+
KV.put(operationKey, JSON.stringify(data), {
2425
expirationTtl: ttlInSeconds,
2526
metadata: { operationKey },
2627
}),
@@ -29,9 +30,9 @@ export async function set(
2930
// Store connections between the entities and the operation key
3031
// E.g if the entities are User:1 and User:2, we need to know that the operation key is connected to both of them
3132
for (const entity of entities) {
32-
const entityKey = buildEntityKey(entity.typename, entity.id, config.keyPrefix);
33+
const entityKey = buildEntityKey(entity.typename, entity.id, keyPrefix);
3334
kvPromises.push(
34-
config.KV.put(`${entityKey}:${operationKeyWithoutPrefix}`, operationKey, {
35+
KV.put(`${entityKey}:${operationKeyWithoutPrefix}`, operationKey, {
3536
expirationTtl: ttlInSeconds,
3637
metadata: { operationKey },
3738
}),

packages/plugins/response-cache-cloudflare-kv/test/index.spec.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,33 @@ type Env = {
1010
};
1111

1212
describe('@envelop/response-cache-cloudflare-kv integration tests', () => {
13-
let env: Env;
14-
let config: KvCacheConfig;
1513
let maxTtl: number;
16-
let executionContext: ExecutionContext;
1714
let cache: Cache;
1815
const dataValue: ExecutionResult<{ key: string }, { extensions: string }> = {
1916
errors: [],
2017
data: { key: 'value' },
2118
extensions: { extensions: 'value' },
2219
};
2320
const dataKey = '1B9502F92EFA53AFF0AC650794AA79891E4B6900';
21+
let KV: KVNamespace;
22+
let executionContext: ExecutionContext;
23+
const keyPrefix = 'vitest';
24+
const KVName = 'GRAPHQL_RESPONSE_CACHE';
2425

2526
beforeEach(() => {
2627
// @ts-expect-error - Unable to get jest-environment-miniflare/globals working the test/build setup
27-
env = getMiniflareBindings<Env>();
28+
const env = getMiniflareBindings<Env>();
2829
// @ts-expect-error - Unable to get jest-environment-miniflare/globals working the test/build setup
2930
executionContext = new ExecutionContext();
30-
config = {
31-
KV: env.GRAPHQL_RESPONSE_CACHE,
32-
waitUntil: executionContext.waitUntil,
33-
keyPrefix: 'vitest',
34-
};
31+
KV = env[KVName];
3532
maxTtl = 60 * 1000; // 1 minute
36-
cache = createKvCache(config);
33+
cache = createKvCache({
34+
KVName,
35+
keyPrefix,
36+
})({
37+
GRAPHQL_RESPONSE_CACHE: KV,
38+
waitUntil: executionContext.waitUntil.bind(executionContext),
39+
});
3740
});
3841

3942
test('should work with a basic set() and get()', async () => {
@@ -49,8 +52,8 @@ describe('@envelop/response-cache-cloudflare-kv integration tests', () => {
4952
const result = await cache.get(dataKey);
5053
expect(result).toEqual(dataValue);
5154

52-
const operationKey = buildOperationKey(dataKey, config.keyPrefix);
53-
const operationValue = await env.GRAPHQL_RESPONSE_CACHE.get(operationKey, 'text');
55+
const operationKey = buildOperationKey(dataKey, keyPrefix);
56+
const operationValue = await KV.get(operationKey, 'text');
5457
expect(operationValue).toBeTruthy();
5558
expect(JSON.parse(operationValue!)).toEqual(dataValue);
5659
});
@@ -77,7 +80,7 @@ describe('@envelop/response-cache-cloudflare-kv integration tests', () => {
7780
const result = await cache.get(dataKey);
7881
expect(result).toBeUndefined();
7982

80-
const allKeys = await config.KV.list();
83+
const allKeys = await KV.list();
8184
expect(allKeys.keys.length).toEqual(0);
8285
});
8386
});

0 commit comments

Comments
 (0)