mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Use Cloudflare Cache API as one of the cache backend (#59)
* Implement cache for cloudflare * Start using it * Fix cache use * Generate proper URLs * Comment * Fix cache always resulting in cache miss * Log * Correctly cancel redis * Write to fallback caches * Disable memory cache * Re-enable memory cache * Try patching * Use it * Also patch package.json * Fix TS errors * Remove yarn.lock * Fix local error * Lint * Try bunx to fix vercel build * Update lockfile * Add back revalidating
This commit is contained in:
+4
-1
@@ -10,7 +10,8 @@
|
||||
"lint": "next lint",
|
||||
"format": "prettier ./ --ignore-unknown --write",
|
||||
"format:check": "prettier ./ --ignore-unknown --list-different",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"postinstall": "bunx patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
@@ -46,6 +47,7 @@
|
||||
"@argos-ci/cli": "^1.0.4",
|
||||
"@argos-ci/puppeteer": "^1.2.1",
|
||||
"@cloudflare/next-on-pages": "^1.7.3",
|
||||
"@cloudflare/workers-types": "^4.20231218.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
"@types/katex": "^0.16.5",
|
||||
@@ -59,6 +61,7 @@
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.6",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.0.3",
|
||||
"psi": "^4.1.0",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
diff --git a/node_modules/@cloudflare/next-on-pages/dist/helpers/getRequestCloudflareContext.d.ts b/node_modules/@cloudflare/next-on-pages/dist/helpers/getRequestCloudflareContext.d.ts
|
||||
new file mode 100644
|
||||
index 0000000..e575127
|
||||
--- /dev/null
|
||||
+++ b/node_modules/@cloudflare/next-on-pages/dist/helpers/getRequestCloudflareContext.d.ts
|
||||
@@ -0,0 +1,11 @@
|
||||
+/// <reference types="@cloudflare/workers-types" />
|
||||
+/**
|
||||
+ * returns the request's execution context (usually referred as ctx).
|
||||
+ *
|
||||
+ * Note:
|
||||
+ * This function throws when run on the client, where there is execution context.
|
||||
+ * This function returns the mocked execution context in non pages environments.
|
||||
+ *
|
||||
+ * @returns the request's execution context
|
||||
+ */
|
||||
+export declare const getRequestCloudflareContext: () => { ctx: ExecutionContext, cf: IncomingRequestCfProperties<any> };
|
||||
diff --git a/node_modules/@cloudflare/next-on-pages/dist/helpers/index.d.ts b/node_modules/@cloudflare/next-on-pages/dist/helpers/index.d.ts
|
||||
new file mode 100644
|
||||
index 0000000..4a415d2
|
||||
--- /dev/null
|
||||
+++ b/node_modules/@cloudflare/next-on-pages/dist/helpers/index.d.ts
|
||||
@@ -0,0 +1 @@
|
||||
+export * from './getRequestCloudflareContext';
|
||||
diff --git a/node_modules/@cloudflare/next-on-pages/dist/helpers/index.js b/node_modules/@cloudflare/next-on-pages/dist/helpers/index.js
|
||||
new file mode 100644
|
||||
index 0000000..c6e2065
|
||||
--- /dev/null
|
||||
+++ b/node_modules/@cloudflare/next-on-pages/dist/helpers/index.js
|
||||
@@ -0,0 +1,15 @@
|
||||
+// src/helpers/getRequestExecutionContext.ts
|
||||
+var getRequestCloudflareContext = () => {
|
||||
+ if (typeof process === "undefined") {
|
||||
+ throw new Error(
|
||||
+ "Error: trying to access the request execution context on the client"
|
||||
+ );
|
||||
+ }
|
||||
+ const cloudflare = process.env.cloudflare;
|
||||
+ if (cloudflare)
|
||||
+ return cloudflare;
|
||||
+ return null;
|
||||
+ };
|
||||
+ export {
|
||||
+ getRequestCloudflareContext
|
||||
+ };
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/@cloudflare/next-on-pages/package.json b/node_modules/@cloudflare/next-on-pages/package.json
|
||||
index 494e9a9..f0e1e21 100644
|
||||
--- a/node_modules/@cloudflare/next-on-pages/package.json
|
||||
+++ b/node_modules/@cloudflare/next-on-pages/package.json
|
||||
@@ -3,12 +3,16 @@
|
||||
"version": "1.8.2",
|
||||
"bin": "./bin/index.js",
|
||||
"exports": {
|
||||
- "./__experimental__next-dev": "./dist/next-dev/index.cjs"
|
||||
+ "./__experimental__next-dev": "./dist/next-dev/index.cjs",
|
||||
+ "./helpers": "./dist/helpers/index.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"__experimental__next-dev": [
|
||||
"./dist/next-dev/index.d.ts"
|
||||
+ ],
|
||||
+ "helpers": [
|
||||
+ "./dist/helpers/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
diff --git a/node_modules/@cloudflare/next-on-pages/templates/_worker.js/index.ts b/node_modules/@cloudflare/next-on-pages/templates/_worker.js/index.ts
|
||||
index 929f6cc..7164930 100644
|
||||
--- a/node_modules/@cloudflare/next-on-pages/templates/_worker.js/index.ts
|
||||
+++ b/node_modules/@cloudflare/next-on-pages/templates/_worker.js/index.ts
|
||||
@@ -33,9 +33,15 @@ export default {
|
||||
return new Response(responseBody, { status: 503 });
|
||||
}
|
||||
|
||||
+ const cloudflare = {
|
||||
+ cf: request.cf,
|
||||
+ env,
|
||||
+ ctx,
|
||||
+ };
|
||||
+
|
||||
return envAsyncLocalStorage.run(
|
||||
// NOTE: The `SUSPENSE_CACHE_URL` is used to tell the Next.js Fetch Cache where to send requests.
|
||||
- { ...env, NODE_ENV: __NODE_ENV__, SUSPENSE_CACHE_URL },
|
||||
+ { ...env, cloudflare, NODE_ENV: __NODE_ENV__, SUSPENSE_CACHE_URL },
|
||||
async () => {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname.startsWith('/_next/image')) {
|
||||
@@ -3,12 +3,17 @@ import { revalidateTags } from '@/lib/cache';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
interface JsonBody {
|
||||
tags: string[];
|
||||
purge?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revalidate cached data based on tags.
|
||||
* The body should be a JSON with { tags: string[] }
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const json = await req.json();
|
||||
const json = (await req.json()) as JsonBody;
|
||||
|
||||
if (!json.tags || !Array.isArray(json.tags)) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -56,7 +56,7 @@ async function fetchVisitorID(): Promise<string> {
|
||||
mode: 'cors', // Need to use cors as we are on a different domain.
|
||||
});
|
||||
|
||||
const { deviceId } = await resp.json();
|
||||
const { deviceId } = (await resp.json()) as { deviceId: string };
|
||||
return deviceId;
|
||||
} catch (error) {
|
||||
return proposed;
|
||||
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
import { cloudflareCache } from './cloudflare';
|
||||
import { memoryCache } from './memory';
|
||||
import { redisCache } from './redis';
|
||||
|
||||
export const cacheBackends = [
|
||||
memoryCache,
|
||||
redisCache,
|
||||
// Cloudflare should be last to delete its cache from the listing of redis/memory
|
||||
cloudflareCache,
|
||||
];
|
||||
Vendored
+42
-28
@@ -1,8 +1,8 @@
|
||||
import hash from 'object-hash';
|
||||
|
||||
import { memoryCache } from './memory';
|
||||
import { redisCache } from './redis';
|
||||
import { cacheBackends } from './backends';
|
||||
import { CacheEntry } from './types';
|
||||
import { waitUntil } from './waitUntil';
|
||||
|
||||
export type CacheFunction<Args extends any[], Result> = ((...args: Args) => Promise<Result>) & {
|
||||
/**
|
||||
@@ -62,10 +62,8 @@ export function cache<Args extends any[], Result>(
|
||||
};
|
||||
|
||||
// Write it to the cache
|
||||
// As soon as it'll be possible with next-on-pages, we should `waitUntil`
|
||||
// to delay writing the cache after the response has been sent to the client.
|
||||
if (result.ttl && result.ttl > 0) {
|
||||
await setCacheEntry(key, cacheEntry);
|
||||
await waitUntil(setCacheEntry(key, cacheEntry));
|
||||
}
|
||||
const writeCacheDuration = now() - startTime - fetchDuration;
|
||||
|
||||
@@ -79,18 +77,15 @@ export function cache<Args extends any[], Result>(
|
||||
const fetchValue = async (key: string, ...args: Args) => {
|
||||
// Read the cache
|
||||
const startTime = now();
|
||||
const hasMemoryHit = !!(await memoryCache.get(key));
|
||||
const cachedEntry = await getCacheEntry(key);
|
||||
const readCacheDuration = now() - startTime;
|
||||
|
||||
// Returns it if it exists
|
||||
if (cachedEntry !== null) {
|
||||
console.log(
|
||||
`cache: ${key} hit in ${readCacheDuration.toFixed(
|
||||
0,
|
||||
)}ms (memory: ${hasMemoryHit}, redis: ${!!redisCache})`,
|
||||
`cache: ${key} hit on ${cachedEntry[1]} in ${readCacheDuration.toFixed(0)}ms`,
|
||||
);
|
||||
return cachedEntry.data;
|
||||
return cachedEntry[0].data;
|
||||
}
|
||||
|
||||
const fetched = await revalidate(key, ...args);
|
||||
@@ -99,7 +94,7 @@ export function cache<Args extends any[], Result>(
|
||||
0,
|
||||
)}ms, read in ${readCacheDuration.toFixed(
|
||||
0,
|
||||
)}ms, write in ${fetched.writeCacheDuration.toFixed(0)}ms (redis: ${!!redisCache})`,
|
||||
)}ms, write in ${fetched.writeCacheDuration.toFixed(0)}ms`,
|
||||
);
|
||||
|
||||
return fetched.data;
|
||||
@@ -153,7 +148,10 @@ export function getCache(name: string): CacheFunction<any[], any> | null {
|
||||
return registeredCaches.get(name) ?? null;
|
||||
}
|
||||
|
||||
function getCacheKey(fnName: string, args: any[]) {
|
||||
/**
|
||||
* Get a cache key for a function and its arguments.
|
||||
*/
|
||||
export function getCacheKey(fnName: string, args: any[]) {
|
||||
let innerKey = args.map((arg) => JSON.stringify(arg)).join(',');
|
||||
|
||||
// Avoid crazy long keys, by fallbacking to a hash
|
||||
@@ -165,26 +163,42 @@ function getCacheKey(fnName: string, args: any[]) {
|
||||
}
|
||||
|
||||
async function setCacheEntry(key: string, entry: CacheEntry) {
|
||||
await Promise.all([memoryCache.set(key, entry), redisCache?.set(key, entry)]);
|
||||
await Promise.all(cacheBackends.map((backend) => backend.set(key, entry)));
|
||||
}
|
||||
|
||||
async function getCacheEntry(key: string): Promise<CacheEntry | null> {
|
||||
const memoryEntry = await memoryCache.get(key);
|
||||
if (memoryEntry) {
|
||||
return memoryEntry;
|
||||
async function getCacheEntry(key: string): Promise<[CacheEntry, string] | null> {
|
||||
const abort = new AbortController();
|
||||
|
||||
let result: [CacheEntry, string] | null = null;
|
||||
|
||||
await Promise.all(
|
||||
cacheBackends.map(async (backend) => {
|
||||
try {
|
||||
const entry = await backend.get(key, { signal: abort.signal });
|
||||
if (entry && !result) {
|
||||
result = [entry, backend.name];
|
||||
abort.abort();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore all errors
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Write to the fallback caches
|
||||
if (result) {
|
||||
const [savedEntry, backendName] = result as [CacheEntry, string];
|
||||
|
||||
await waitUntil(
|
||||
Promise.all(
|
||||
cacheBackends
|
||||
.filter((backend) => backend.name !== backendName && backend.fallback)
|
||||
.map((backend) => backend.set(key, savedEntry)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const redisEntry = (await redisCache?.get(key)) ?? null;
|
||||
if (redisEntry) {
|
||||
await memoryCache.set(key, redisEntry);
|
||||
}
|
||||
|
||||
return redisEntry;
|
||||
} catch (error) {
|
||||
console.error(`Error while getting cache entry for ${key} from redis`, error);
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function now(): number {
|
||||
|
||||
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
import type { CacheStorage, Cache, Response as WorkerResponse } from '@cloudflare/workers-types';
|
||||
|
||||
import { CacheBackend, CacheEntry } from './types';
|
||||
|
||||
/**
|
||||
* Cache implementation using the Cloudflare Cache API.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/cache/
|
||||
*/
|
||||
export const cloudflareCache: CacheBackend = {
|
||||
name: 'cloudflare',
|
||||
fallback: true,
|
||||
async get(key, options) {
|
||||
const cache = getCache();
|
||||
if (!cache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = await serializeKey(key);
|
||||
const response = await cache.match(cacheKey);
|
||||
if (!response || options?.signal?.aborted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = await deserializeEntry(response);
|
||||
return entry;
|
||||
},
|
||||
async set(key, entry) {
|
||||
const cache = getCache();
|
||||
if (cache) {
|
||||
const cacheKey = await serializeKey(key);
|
||||
await cache.put(cacheKey, serializeEntry(entry));
|
||||
}
|
||||
},
|
||||
async revalidateTags(tags) {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
function getCache(): Cache | null {
|
||||
if (typeof caches === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return (caches as CacheStorage).default ?? null;
|
||||
}
|
||||
|
||||
async function serializeKey(key: string): Promise<string> {
|
||||
const digest = await crypto.subtle.digest(
|
||||
{
|
||||
name: 'SHA-256',
|
||||
},
|
||||
new TextEncoder().encode(key),
|
||||
);
|
||||
|
||||
const hash = Buffer.from(digest).toString('base64');
|
||||
|
||||
return `gitbook://gitbook.com/${hash}`;
|
||||
}
|
||||
|
||||
function serializeEntry(entry: CacheEntry): WorkerResponse {
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Type', 'application/json');
|
||||
headers.set('Cache-Control', `public, max-age=${(entry.meta.expiresAt - Date.now()) / 1000}`);
|
||||
|
||||
// @ts-ignore
|
||||
return new Response(JSON.stringify(entry), {
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
async function deserializeEntry(response: WorkerResponse): Promise<CacheEntry> {
|
||||
const entry = (await response.json()) as CacheEntry;
|
||||
return entry;
|
||||
}
|
||||
Vendored
+2
@@ -1,6 +1,8 @@
|
||||
import { CacheBackend, CacheEntry } from './types';
|
||||
|
||||
export const memoryCache: CacheBackend = {
|
||||
name: 'memory',
|
||||
fallback: true,
|
||||
async get(key) {
|
||||
const memoryCache = getMemoryCache();
|
||||
const memoryEntry = memoryCache.get(key);
|
||||
|
||||
Vendored
+91
-74
@@ -3,98 +3,115 @@ import { Redis } from '@upstash/redis/cloudflare';
|
||||
import { CacheBackend, CacheEntry, CacheEntryMeta } from './types';
|
||||
import { filterOutNullable } from '../typescript';
|
||||
|
||||
const redis =
|
||||
process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN
|
||||
? new Redis({
|
||||
url: process.env.UPSTASH_REDIS_REST_URL,
|
||||
token: process.env.UPSTASH_REDIS_REST_TOKEN,
|
||||
})
|
||||
: null;
|
||||
|
||||
const cacheNamespace = process.env.UPSTASH_REDIS_NAMESPACE ?? 'gitbook';
|
||||
const cacheVersion = 1;
|
||||
|
||||
export const redisCache: CacheBackend | null = redis
|
||||
? {
|
||||
async get(key) {
|
||||
try {
|
||||
const [, redisEntry] = await redis
|
||||
.multi()
|
||||
.json.numincrby(getRedisKey(key), '$.meta.hits', 1)
|
||||
.json.get(getRedisKey(key))
|
||||
.exec<[any, CacheEntry | null]>();
|
||||
if (!redisEntry) {
|
||||
return null;
|
||||
}
|
||||
export const redisCache: CacheBackend = {
|
||||
name: 'redis',
|
||||
async get(key, options) {
|
||||
const redis = getRedis(options?.signal);
|
||||
if (!redis) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return redisEntry;
|
||||
} catch (error) {
|
||||
// "JSON.NUMINCRBY" throws an error if the key does not exist
|
||||
if ((error as Error).message.includes('ERR no such key')) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const [, redisEntry] = await redis
|
||||
.multi()
|
||||
.json.numincrby(getRedisKey(key), '$.meta.hits', 1)
|
||||
.json.get(getRedisKey(key))
|
||||
.exec<[any, CacheEntry | null]>();
|
||||
if (!redisEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
return redisEntry;
|
||||
} catch (error) {
|
||||
// "JSON.NUMINCRBY" throws an error if the key does not exist
|
||||
if ((error as Error).message.includes('ERR no such key')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async set(key, entry) {
|
||||
const multi = redis.multi();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
const redisKey = getRedisKey(key);
|
||||
const expire = Math.max(0, (entry.meta.expiresAt - Date.now()) / 1000);
|
||||
async set(key, entry) {
|
||||
const redis = getRedis();
|
||||
if (!redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry.meta.tags.forEach((tag) => {
|
||||
const redisTagKey = getCacheTagKey(tag);
|
||||
const multi = redis.multi();
|
||||
|
||||
multi.sadd(redisTagKey, redisKey);
|
||||
const redisKey = getRedisKey(key);
|
||||
const expire = Math.max(0, (entry.meta.expiresAt - Date.now()) / 1000);
|
||||
|
||||
// Set am expiration on the tag to be the maximum of the expiration of all keys
|
||||
multi.expire(redisTagKey, expire, 'GT');
|
||||
multi.expire(redisTagKey, expire, 'NX');
|
||||
});
|
||||
entry.meta.tags.forEach((tag) => {
|
||||
const redisTagKey = getCacheTagKey(tag);
|
||||
|
||||
// @ts-ignore
|
||||
multi.json.set(redisKey, '$', entry);
|
||||
multi.expire(redisKey, expire);
|
||||
multi.sadd(redisTagKey, redisKey);
|
||||
|
||||
await multi.exec();
|
||||
},
|
||||
// Set am expiration on the tag to be the maximum of the expiration of all keys
|
||||
multi.expire(redisTagKey, expire, 'GT');
|
||||
multi.expire(redisTagKey, expire, 'NX');
|
||||
});
|
||||
|
||||
async revalidateTags(tags) {
|
||||
const keys = new Set(
|
||||
(
|
||||
await Promise.all(tags.map((tag) => redis.smembers(getCacheTagKey(tag))))
|
||||
).flat(),
|
||||
);
|
||||
// @ts-ignore
|
||||
multi.json.set(redisKey, '$', entry);
|
||||
multi.expire(redisKey, expire);
|
||||
|
||||
const pipeline = redis.pipeline();
|
||||
let metas: Array<CacheEntryMeta | null> = [];
|
||||
await multi.exec();
|
||||
},
|
||||
|
||||
if (keys.size > 0) {
|
||||
metas = (
|
||||
await redis.json.mget(
|
||||
// Hard limit to avoid fetching a massive list of data
|
||||
Array.from(keys).slice(0, 1000),
|
||||
'$.meta',
|
||||
)
|
||||
).flat() as Array<CacheEntryMeta | null>;
|
||||
async revalidateTags(tags) {
|
||||
const redis = getRedis();
|
||||
if (!redis) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Finally, delete all keys
|
||||
keys.forEach((key) => {
|
||||
pipeline.del(key);
|
||||
});
|
||||
}
|
||||
const keys = new Set(
|
||||
(await Promise.all(tags.map((tag) => redis.smembers(getCacheTagKey(tag))))).flat(),
|
||||
);
|
||||
|
||||
tags.forEach((tag) => {
|
||||
pipeline.del(getCacheTagKey(tag));
|
||||
});
|
||||
const pipeline = redis.pipeline();
|
||||
let metas: Array<CacheEntryMeta | null> = [];
|
||||
|
||||
await pipeline.exec();
|
||||
return metas.filter(filterOutNullable);
|
||||
},
|
||||
}
|
||||
: null;
|
||||
if (keys.size > 0) {
|
||||
metas = (
|
||||
await redis.json.mget(
|
||||
// Hard limit to avoid fetching a massive list of data
|
||||
Array.from(keys).slice(0, 1000),
|
||||
'$.meta',
|
||||
)
|
||||
).flat() as Array<CacheEntryMeta | null>;
|
||||
|
||||
// Finally, delete all keys
|
||||
keys.forEach((key) => {
|
||||
pipeline.del(key);
|
||||
});
|
||||
}
|
||||
|
||||
tags.forEach((tag) => {
|
||||
pipeline.del(getCacheTagKey(tag));
|
||||
});
|
||||
|
||||
await pipeline.exec();
|
||||
return metas.filter(filterOutNullable);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the redis client.
|
||||
*/
|
||||
export function getRedis(signal?: AbortSignal) {
|
||||
return process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN
|
||||
? new Redis({
|
||||
url: process.env.UPSTASH_REDIS_REST_URL,
|
||||
token: process.env.UPSTASH_REDIS_REST_TOKEN,
|
||||
signal,
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for a tag.
|
||||
|
||||
Vendored
+17
-5
@@ -1,8 +1,8 @@
|
||||
import pMap from 'p-map';
|
||||
|
||||
import { getCache } from './cache';
|
||||
import { memoryCache } from './memory';
|
||||
import { redisCache } from './redis';
|
||||
import { cacheBackends } from './backends';
|
||||
import { getCache, getCacheKey } from './cache';
|
||||
import { CacheEntryMeta } from './types';
|
||||
|
||||
/**
|
||||
* Revalidate all values associated with tags.
|
||||
@@ -13,9 +13,21 @@ export async function revalidateTags(tags: string[], purge: boolean): Promise<vo
|
||||
return;
|
||||
}
|
||||
|
||||
await memoryCache.revalidateTags(tags);
|
||||
const processed = new Set<string>();
|
||||
const metas: CacheEntryMeta[] = [];
|
||||
|
||||
const metas = await redisCache?.revalidateTags(tags);
|
||||
await Promise.all(
|
||||
cacheBackends.map(async (backend) => {
|
||||
const addedMetas = await backend.revalidateTags(tags);
|
||||
addedMetas.forEach((meta) => {
|
||||
const key = getCacheKey(meta.cache, meta.args);
|
||||
if (!processed.has(key)) {
|
||||
metas.push(meta);
|
||||
processed.add(key);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Refresh the values in the cache
|
||||
if (metas && !purge) {
|
||||
|
||||
Vendored
+8
-1
@@ -31,10 +31,17 @@ export interface CacheEntry {
|
||||
}
|
||||
|
||||
export interface CacheBackend {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* If true, we'll set entries in this cache that have been found in another cache.
|
||||
*/
|
||||
fallback?: boolean;
|
||||
|
||||
/**
|
||||
* Get a value from the cache.
|
||||
*/
|
||||
get(key: string): Promise<CacheEntry | null>;
|
||||
get(key: string, options?: { signal?: AbortSignal }): Promise<CacheEntry | null>;
|
||||
|
||||
/**
|
||||
* Set a value in the cache.
|
||||
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { getRequestCloudflareContext } from '@cloudflare/next-on-pages/helpers';
|
||||
|
||||
/**
|
||||
* Extend the lifetime of the event handler until the promise is resolved.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#contextwaituntil
|
||||
*/
|
||||
export async function waitUntil(promise: Promise<unknown>) {
|
||||
const cloudflare = getRequestCloudflareContext();
|
||||
if (cloudflare) {
|
||||
cloudflare.ctx.waitUntil(promise);
|
||||
} else {
|
||||
await promise;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user