Skip to content

Respond with all tags of all cache used for page generation #2973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/gitbook/src/cloudflare-entrypoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-ignore
import nextOnPagesHandler from '@cloudflare/next-on-pages/fetch-handler';

import { withResponseCacheTags } from './lib/cache/response';
import { withMiddlewareHeadersStorage } from './lib/middleware';

/**
Expand All @@ -9,8 +10,8 @@ import { withMiddlewareHeadersStorage } from './lib/middleware';
*/
export default {
async fetch(request, env, ctx) {
const response = await withMiddlewareHeadersStorage(() =>
nextOnPagesHandler.fetch(request, env, ctx)
const response = await withResponseCacheTags(() =>
withMiddlewareHeadersStorage(() => nextOnPagesHandler.fetch(request, env, ctx))
);

return response;
Expand Down
8 changes: 8 additions & 0 deletions packages/gitbook/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export const getUserById = cache({
tag: 'user',
user: userId,
}),
tagImmutable: true,
get: async (userId: string, options: CacheFunctionOptions) => {
try {
const apiCtx = await api();
Expand Down Expand Up @@ -379,6 +380,7 @@ export const getRevision = cache({
name: 'api.getRevision.v2',
tag: (spaceId, revisionId) =>
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
tagImmutable: true,
getKeySuffix: getAPIContextId,
get: async (
spaceId: string,
Expand Down Expand Up @@ -411,6 +413,7 @@ export const getRevisionPages = cache({
name: 'api.getRevisionPages.v4',
tag: (spaceId, revisionId) =>
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
tagImmutable: true,
getKeySuffix: getAPIContextId,
get: async (
spaceId: string,
Expand Down Expand Up @@ -446,6 +449,7 @@ export const getRevisionPageByPath = cache({
name: 'api.getRevisionPageByPath.v3',
tag: (spaceId, revisionId) =>
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
tagImmutable: true,
getKeySuffix: getAPIContextId,
get: async (
spaceId: string,
Expand Down Expand Up @@ -492,6 +496,7 @@ const getRevisionFileById = cache({
name: 'api.getRevisionFile.v3',
tag: (spaceId, revisionId) =>
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
tagImmutable: true,
get: async (
spaceId: string,
revisionId: string,
Expand Down Expand Up @@ -528,6 +533,7 @@ const getRevisionReusableContentById = cache({
name: 'api.getRevisionReusableContentById.v1',
tag: (spaceId, revisionId) =>
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
tagImmutable: true,
getKeySuffix: getAPIContextId,
get: async (
spaceId: string,
Expand Down Expand Up @@ -569,6 +575,7 @@ const getRevisionAllFiles = cache({
name: 'api.getRevisionAllFiles.v2',
tag: (spaceId, revisionId) =>
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
tagImmutable: true,
get: async (spaceId: string, revisionId: string, options: CacheFunctionOptions) => {
const response = await getAll(
async (params) => {
Expand Down Expand Up @@ -684,6 +691,7 @@ export const getDocument = cache({
name: 'api.getDocument.v2',
tag: (spaceId, documentId) =>
getCacheTag({ tag: 'document', space: spaceId, document: documentId }),
tagImmutable: true,
getKeySuffix: getAPIContextId,
get: async (spaceId: string, documentId: string, options: CacheFunctionOptions) => {
const apiCtx = await api();
Expand Down
9 changes: 9 additions & 0 deletions packages/gitbook/src/lib/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { assertIsNotV2 } from '../v2';
import { waitUntil } from '../waitUntil';
import { cacheBackends } from './backends';
import { memoryCache } from './memory';
import { addResponseCacheTag } from './response';
import type { CacheBackend, CacheEntry } from './types';

export type CacheFunctionOptions = {
Expand Down Expand Up @@ -54,6 +55,9 @@ export interface CacheDefinition<Args extends any[], Result> {
/** Tag to associate to the entry */
tag?: (...args: Args) => string;

/** If true, the tag will not be sent to the HTTP response, as we consider it immutable */
tagImmutable?: boolean;

/** Filter the arguments that should be taken into consideration for the cache key */
getKeyArgs?: (args: Args) => any[];

Expand Down Expand Up @@ -134,6 +138,11 @@ export function cache<Args extends any[], Result>(
let result: readonly [CacheEntry, string] | null = null;
const tag = cacheDef.tag?.(...args);

// Add the cache tag to the HTTP response
if (tag && !cacheDef.tagImmutable) {
addResponseCacheTag(tag);
}

// Try the memory backend, independently of the other backends as it doesn't have a network cost
const memoryEntry = await memoryCache.get({ key, tag });
if (memoryEntry) {
Expand Down
44 changes: 44 additions & 0 deletions packages/gitbook/src/lib/cache/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AsyncLocalStorage } from 'node:async_hooks';

const symbolResponseCacheTagsLocalStorage = Symbol.for('__gitbook-response-cache-tags__');

/**
* Wrap a response handler with a storage to store cache tags and write them as header to the response.
*/
export async function withResponseCacheTags(handler: () => Promise<Response>): Promise<Response> {
const responseCacheTagsLocalStorage =
// @ts-ignore
(globalThis[symbolResponseCacheTagsLocalStorage] as AsyncLocalStorage<Set<string>>) ??
new AsyncLocalStorage<Set<string>>();
// @ts-ignore
globalThis[symbolResponseCacheTagsLocalStorage] = responseCacheTagsLocalStorage;

const responseCacheTags = new Set<string>();
const response = await responseCacheTagsLocalStorage.run(responseCacheTags, handler);

if (responseCacheTags.size > 0) {
const headerCacheTag = Array.from(responseCacheTags).join(',');
response.headers.set('cache-tag', headerCacheTag);
response.headers.set('x-gitbook-cache-tag', headerCacheTag);
}

return response;
}

/**
* Add a cache tag to the HTTP response.
* This is a hack for v1, for v2, we use ISR and do not cache the whole response.
*/
export function addResponseCacheTag(...tags: string[]) {
const responseCacheTagsLocalStorage =
// @ts-ignore
(globalThis[symbolResponseCacheTagsLocalStorage] as AsyncLocalStorage<Set<string>>) ??
new AsyncLocalStorage<Set<string>>();
// @ts-ignore
globalThis[symbolResponseCacheTagsLocalStorage] = responseCacheTagsLocalStorage;

const store = responseCacheTagsLocalStorage.getStore();
tags.forEach((tag) => {
store?.add(tag);
});
}
4 changes: 4 additions & 0 deletions packages/gitbook/src/lib/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AsyncLocalStorage } from 'node:async_hooks';

/*
* This code is ONLY for v1.
*/

/**
* Set a header on the middleware response.
* We do this because of https://github.com/opennextjs/opennextjs-cloudflare/issues/92
Expand Down
7 changes: 3 additions & 4 deletions packages/gitbook/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import { joinPath, normalizePathname } from '@/lib/paths';
import { getProxyModeBasePath } from '@/lib/proxy';
import { MiddlewareHeaders } from '@v2/lib/middleware';
import { addResponseCacheTag } from './lib/cache/response';

export const config = {
matcher:
Expand Down Expand Up @@ -287,10 +288,8 @@ export async function middleware(request: NextRequest) {
}
}

if (resolved.cacheTags && resolved.cacheTags.length > 0) {
const headerCacheTag = resolved.cacheTags.join(',');
setMiddlewareHeader(response, 'cache-tag', headerCacheTag);
setMiddlewareHeader(response, 'x-gitbook-cache-tag', headerCacheTag);
if (resolved.cacheTags) {
addResponseCacheTag(...resolved.cacheTags);
}

return response;
Expand Down
Loading