Skip to content

Commit 05ffd0e

Browse files
authored
Proper data cache tagging for computed content (#2915)
1 parent 701eaad commit 05ffd0e

File tree

21 files changed

+373
-180
lines changed

21 files changed

+373
-180
lines changed

.changeset/real-elephants-lie.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'gitbook-v2': patch
3+
'gitbook': patch
4+
---
5+
6+
Improving data cache management for computed content

.changeset/two-fans-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/cache-tags': minor
3+
---
4+
5+
Initial version of the package

bun.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@
2222
"wrangler": "^3.109.2",
2323
},
2424
},
25+
"packages/cache-tags": {
26+
"name": "@gitbook/cache-tags",
27+
"version": "0.0.0",
28+
"dependencies": {
29+
"@gitbook/api": "0.96.1",
30+
"assert-never": "^1.2.1",
31+
},
32+
"devDependencies": {
33+
"typescript": "^5.5.3",
34+
},
35+
},
2536
"packages/colors": {
2637
"name": "@gitbook/colors",
2738
"version": "0.2.0",
@@ -42,6 +53,7 @@
4253
"dependencies": {
4354
"@gitbook/api": "0.96.1",
4455
"@gitbook/cache-do": "workspace:*",
56+
"@gitbook/cache-tags": "workspace:*",
4557
"@gitbook/colors": "workspace:*",
4658
"@gitbook/emoji-codepoints": "workspace:*",
4759
"@gitbook/icons": "workspace:*",
@@ -122,6 +134,7 @@
122134
"version": "0.1.1",
123135
"dependencies": {
124136
"@gitbook/api": "0.96.1",
137+
"@gitbook/cache-tags": "workspace:*",
125138
"@sindresorhus/fnv1a": "^3.1.0",
126139
"next": "^15.2.0",
127140
"react": "^19.0.0",
@@ -618,6 +631,8 @@
618631

619632
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
620633

634+
"@gitbook/cache-tags": ["@gitbook/cache-tags@workspace:packages/cache-tags"],
635+
621636
"@gitbook/colors": ["@gitbook/colors@workspace:packages/colors"],
622637

623638
"@gitbook/emoji-codepoints": ["@gitbook/emoji-codepoints@workspace:packages/emoji-codepoints"],

packages/cache-tags/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/

packages/cache-tags/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# `@gitbook/cache-tags`
2+
3+
Utility to generate cache tags for GitBook Open.

packages/cache-tags/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@gitbook/cache-tags",
3+
"type": "module",
4+
"exports": {
5+
".": {
6+
"types": "./dist/index.d.ts",
7+
"development": "./src/index.ts",
8+
"default": "./dist/index.js"
9+
}
10+
},
11+
"version": "0.0.0",
12+
"dependencies": {
13+
"@gitbook/api": "0.96.1",
14+
"assert-never": "^1.2.1"
15+
},
16+
"devDependencies": {
17+
"typescript": "^5.5.3"
18+
},
19+
"scripts": {
20+
"build": "tsc",
21+
"typecheck": "tsc --noEmit",
22+
"dev": "tsc -w"
23+
},
24+
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
25+
}

packages/cache-tags/src/index.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import type { ComputedContentSource } from '@gitbook/api';
2+
import assertNever from 'assert-never';
3+
4+
/**
5+
* Get a stringified cache tag for a given object.
6+
*/
7+
export function getCacheTag(
8+
spec: /**
9+
* All data related to a user
10+
* @deprecated - in v2, no tag as this is an immutable data
11+
*/
12+
| {
13+
tag: 'user';
14+
user: string;
15+
}
16+
/**
17+
* All data related to a space
18+
*/
19+
| {
20+
tag: 'space';
21+
space: string;
22+
}
23+
/**
24+
* All data related to an integration.
25+
*/
26+
| {
27+
tag: 'integration';
28+
integration: string;
29+
}
30+
/**
31+
* All data related to a change request
32+
*/
33+
| {
34+
tag: 'change-request';
35+
space: string;
36+
changeRequest: string;
37+
}
38+
/**
39+
* Immutable data related to a revision
40+
* @deprecated - in v2, no tag as this is an immutable data
41+
*/
42+
| {
43+
tag: 'revision';
44+
space: string;
45+
revision: string;
46+
}
47+
/**
48+
* Immutable data related to a document
49+
* @deprecated - in v2, no tag as this is an immutable data
50+
*/
51+
| {
52+
tag: 'document';
53+
space: string;
54+
document: string;
55+
}
56+
/**
57+
* Immutable data related to a computed document
58+
* @deprecated - in v2, no tag as this is an immutable data
59+
*/
60+
| {
61+
tag: 'computed-document';
62+
space: string;
63+
integration: string;
64+
}
65+
/**
66+
* All data related to the URL of a content
67+
*/
68+
| {
69+
tag: 'url';
70+
hostname: string;
71+
}
72+
/**
73+
* All data related to a site
74+
*/
75+
| {
76+
tag: 'site';
77+
site: string;
78+
}
79+
/**
80+
* All data related to an OpenAPI spec
81+
*/
82+
| {
83+
tag: 'openapi';
84+
organization: string;
85+
openAPISpec: string;
86+
}
87+
): string {
88+
switch (spec.tag) {
89+
case 'user':
90+
return `user:${spec.user}`;
91+
case 'url':
92+
return `url:${spec.hostname}`;
93+
case 'space':
94+
return `space:${spec.space}`;
95+
case 'change-request':
96+
return `space:${spec.space}:change-request:${spec.changeRequest}`;
97+
case 'revision':
98+
return `space:${spec.space}:revision:${spec.revision}`;
99+
case 'document':
100+
return `space:${spec.space}:document:${spec.document}`;
101+
case 'computed-document':
102+
return `space:${spec.space}:computed-document:${spec.integration}`;
103+
case 'site':
104+
return `site:${spec.site}`;
105+
case 'integration':
106+
return `integration:${spec.integration}`;
107+
case 'openapi':
108+
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
109+
default:
110+
assertNever(spec);
111+
}
112+
}
113+
114+
/**
115+
* Get the tags for a computed content source.
116+
*/
117+
export function getComputedContentSourceCacheTags(
118+
inContext: {
119+
spaceId: string;
120+
organizationId: string;
121+
},
122+
source: ComputedContentSource
123+
) {
124+
const tags: string[] = [];
125+
126+
// We add the dependencies as tags, to ensure that the computed content is invalidated
127+
// when the dependencies are updated.
128+
const dependencies = Object.values(source.dependencies ?? {});
129+
if (dependencies.length > 0) {
130+
dependencies.forEach((dependency) => {
131+
switch (dependency.ref.kind) {
132+
case 'space':
133+
tags.push(
134+
getCacheTag({
135+
tag: 'space',
136+
space: dependency.ref.space,
137+
})
138+
);
139+
break;
140+
case 'openapi':
141+
tags.push(
142+
getCacheTag({
143+
tag: 'openapi',
144+
organization: inContext.organizationId,
145+
openAPISpec: dependency.ref.spec,
146+
})
147+
);
148+
break;
149+
default:
150+
// Do not throw for unknown dependency types
151+
// as it might mean we are lacking behind the API version
152+
break;
153+
}
154+
});
155+
} else {
156+
// Push a dummy tag, as the v1 is only using the first tag
157+
tags.push(
158+
getCacheTag({
159+
tag: 'computed-document',
160+
space: inContext.spaceId,
161+
integration: source.integration,
162+
})
163+
);
164+
}
165+
166+
// We invalidate the computed content when a new version of the integration is deployed.
167+
tags.push(
168+
getCacheTag({
169+
tag: 'integration',
170+
integration: source.integration,
171+
})
172+
);
173+
174+
return tags;
175+
}

packages/cache-tags/tsconfig.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"strict": true,
8+
"noEmit": false,
9+
"declaration": true,
10+
"outDir": "dist",
11+
"esModuleInterop": true,
12+
"module": "esnext",
13+
"moduleResolution": "bundler",
14+
"resolveJsonModule": true,
15+
"isolatedModules": true,
16+
"jsx": "react-jsx",
17+
"incremental": true,
18+
"types": [
19+
"bun-types" // add Bun global
20+
]
21+
},
22+
"include": ["src/**/*.ts", "src/**/*.tsx"],
23+
"exclude": ["node_modules"]
24+
}

packages/gitbook-v2/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"react": "^19.0.0",
88
"react-dom": "^19.0.0",
99
"@gitbook/api": "0.96.1",
10+
"@gitbook/cache-tags": "workspace:*",
1011
"@sindresorhus/fnv1a": "^3.1.0",
1112
"server-only": "^0.0.1"
1213
},

packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {
33
generateSitePageMetadata,
44
generateSitePageViewport,
55
} from '@/components/SitePage';
6+
import { getCacheTag } from '@gitbook/cache-tags';
67
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
7-
import { getSiteCacheTag } from '@v2/lib/cache';
88
import type { Metadata, Viewport } from 'next';
99
import { unstable_cacheTag as cacheTag } from 'next/cache';
1010

@@ -21,7 +21,12 @@ export default async function Page(props: PageProps) {
2121
const context = await getStaticSiteContext(params);
2222
const pathname = getPagePathFromParams(params);
2323

24-
cacheTag(getSiteCacheTag(context.site.id));
24+
cacheTag(
25+
getCacheTag({
26+
tag: 'site',
27+
site: context.site.id,
28+
})
29+
);
2530

2631
return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />;
2732
}

0 commit comments

Comments
 (0)