diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 52ba17c1a3..88c95c4016 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -113,6 +113,15 @@ export function createDataFetcher( }) ); }, + getRevisionPageDocument(params) { + return trace('getRevisionPageDocument', () => + getRevisionPageDocument(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + pageId: params.pageId, + }) + ); + }, getReusableContent(params) { return trace('getReusableContent', () => getReusableContent(input, { @@ -417,6 +426,42 @@ const getRevisionPageMarkdown = withCacheKey( ) ); +const getRevisionPageDocument = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; pageId: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageDocumentInRevisionById( + params.spaceId, + params.revisionId, + params.pageId, + { + evaluated: true, + }, + { + ...noCacheFetchOptions, + } + ); + + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + + return res.data; + }); + } + ); + } + ) +); + const getRevisionPageByPath = withCacheKey( withoutConcurrentExecution( async ( diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts index 2e37e2fbb4..93266d124e 100644 --- a/packages/gitbook-v2/src/lib/data/index.ts +++ b/packages/gitbook-v2/src/lib/data/index.ts @@ -1,7 +1,7 @@ export * from './api'; export * from './types'; -export * from './pages'; export * from './urls'; export * from './errors'; export * from './lookup'; export * from './visitor'; +export * from './pages'; diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts index 4324254d9b..642b486b82 100644 --- a/packages/gitbook-v2/src/lib/data/pages.ts +++ b/packages/gitbook-v2/src/lib/data/pages.ts @@ -1,15 +1,30 @@ -import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api'; +import { waitUntil } from '@/lib/waitUntil'; +import type { JSONDocument, RevisionPageDocument } from '@gitbook/api'; +import type { GitBookSiteContext, GitBookSpaceContext } from '../context'; import { getDataOrNull } from './errors'; -import type { GitBookDataFetcher } from './types'; /** * Get the document for a page. */ export async function getPageDocument( - dataFetcher: GitBookDataFetcher, - space: Space, + context: GitBookSpaceContext | GitBookSiteContext, page: RevisionPageDocument ): Promise { + const { dataFetcher, space } = context; + + if ( + 'site' in context && + (context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj') + ) { + return getDataOrNull( + dataFetcher.getRevisionPageDocument({ + spaceId: space.id, + revisionId: space.revision, + pageId: page.id, + }) + ); + } + if (page.documentId) { return getDataOrNull( dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId }) @@ -26,5 +41,30 @@ export async function getPageDocument( ); } + // Pre-fetch the document to start filling the cache before we migrate to this API. + if (isInPercentRollout(space.id, 10)) { + await waitUntil( + getDataOrNull( + dataFetcher.getRevisionPageDocument({ + spaceId: space.id, + revisionId: space.revision, + pageId: page.id, + }) + ) + ); + } + return null; } + +function isInPercentRollout(value: string, rollout: number) { + return getRandomPercent(value) < rollout; +} + +function getRandomPercent(value: string) { + const hash = value.split('').reduce((acc, char) => { + return acc + char.charCodeAt(0); + }, 0); + + return hash % 100; +} diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index 178a0ba77d..8d987047e2 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -106,6 +106,15 @@ export interface GitBookDataFetcher { pageId: string; }): Promise>; + /** + * Get the document of a page by its path. + */ + getRevisionPageDocument(params: { + spaceId: string; + revisionId: string; + pageId: string; + }): Promise>; + /** * Get a document by its space ID and document ID. */ diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 666cd114d0..379f975a1e 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc --noEmit", "e2e": "playwright test e2e/internal.spec.ts", "e2e-customers": "playwright test e2e/customers.spec.ts", - "unit": "bun test {src,packages}", + "unit": "bun test {src,packages} --preload ./tests/preload-bun.ts", "generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math", "copy:icons": "gitbook-icons ./public/~gitbook/static/icons", "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx index 96cb784ccd..f70b340b41 100644 --- a/packages/gitbook/src/components/PDF/PDFPage.tsx +++ b/packages/gitbook/src/components/PDF/PDFPage.tsx @@ -9,7 +9,6 @@ import { } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context'; -import { getPageDocument } from '@v2/lib/data'; import type { GitBookLinker } from '@v2/lib/links'; import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; @@ -29,6 +28,7 @@ import { PageControlButtons } from './PageControlButtons'; import { PrintButton } from './PrintButton'; import './pdf.css'; import { sanitizeGitBookAppURL } from '@/lib/app'; +import { getPageDocument } from '@v2/lib/data'; const DEFAULT_LIMIT = 100; @@ -224,8 +224,7 @@ async function PDFPageDocument(props: { context: GitBookSpaceContext; }) { const { page, context } = props; - const { space } = context; - const document = await getPageDocument(context.dataFetcher, space, page); + const document = await getPageDocument(context, page); return ( diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx index fe6934a529..9e7c08515f 100644 --- a/packages/gitbook/src/components/SitePage/SitePage.tsx +++ b/packages/gitbook/src/components/SitePage/SitePage.tsx @@ -62,7 +62,7 @@ export async function SitePage(props: SitePageProps) { const withSections = Boolean(sections && sections.list.length > 0); const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader }; - const document = await getPageDocument(context.dataFetcher, context.space, page); + const document = await getPageDocument(context, page); return ( diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index 310074450d..cb612664b0 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -493,6 +493,39 @@ export const getRevisionPageByPath = cache({ }, }); +/** + * Get a document from a page by its ID + */ +export const getRevisionPageDocument = cache({ + name: 'api.getRevisionPageDocument.v1', + tag: (spaceId, revisionId) => + getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), + tagImmutable: true, + getKeySuffix: getAPIContextId, + get: async ( + spaceId: string, + revisionId: string, + pageId: string, + options: CacheFunctionOptions + ) => { + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getPageDocumentInRevisionById( + spaceId, + revisionId, + pageId, + { + evaluated: true, + }, + { + ...noCacheFetchOptions, + signal: options.signal, + } + ); + + return cacheResponse(response, cacheTtl_7days); + }, +}); + /** * Resolve a file by its ID. * It should not be used directly, use `getRevisionFile` instead. diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 8cfef861d0..7362decbe6 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -155,7 +155,7 @@ export async function resolveContentRef( }); if (resolveAnchorText) { - const document = await getPageDocument(dataFetcher, space, page); + const document = await getPageDocument(context, page); if (document) { const block = getBlockById(document, anchor); if (block) { diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts index eb2accdc24..db6118a84c 100644 --- a/packages/gitbook/src/lib/v1.ts +++ b/packages/gitbook/src/lib/v1.ts @@ -25,6 +25,7 @@ import { getRevision, getRevisionFile, getRevisionPageByPath, + getRevisionPageDocument, getRevisionPages, getSiteRedirectBySource, getSpace, @@ -241,6 +242,18 @@ function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher { ); }, + getRevisionPageDocument(params) { + return withAPI(() => + wrapDataFetcherError(async () => { + return getRevisionPageDocument( + params.spaceId, + params.revisionId, + params.pageId + ); + }) + ); + }, + getRevisionPageByPath(params) { return withAPI(() => wrapDataFetcherError(async () => { diff --git a/packages/gitbook/src/lib/waitUntil.ts b/packages/gitbook/src/lib/waitUntil.ts index 6e2940f17b..e81e662e2d 100644 --- a/packages/gitbook/src/lib/waitUntil.ts +++ b/packages/gitbook/src/lib/waitUntil.ts @@ -1,4 +1,6 @@ import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types'; +import { getCloudflareContext as getCloudflareContextV2 } from '@v2/lib/data/cloudflare'; +import { isV2 } from './v2'; let pendings: Array> = []; @@ -47,6 +49,14 @@ export async function waitUntil(promise: Promise) { return; } + if (isV2()) { + const context = getCloudflareContextV2(); + if (context) { + context.ctx.waitUntil(promise); + return; + } + } + const cloudflareContext = await getGlobalContext(); if ('waitUntil' in cloudflareContext) { cloudflareContext.waitUntil(promise); diff --git a/packages/gitbook/tests/preload-bun.ts b/packages/gitbook/tests/preload-bun.ts new file mode 100644 index 0000000000..3fc846ae26 --- /dev/null +++ b/packages/gitbook/tests/preload-bun.ts @@ -0,0 +1,8 @@ +import { mock } from 'bun:test'; + +/** + * Mock the `server-only` module to avoid errors when running tests as it doesn't work well in Bun + */ +mock.module('server-only', () => { + return {}; +});