Skip to content

Optimize fetching of revisions #3382

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 13 commits into from
Jun 23, 2025
21 changes: 7 additions & 14 deletions packages/gitbook-v2/src/lib/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getSiteStructureSections } from '@/lib/sites';
import type {
ChangeRequest,
PublishedSiteContent,
RevisionPage,
Revision,
RevisionPageDocument,
Site,
SiteCustomizationSettings,
Expand Down Expand Up @@ -84,11 +84,8 @@ export type GitBookSpaceContext = GitBookBaseContext & {
space: Space;
changeRequest: ChangeRequest | null;

/** ID of the current revision. */
revisionId: string;

/** Pages of the space. */
pages: RevisionPage[];
/** Revision of the space. */
revision: Revision;

/** Share key of the space. */
shareKey: string | undefined;
Expand Down Expand Up @@ -351,30 +348,26 @@ export async function fetchSpaceContextByIds(

const revisionId = ids.revision ?? changeRequest?.revision ?? space.revision;

const pages = await getDataOrNull(
dataFetcher.getRevisionPages({
const revision = await getDataOrNull(
dataFetcher.getRevision({
spaceId: ids.space,
revisionId,
// We only care about the Git metadata when the Git sync is enabled,
// otherwise we can optimize performance by not fetching it
metadata: !!space.gitSync,
}),

// When trying to render a revision with an invalid / non-existing ID,
// we should handle gracefully the 404 and throw notFound.
ids.revision ? [404] : undefined
);
if (!pages) {
if (!revision) {
notFound();
}

return {
...baseContext,
organizationId: space.organization,
space,
pages,
revision,
changeRequest,
revisionId,
shareKey: ids.shareKey,
};
}
Expand Down
114 changes: 1 addition & 113 deletions packages/gitbook-v2/src/lib/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-t
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
import { cache } from '../cache';
import { DataFetcherError, throwIfDataError, wrapCacheDataFetcherError } from './errors';
import { DataFetcherError, wrapCacheDataFetcherError } from './errors';
import type { GitBookDataFetcher } from './types';

interface DataFetcherInput {
Expand Down Expand Up @@ -76,24 +76,6 @@ export function createDataFetcher(
})
);
},
getRevisionPages(params) {
return trace('getRevisionPages', () =>
getRevisionPages(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
metadata: params.metadata,
})
);
},
getRevisionFile(params) {
return trace('getRevisionFile', () =>
getRevisionFile(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
fileId: params.fileId,
})
);
},
getRevisionPageByPath(params) {
return trace('getRevisionPageByPath', () =>
getRevisionPageByPath(input, {
Expand Down Expand Up @@ -121,15 +103,6 @@ export function createDataFetcher(
})
);
},
getReusableContent(params) {
return trace('getReusableContent', () =>
getReusableContent(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
reusableContentId: params.reusableContentId,
})
);
},
getLatestOpenAPISpecVersionContent(params) {
return trace('getLatestOpenAPISpecVersionContent', () =>
getLatestOpenAPISpecVersionContent(input, {
Expand Down Expand Up @@ -304,62 +277,6 @@ const getRevision = cache(
}
);

const getRevisionPages = cache(
async (
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; metadata: boolean }
) => {
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
const api = apiClient(input);
const res = await api.spaces.listPagesInRevisionById(
params.spaceId,
params.revisionId,
{
metadata: params.metadata,
},
{
...noCacheFetchOptions,
}
);
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
return res.data.pages;
});
});
}
);

