UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

156 lines (155 loc) 6.98 kB
import { print } from "graphql"; import * as React from "react"; import { filter, firstValueFrom } from "rxjs"; import { getApolloContext } from "@apollo/client/react"; import { wrapperSymbol } from "@apollo/client/react/internal"; import { canonicalStringify } from "@apollo/client/utilities"; import { invariant } from "@apollo/client/utilities/invariant"; import { useSSRQuery } from "./useSSRQuery.js"; function getObservableQueryKey(query, variables = {}) { const queryKey = print(query); const variablesKey = canonicalStringify(variables); return `${queryKey}|${variablesKey}`; } const noopObserver = { complete() { } }; /** * This function will rerender your React tree until no more network requests need * to be made. * If you only use suspenseful hooks (and a suspense-ready `renderFunction`), this * means that the tree will be rendered once. * If you use non-suspenseful hooks like `useQuery`, this function will render all * components, wait for all requests started by your rendered * hooks to finish, and then render the tree again, until no more requests are made. * * After executing this function, you can use `client.extract()` to get a full set * of the data that was fetched during these renders. * You can then transport that data and hydrate your cache via `client.restore(extractedData)` * before hydrating your React tree in the browser. */ export function prerenderStatic( { tree, context = {}, // The rendering function is configurable! We use renderToStaticMarkup as // the default, because it's a little less expensive than renderToString, // and legacy usage of getDataFromTree ignores the return value anyway. renderFunction, signal, ignoreResults, diagnostics, maxRerenders = 50, } ) { const availableObservableQueries = new Map(); const subscriptions = new Set(); let recentlyCreatedObservableQueries = new Set(); let renderCount = 0; const internalContext = { getObservableQuery(query, variables) { return availableObservableQueries.get(getObservableQueryKey(query, variables)); }, onCreatedObservableQuery: (observable, query, variables) => { availableObservableQueries.set(getObservableQueryKey(query, variables), observable); // we keep the observable subscribed to until we are done with rendering // otherwise it will be torn down after every render pass subscriptions.add(observable.subscribe(noopObserver)); if (observable.options.fetchPolicy !== "cache-only") { recentlyCreatedObservableQueries.add(observable); } }, }; async function process() { renderCount++; invariant(renderCount <= maxRerenders, 25, maxRerenders); invariant(!signal?.aborted, 26); // Always re-render from the rootElement, even though it might seem // better to render the children of the component responsible for the // promise, because it is not possible to reconstruct the full context // of the original rendering (including all unknown context provider // elements) for a subtree of the original component tree. const ApolloContext = getApolloContext(); const element = (React.createElement(ApolloContext.Provider, { value: { ...context, [wrapperSymbol]: { useQuery: () => useSSRQuery.bind(internalContext), }, } }, tree)); const result = await consume(await renderFunction(element)); if (recentlyCreatedObservableQueries.size == 0) { return { result, aborted: false }; } if (signal?.aborted) { return { result, aborted: true }; } const dataPromise = Promise.all(Array.from(recentlyCreatedObservableQueries).map(async (observable) => { await firstValueFrom(observable.pipe(filter((result) => result.loading === false))); recentlyCreatedObservableQueries.delete(observable); })); let resolveAbortPromise; const abortPromise = new Promise((resolve) => { resolveAbortPromise = resolve; }); signal?.addEventListener("abort", resolveAbortPromise); await Promise.race([abortPromise, dataPromise]); signal?.removeEventListener("abort", resolveAbortPromise); if (signal?.aborted) { return { result, aborted: true }; } return process(); } return Promise.resolve() .then(process) .then((result) => diagnostics ? { ...result, diagnostics: { renderCount, }, } : result) .finally(() => { availableObservableQueries.clear(); recentlyCreatedObservableQueries.clear(); subscriptions.forEach((subscription) => subscription.unsubscribe()); subscriptions.clear(); }); async function consume(value) { if (typeof value === "string") { return ignoreResults ? "" : value; } if (!value.prelude) { throw new Error("`getMarkupFromTree` was called with an incompatible render method.\n" + 'It is compatible with `renderToStaticMarkup` and `renderToString` from `"react-dom/server"`\n' + 'as well as `prerender` and `prerenderToNodeStream` from "react-dom/static"'); } const prelude = value.prelude; let result = ""; if ("getReader" in prelude) { /** * The "web" `ReadableStream` consuming path. * This could also be done with the `AsyncIterable` branch, but we add this * code for two reasons: * * 1. potential performance benefits if we don't need to create an `AsyncIterator` on top * 2. some browsers (looking at Safari) don't support `AsyncIterable` for `ReadableStream` yet * and we're not 100% sure how good this is covered on edge runtimes * * The extra code here doesn't really matter, since _usually_ this would not * be run in a browser, so we don't have to shave every single byte. */ const reader = prelude.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } if (!ignoreResults) { result += Buffer.from(value).toString("utf8"); } } } else { for await (const chunk of prelude) { if (!ignoreResults) { result += typeof chunk === "string" ? chunk : (Buffer.from(chunk).toString("utf8")); } } } return result; } } //# sourceMappingURL=prerenderStatic.js.map