UNPKG

@apollo/client-react-streaming

Version:

This package provides building blocks to create framework-level integration of Apollo Client with React's streaming SSR. See the [@apollo/client-integration-nextjs](https://github.com/apollographql/apollo-client-integrations/tree/main/packages/nextjs) pac

1 lines 19.9 kB
{"version":3,"sources":["../src/ManualDataTransport/ManualDataTransport.tsx","../src/ManualDataTransport/RehydrationContext.tsx","../src/ManualDataTransport/ApolloRehydrateSymbols.tsx","../src/ManualDataTransport/lateInitializingQueue.ts","../src/ManualDataTransport/dataTransport.ts","../src/ManualDataTransport/serialization.ts","../src/ManualDataTransport/index.ts"],"names":["React","revive","invariant","useStaticValueRef"],"mappings":";AAAA,OAAOA,UAAS,aAAa,WAAW,OAAO,SAAS,cAAc;AAEtE,SAAS,4BAA4B;;;ACFrC,OAAO,WAAW;;;ACSX,IAAM,yBAAuC,uBAAO;AAAA,EACzD;AACF;AAEO,IAAM,6BAA2C,uBAAO;AAAA,EAC7D;AACF;;;ACCO,SAAS,8BACd,KACA,UACA;AACA,QAAM,eAAe,OAAO,GAAG,KAAK,CAAC;AACrC,MAAI,MAAM,QAAQ,YAAY,GAAG;AAC/B,WAAO,GAAG,IAAI;AAAA,MACZ,MAAM,IAAI,SAAgB;AACxB,mBAAW,SAAS,MAAM;AACxB,mBAAS,KAAK;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AACA,WAAO,GAAG,EAAE,KAAK,GAAG,YAAY;AAAA,EAClC;AACF;;;AC5BA,SAAS,iBAAiB;AA0BnB,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA,QAAAC;AACF,GAIG;AACD,gCAA8B,wBAAwB,CAAC,SAAS;AAC9D,UAAM,SAASA,QAAO,IAAI;AAC1B,cAAU,MAAM,kCAAkC,MAAM;AACxD,gBAAY,OAAO,SAAS;AAC5B,eAAW,UAAU,OAAO,QAAQ;AAClC,mBAAa,MAAM;AAAA,IACrB;AAAA,EACF,CAAC;AACH;;;AH3CA,SAAS,aAAAC,kBAAiB;;;AIYnB,SAAS,OAAO,OAAiB;AACtC,SAAO;AACT;;;ALiFA,IAAM,sCAAsC,CAAC;AAAA,EAC3C,mBAAmB;AACrB,MACE,SAAS,+BAA+B;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AACF,GAAG;AACD,QAAM,uBAAuB;AAAA,IAC1B,OAAO,0BAA0B,MAAM,CAAC;AAAA,EAC3C;AACA,wBAAsB;AAAA,IACpB;AAAA,IACA,YAAY,WAAW;AACrB,aAAO,OAAO,qBAAqB,SAAS,SAAS;AAAA,IACvD;AAAA,IACA,QAAQ;AAAA,EACV,CAAC;AAED,YAAU,MAAM;AACd,QAAI,SAAS,eAAe,YAAY;AAGtC,aAAO,iBAAiB,QAAQ,uBAAwB;AAAA,QACtD,MAAM;AAAA,MACR,CAAC;AACD,aAAO,MAAM,OAAO,oBAAoB,QAAQ,qBAAsB;AAAA,IACxE,OAAO;AACL,4BAAuB;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,qBAAqB,CAAC;AAE1B,QAAM,oBAAoB,YAAY,SAASC,mBAAqB,GAAM;AACxE,UAAM,KAAK,MAAM;AACjB,UAAM,QAAQ,qBAAqB;AACnC,UAAM,UAAU,OAAO,aAAkB;AACzC,QAAI,QAAQ,YAAY,eAAe;AACrC,UAAI,SAAS,MAAM,OAAO;AACxB,gBAAQ,UAAU,MAAM,EAAE;AAC1B,eAAO,MAAM,EAAE;AAAA,MACjB,OAAO;AACL,gBAAQ,UAAU;AAAA,MACpB;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,SACE,gBAAAH,OAAA;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC,OAAO;AAAA,QACL,OAAO;AAAA,UACL;AAAA,QACF;AAAA,QACA,CAAC,iBAAiB;AAAA,MACpB;AAAA;AAAA,IAEC;AAAA,EACH;AAEJ;AAEF,IAAM,gBAAgB,CAAC;AAkDhB,IAAM,2BAGX,QACI,kCACA;;;AM9MN,SAAS,6BAA6B;AAc/B,SAAS,iCAAiC;AAC/C,wBAAsB;AACtB,SAAO,OAAO,0BAA0B;AACxC,SAAO,OAAO,sBAAsB;AACtC","sourcesContent":["import React, { useCallback, useEffect, useId, useMemo, useRef } from \"react\";\nimport type { DataTransportProviderImplementation } from \"@apollo/client-react-streaming\";\nimport { DataTransportContext } from \"@apollo/client-react-streaming\";\nimport type { RehydrationCache, RehydrationContextValue } from \"./types.js\";\nimport type { HydrationContextOptions } from \"./RehydrationContext.js\";\nimport { buildApolloRehydrationContext } from \"./RehydrationContext.js\";\nimport { registerDataTransport } from \"./dataTransport.js\";\nimport { revive, stringify } from \"./serialization.js\";\nimport { ApolloHookRehydrationCache } from \"./ApolloRehydrateSymbols.js\";\n\ninterface ManualDataTransportOptions {\n /**\n * A hook that allows for insertion into the stream.\n * Will only be called during SSR, doesn't need to actiually return something otherwise.\n */\n useInsertHtml(): (callbacks: () => React.ReactNode) => void;\n /**\n * Prepare data for injecting into the stream by converting it into a string that can be parsed as JavaScript by the browser.\n * Could e.g. be `SuperJSON.stringify` or `serialize-javascript`.\n * The default implementation act like a JSON.stringify that preserves `undefined`, but not do much on top of that.\n */\n stringifyForStream?: (value: any) => string;\n /**\n * If necessary, additional deserialization steps that need to be applied on top of executing the result of `stringifyForStream` in the browser.\n * Could e.g. be `SuperJSON.deserialize`. (Not needed in the case of using `serialize-javascript`)\n */\n reviveFromStream?: (value: any) => any;\n /**\n * **Read the whole comment before using this option!**\n *\n * If `true`, the `useStaticValueRef` hook will not transport values over to the client.\n * This hook is used to transport the values of hook calls during SSR to the client, to ensure that\n * the client will rehydrate with the exact same values as it rendered on the server.\n *\n * This mechanism is in place to prevent hydration mismatches as described in\n * https://github.com/apollographql/apollo-client-integrations/blob/pr/RFC-2/RFC.md#challenges-of-a-client-side-cache-in-streaming-ssr\n * (first graph of the \"Challenges of a client-side cache in streaming SSR\" section).\n *\n * Setting this value to `true` will save on data transported over the wire, but comes with the risk\n * of hydration mismatches.\n * Strongly discouraged with older React versions, as hydration mismatches there will likely crash\n * the application, setting this to `true` might be okay with React 19, which is much better at recovering\n * from hydration mismatches - but it still comes with a risk.\n * When enabling this, you should closely monitor error reporting and user feedback.\n */\n dangerous_disableHookValueTransportation?: boolean;\n}\n\nconst buildManualDataTransportSSRImpl = ({\n useInsertHtml,\n stringifyForStream = stringify,\n dangerous_disableHookValueTransportation: disableHookValueTransportation,\n}: ManualDataTransportOptions): DataTransportProviderImplementation<HydrationContextOptions> =>\n function ManualDataTransportSSRImpl({\n extraScriptProps,\n children,\n registerDispatchRequestStarted,\n }) {\n const insertHtml = useInsertHtml();\n\n const rehydrationContext = useRef<RehydrationContextValue>(undefined);\n if (!rehydrationContext.current) {\n rehydrationContext.current = buildApolloRehydrationContext({\n insertHtml,\n extraScriptProps,\n stringify: stringifyForStream,\n });\n }\n\n registerDispatchRequestStarted!(({ event, observable }) => {\n rehydrationContext.current!.incomingEvents.push(event);\n observable.subscribe({\n next(event) {\n rehydrationContext.current!.incomingEvents.push(event);\n },\n });\n });\n\n const contextValue = useMemo(\n () => ({\n useStaticValueRef: function useStaticValueRef<T>(value: T) {\n const id = useId();\n if (!disableHookValueTransportation) {\n rehydrationContext.current!.transportValueData[id] = value;\n }\n return { current: value };\n },\n }),\n []\n );\n\n return (\n <DataTransportContext.Provider value={contextValue}>\n {children}\n </DataTransportContext.Provider>\n );\n };\n\nconst buildManualDataTransportBrowserImpl = ({\n reviveFromStream = revive,\n}: ManualDataTransportOptions): DataTransportProviderImplementation<HydrationContextOptions> =>\n function ManualDataTransportBrowserImpl({\n children,\n onQueryEvent,\n rerunSimulatedQueries,\n }) {\n const hookRehydrationCache = useRef<RehydrationCache>(\n (window[ApolloHookRehydrationCache] ??= {})\n );\n registerDataTransport({\n onQueryEvent: onQueryEvent!,\n onRehydrate(rehydrate) {\n Object.assign(hookRehydrationCache.current, rehydrate);\n },\n revive: reviveFromStream,\n });\n\n useEffect(() => {\n if (document.readyState !== \"complete\") {\n // happens simulatenously to `readyState` changing to `\"complete\"`, see\n // https://html.spec.whatwg.org/multipage/parsing.html#the-end (step 9.1 and 9.5)\n window.addEventListener(\"load\", rerunSimulatedQueries!, {\n once: true,\n });\n return () => window.removeEventListener(\"load\", rerunSimulatedQueries!);\n } else {\n rerunSimulatedQueries!();\n }\n }, [rerunSimulatedQueries]);\n\n const useStaticValueRef = useCallback(function useStaticValueRef<T>(v: T) {\n const id = useId();\n const store = hookRehydrationCache.current;\n const dataRef = useRef(UNINITIALIZED as T);\n if (dataRef.current === UNINITIALIZED) {\n if (store && id in store) {\n dataRef.current = store[id] as T;\n delete store[id];\n } else {\n dataRef.current = v;\n }\n }\n return dataRef;\n }, []);\n\n return (\n <DataTransportContext.Provider\n value={useMemo(\n () => ({\n useStaticValueRef,\n }),\n [useStaticValueRef]\n )}\n >\n {children}\n </DataTransportContext.Provider>\n );\n };\n\nconst UNINITIALIZED = {};\n\n/**\n * > This export is only available in React Client Components\n *\n * Creates a \"manual\" Data Transport, to be used with `WrapApolloProvider`.\n *\n * @remarks\n *\n * ### Drawbacks\n *\n * While this Data Transport enables streaming SSR, it has some conceptual drawbacks:\n *\n * - It does not have a way of keeping your connection open if your application already finished, but there are still ongoing queries that might need to be transported over.\n * - This can happen if a component renders `useBackgroundQuery`, but does not read the `queryRef` with `useReadQuery`\n * - These \"cut off\" queries will be restarted in the browser once the browser's `load` event happens\n * - If the `useInsertHtml` doesn't immediately flush data to the browser, the browser might already attain \"newer\" data through queries triggered by user interaction.\n * - This delayed behaviour is the case with the Next.js `ServerInsertedHTMLContext` and in the example Vite implementation.\n * - In this, case, older data from the server might overwrite newer data in the browser. This is minimized by simulating ongoing queries in the browser once the information of a started query is transported over.\n * If the browser would try to trigger the exact same query, query deduplication would make the browser wait for the server query to resolve instead.\n * - For more timing-related details, see https://github.com/apollographql/apollo-client-integrations/pull/9\n *\n * To fully work around these drawbacks, React needs to add \"data injection into the stream\" to it's public API, which is not the case today.\n * We provide an [example with a patched React version](https://github.com/apollographql/apollo-client-integrations/blob/main/integration-test/experimental-react) to showcase how that could look.\n *\n * @example\n * For usage examples, see the implementation of the `@apollo/client-integration-nextjs`\n * [`ApolloNextAppProvider`](https://github.com/apollographql/apollo-client-integrations/blob/c0715a05cf8ca29a3cbb9ce294cdcbc5ce251b2e/packages/experimental-nextjs-app-support/src/ApolloNextAppProvider.ts)\n *\n * ```tsx\n * export const ApolloNextAppProvider = WrapApolloProvider(\n * buildManualDataTransport({\n * useInsertHtml() {\n * const insertHtml = useContext(ServerInsertedHTMLContext);\n * if (!insertHtml) {\n * throw new Error(\n * \"ApolloNextAppProvider cannot be used outside of the Next App Router!\"\n * );\n * }\n * return insertHtml;\n * },\n * })\n * );\n * ```\n *\n * @example\n * Another usage example is our integration example with Vite and streaming SSR, which you can find at https://github.com/apollographql/apollo-client-integrations/tree/main/integration-test/vite-streaming\n *\n * @public\n */\nexport const buildManualDataTransport: (\n args: ManualDataTransportOptions\n) => DataTransportProviderImplementation<HydrationContextOptions> =\n process.env.REACT_ENV === \"ssr\"\n ? buildManualDataTransportSSRImpl\n : buildManualDataTransportBrowserImpl;\n","import React from \"react\";\nimport type { RehydrationContextValue } from \"./types.js\";\nimport { transportDataToJS } from \"./dataTransport.js\";\nimport { invariant } from \"ts-invariant\";\nimport type { Stringify } from \"./serialization.js\";\n\n/**\n * @public\n */\nexport interface HydrationContextOptions {\n /**\n * Props that will be passed down to `script` tags that will be used to transport\n * data to the browser.\n * Can e.g. be used to add a `nonce`.\n */\n extraScriptProps?: ScriptProps;\n}\n\ntype SerializableProps<T> = Pick<\n T,\n {\n [K in keyof T]: T[K] extends string | number | boolean | undefined | null\n ? K\n : never;\n }[keyof T]\n>;\n\ntype ScriptProps = SerializableProps<\n React.ScriptHTMLAttributes<HTMLScriptElement>\n>;\n\nexport function buildApolloRehydrationContext({\n insertHtml,\n stringify,\n extraScriptProps,\n}: HydrationContextOptions & {\n insertHtml: (callbacks: () => React.ReactNode) => void;\n stringify: Stringify;\n}): RehydrationContextValue {\n function ensureInserted() {\n if (!rehydrationContext.currentlyInjected) {\n rehydrationContext.currentlyInjected = true;\n insertHtml(() => <rehydrationContext.RehydrateOnClient />);\n }\n }\n\n const rehydrationContext: RehydrationContextValue = {\n currentlyInjected: false,\n transportValueData: getTransportObject(ensureInserted),\n transportedValues: {},\n incomingEvents: getTransportArray(ensureInserted),\n RehydrateOnClient() {\n rehydrationContext.currentlyInjected = false;\n if (\n !Object.keys(rehydrationContext.transportValueData).length &&\n !Object.keys(rehydrationContext.incomingEvents).length\n )\n return <></>;\n invariant.debug(\n \"transporting data\",\n rehydrationContext.transportValueData\n );\n invariant.debug(\"transporting events\", rehydrationContext.incomingEvents);\n\n const __html = transportDataToJS(\n {\n rehydrate: Object.fromEntries(\n Object.entries(rehydrationContext.transportValueData).filter(\n ([key, value]) =>\n rehydrationContext.transportedValues[key] !== value\n )\n ),\n events: rehydrationContext.incomingEvents,\n },\n stringify\n );\n Object.assign(\n rehydrationContext.transportedValues,\n rehydrationContext.transportValueData\n );\n rehydrationContext.transportValueData =\n getTransportObject(ensureInserted);\n rehydrationContext.incomingEvents = getTransportArray(ensureInserted);\n return (\n <script\n {...extraScriptProps}\n dangerouslySetInnerHTML={{\n __html,\n }}\n />\n );\n },\n };\n return rehydrationContext;\n}\n\nfunction getTransportObject(ensureInserted: () => void) {\n return new Proxy(\n {},\n {\n set(...args) {\n ensureInserted();\n return Reflect.set(...args);\n },\n }\n );\n}\nfunction getTransportArray(ensureInserted: () => void) {\n return new Proxy<any[]>([], {\n get(...args) {\n if (args[1] === \"push\") {\n return (...values: any[]) => {\n ensureInserted();\n return args[0].push(...values);\n };\n }\n return Reflect.get(...args);\n },\n });\n}\n","import type { DataTransport } from \"./dataTransport.js\";\nimport type { RehydrationCache } from \"./types.js\";\n\ndeclare global {\n interface Window {\n [ApolloSSRDataTransport]?: DataTransport<unknown>;\n [ApolloHookRehydrationCache]?: RehydrationCache;\n }\n}\nexport const ApolloSSRDataTransport = /*#__PURE__*/ Symbol.for(\n \"ApolloSSRDataTransport\"\n);\n\nexport const ApolloHookRehydrationCache = /*#__PURE__*/ Symbol.for(\n \"apollo.hookRehydrationCache\"\n);\n","type ValidQueueKeys = {\n [K in keyof Window]-?: NonNullable<Window[K]> extends {\n push(...args: any[]): any;\n }\n ? K\n : never;\n}[keyof Window];\n\n/**\n * Registers a queue that can be filled with data before it has actually been initialized with this function.\n * Before calling this function, `window[key]` can just be handled as an array of data.\n * When calling this funcation, all accumulated data will be passed to the callback.\n * After calling this function, `window[key]` will be an object with a `push` method that will call the callback with the data.\n *\n * @public\n */\nexport function registerLateInitializingQueue<K extends ValidQueueKeys>(\n key: K,\n callback: (data: Parameters<NonNullable<Window[K]>[\"push\"]>[0]) => void\n) {\n const previousData = window[key] || [];\n if (Array.isArray(previousData)) {\n window[key] = {\n push: (...data: any[]) => {\n for (const value of data) {\n callback(value);\n }\n },\n };\n window[key].push(...previousData);\n }\n}\n","import { ApolloSSRDataTransport } from \"./ApolloRehydrateSymbols.js\";\nimport type { RehydrationCache } from \"./types.js\";\nimport { registerLateInitializingQueue } from \"./lateInitializingQueue.js\";\nimport { invariant } from \"ts-invariant\";\nimport { htmlEscapeJsonString } from \"./htmlescape.js\";\nimport type { QueryEvent } from \"@apollo/client-react-streaming\";\nimport type { Revive, Stringify } from \"./serialization.js\";\n\nexport type DataTransport<T> = Array<T> | { push(...args: T[]): void };\n\ntype DataToTransport = {\n rehydrate: RehydrationCache;\n events: QueryEvent[];\n};\n\n/**\n * Returns a string of JavaScript that can be used to transport data to the client.\n */\nexport function transportDataToJS(data: DataToTransport, stringify: Stringify) {\n const key = Symbol.keyFor(ApolloSSRDataTransport);\n return `(window[Symbol.for(\"${key}\")] ??= []).push(${htmlEscapeJsonString(\n stringify(data)\n )})`;\n}\n\n/**\n * Registers a lazy queue that will be filled with data by `transportDataToJS`.\n * All incoming data will be added either to the rehydration cache or the result cache.\n */\nexport function registerDataTransport({\n onQueryEvent,\n onRehydrate,\n revive,\n}: {\n onQueryEvent(event: QueryEvent): void;\n onRehydrate(rehydrate: RehydrationCache): void;\n revive: Revive;\n}) {\n registerLateInitializingQueue(ApolloSSRDataTransport, (data) => {\n const parsed = revive(data) as DataToTransport;\n invariant.debug(`received data from the server:`, parsed);\n onRehydrate(parsed.rehydrate);\n for (const result of parsed.events) {\n onQueryEvent(result);\n }\n });\n}\n","/**\n * Stringifies a value to be injected into JavaScript \"text\" - preserves `undefined` values.\n */\nexport function stringify(value: any) {\n let undefinedPlaceholder = \"$apollo.undefined$\";\n\n const stringified = JSON.stringify(value);\n while (stringified.includes(JSON.stringify(undefinedPlaceholder))) {\n undefinedPlaceholder = \"$\" + undefinedPlaceholder;\n }\n return JSON.stringify(value, (_, v) =>\n v === undefined ? undefinedPlaceholder : v\n ).replaceAll(JSON.stringify(undefinedPlaceholder), \"undefined\");\n}\n\nexport function revive(value: any): any {\n return value;\n}\n\nexport type Stringify = typeof stringify;\nexport type Revive = typeof revive;\n","export { buildManualDataTransport } from \"./ManualDataTransport.js\";\nexport { registerLateInitializingQueue } from \"./lateInitializingQueue.js\";\nexport type { HydrationContextOptions } from \"./RehydrationContext.js\";\n\nimport {\n ApolloHookRehydrationCache,\n ApolloSSRDataTransport,\n} from \"./ApolloRehydrateSymbols.js\";\nimport { resetApolloSingletons } from \"@apollo/client-react-streaming\";\n\n/**\n * > This export is only available in React Client Components\n *\n * Resets the singleton instances created for the Apollo SSR data transport and caches.\n *\n * To be used in testing only, like\n * ```ts\n * afterEach(resetManualSSRApolloSingletons);\n * ```\n *\n * @public\n */\nexport function resetManualSSRApolloSingletons() {\n resetApolloSingletons();\n delete window[ApolloHookRehydrationCache];\n delete window[ApolloSSRDataTransport];\n}\n"]}