const getRevisionFile = cache(
async (
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; fileId: string }
) => {
return wrapCacheDataFetcherError(async () => {
return trace(
`getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
async () => {
const revision = await throwIfDataError(
getRevision(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
})
);

const file = revision.files.find((file) => file.id === params.fileId);

if (!file) {
throw new DataFetcherError('File not found', 404);
}

return file;
}
);
});
}
);

const getRevisionPageMarkdown = cache(
async (
input: DataFetcherInput,
Expand Down Expand Up @@ -527,35 +444,6 @@ const getComputedDocument = cache(
}
);

const getReusableContent = cache(
async (
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; reusableContentId: string }
) => {
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
async () => {
const api = apiClient(input);
const res = await api.spaces.getReusableContentInRevisionById(
params.spaceId,
params.revisionId,
params.reusableContentId,
{},
{
...noCacheFetchOptions,
}
);
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
return res.data;
}
);
});
}
);

const getLatestOpenAPISpecVersionContent = cache(
async (input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
'use cache';
Expand Down
1 change: 1 addition & 0 deletions packages/gitbook-v2/src/lib/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './errors';
export * from './lookup';
export * from './visitor';
export * from './pages';
export * from './revisions';
36 changes: 36 additions & 0 deletions packages/gitbook-v2/src/lib/data/revisions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Revision, RevisionFile, RevisionReusableContent } from '@gitbook/api';
import * as React from 'react';

const getRevisionReusableContents = React.cache((revision: Revision) => {
return new Map(
revision.reusableContents.map((reusableContent) => [reusableContent.id, reusableContent])
);
});

const getRevisionFiles = React.cache((revision: Revision) => {
return new Map(revision.files.map((file) => [file.id, file]));
});

/**
* Get a revision file by its ID.
*/
export function getRevisionFile(input: {
revision: Revision;
fileId: string;
}): RevisionFile | null {
const { revision, fileId } = input;
const files = getRevisionFiles(revision);
return files.get(fileId) ?? null;
}

/**
* Get a revision reusable content by its ID.
*/
export function getRevisionReusableContent(input: {
revision: Revision;
reusableContentId: string;
}): RevisionReusableContent | null {
const { revision, reusableContentId } = input;
const reusableContents = getRevisionReusableContents(revision);
return reusableContents.get(reusableContentId) ?? null;
}
27 changes: 0 additions & 27 deletions packages/gitbook-v2/src/lib/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,24 +69,6 @@ export interface GitBookDataFetcher {
revisionId: string;
}): Promise<DataFetcherResponse<api.Revision>>;

/**
* Get the revision pages by its space ID and revision ID.
*/
getRevisionPages(params: {
spaceId: string;
revisionId: string;
metadata: boolean;
}): Promise<DataFetcherResponse<api.RevisionPage[]>>;

/**
* Get a revision file by its space ID, revision ID and file ID.
*/
getRevisionFile(params: {
spaceId: string;
revisionId: string;
fileId: string;
}): Promise<DataFetcherResponse<api.RevisionFile>>;

/**
* Get a revision page by its path.
*/
Expand Down Expand Up @@ -131,15 +113,6 @@ export interface GitBookDataFetcher {
seed: string;
}): Promise<DataFetcherResponse<api.JSONDocument>>;

/**
* Get a reusable content by its space ID, revision ID and reusable content ID.
*/
getReusableContent(params: {
spaceId: string;
revisionId: string;
reusableContentId: string;
}): Promise<DataFetcherResponse<api.RevisionReusableContent>>;

/**
* Get the latest OpenAPI spec version content by its organization ID and slug.
*/
Expand Down
12 changes: 2 additions & 10 deletions packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { headers } from 'next/headers';
import React from 'react';

import { tcls } from '@/lib/tailwind';
import { throwIfDataError } from '@v2/lib/data';

import { DateRelative } from '../primitives';
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
Expand Down Expand Up @@ -59,7 +58,7 @@ export async function AdminToolbar(props: AdminToolbarProps) {
return <ChangeRequestToolbar context={context} />;
}

if (context.revisionId !== context.space.revision) {
if (context.revision.id !== context.space.revision) {
return <RevisionToolbar context={context} />;
}

Expand Down Expand Up @@ -106,14 +105,7 @@ async function ChangeRequestToolbar(props: { context: GitBookSiteContext }) {

async function RevisionToolbar(props: { context: GitBookSiteContext }) {
const { context } = props;
const { space, revisionId } = context;

const revision = await throwIfDataError(
context.dataFetcher.getRevision({
spaceId: space.id,
revisionId,
})
);
const { revision } = context;

return (
<ToolbarLayout>
Expand Down
20 changes: 11 additions & 9 deletions packages/gitbook/src/components/DocumentView/ReusableContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
dataFetcher,
});

if (!resolved?.reusableContent) {
if (!resolved) {
return null;
}

const reusableContent = resolved.reusableContent.revisionReusableContent;
if (!reusableContent.document) {
const { reusableContent } = resolved;
if (!reusableContent || !reusableContent.revisionReusableContent.document) {
return null;
}

const document = await getDataOrNull(
dataFetcher.getDocument({
spaceId: resolved.reusableContent.space.id,
documentId: reusableContent.document,
spaceId: reusableContent.space.id,
documentId: reusableContent.revisionReusableContent.document,
})
);

Expand All @@ -47,18 +47,20 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
// the data fetcher with the token from the block meta and the correct
// space and revision pointers.
const reusableContentContext: GitBookSpaceContext =
context.contentContext.space.id === resolved.reusableContent.space.id
context.contentContext.space.id === reusableContent.space.id
? context.contentContext
: {
...context.contentContext,
dataFetcher,
space: resolved.reusableContent.space,
revisionId: resolved.reusableContent.revision,
space: reusableContent.space,
// When the reusable content is in a different space, we don't resolve relative links to pages
// as this space might not be part of the current site.
// In the future, we might expand the logic to look up the space from the list of all spaces in the site
// and adapt the relative links to point to the correct variant.
pages: [],
revision: {
...reusableContent.revision,
pages: [], // TODO: check with Steven
},
shareKey: undefined,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/gitbook/src/components/PDF/PDFPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function PDFPage(props: {
const language = getSpaceLanguage(customization);

// Compute the pages to render
const { pages, total } = selectPages(baseContext.pages, pdfParams);
const { pages, total } = selectPages(baseContext.revision.pages, pdfParams);
const pageIds = pages.map(
({ page }) => [page.id, getPagePDFContainerId(page)] as [string, string]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ export async function PageBodyBlankslate(props: {
/>
);
}
const href = context.linker.toPathForPage({ pages: context.pages, page: child });
const href = context.linker.toPathForPage({
pages: context.revision.pages,
page: child,
});
return <Card key={child.id} title={child.title} leadingIcon={icon} href={href} />;
})
);
Expand Down
Loading