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

781 lines (772 loc) 25 kB
import { ApolloLink, InMemoryCache as InMemoryCache$1, Observable as Observable$1, gql, ApolloClient as ApolloClient$1 } from '@apollo/client'; import { Observable, print } from '@apollo/client/utilities'; import { hasDirectives, removeDirectivesFromDocument } from '@apollo/client/utilities/internal'; import { of } from 'rxjs'; import { invariant } from '@apollo/client/utilities/invariant'; import React, { createContext, useEffect, useRef, useMemo, useContext, useSyncExternalStore } from 'react'; import { equal } from '@wry/equality'; import { wrapQueryRef, unwrapQueryRef, InternalQueryReference } from '@apollo/client/react/internal'; import { useApolloClient, ApolloProvider } from '@apollo/client/react'; import { stripIgnoredCharacters } from 'graphql'; import { JSONEncodeStream, JSONDecodeStream } from '@apollo/client-react-streaming/stream-utils'; import { __DEV__ } from '@apollo/client/utilities/environment'; // src/AccumulateMultipartResponsesLink.ts var AccumulateMultipartResponsesLink = class extends ApolloLink { maxDelay; constructor(config) { super(); this.maxDelay = config.cutoffDelay; } request(operation, forward) { if (!forward) { throw new Error("This is not a terminal link!"); } const operationContainsMultipartDirectives = hasDirectives( ["defer"], operation.query ); const upstream = forward(operation); if (!operationContainsMultipartDirectives) return upstream; const maxDelay = this.maxDelay; let accumulatedData, maxDelayTimeout; const incrementalHandler = operation.client["queryManager"].incrementalHandler; let incremental; return new Observable((subscriber) => { const upstreamSubscription = upstream.subscribe({ next: (result) => { if (incrementalHandler.isIncrementalResult(result)) { incremental ||= incrementalHandler.startRequest({ query: operation.query }); accumulatedData = incremental.handle(accumulatedData.data, result); } else { accumulatedData = result; } if (!maxDelay) { flushAccumulatedData(); } else if (!maxDelayTimeout) { maxDelayTimeout = setTimeout(flushAccumulatedData, maxDelay); } }, error: (error) => { if (maxDelayTimeout) clearTimeout(maxDelayTimeout); subscriber.error(error); }, complete: () => { if (maxDelayTimeout) { clearTimeout(maxDelayTimeout); flushAccumulatedData(); } subscriber.complete(); } }); function flushAccumulatedData() { subscriber.next(accumulatedData); subscriber.complete(); upstreamSubscription.unsubscribe(); } return function cleanUp() { clearTimeout(maxDelayTimeout); upstreamSubscription.unsubscribe(); }; }); } }; function getDirectiveArgumentValue(directive, argument) { return directive.arguments?.find((arg) => arg.name.value === argument)?.value; } var RemoveMultipartDirectivesLink = class extends ApolloLink { stripDirectives = []; constructor(config) { super(); if (config.stripDefer !== false) this.stripDirectives.push("defer"); } request(operation, forward) { if (!forward) { throw new Error("This is not a terminal link!"); } const { query } = operation; let modifiedQuery = query; modifiedQuery = removeDirectivesFromDocument( this.stripDirectives.map( (directive) => ({ test(node) { let shouldStrip = node.kind === "Directive" && node.name.value === directive; const label = getDirectiveArgumentValue(node, "label"); if (label?.kind === "StringValue" && label.value.startsWith("SsrDontStrip")) { shouldStrip = false; } return shouldStrip; }, remove: true }) ).concat({ test(node) { if (node.kind !== "Directive") return false; const label = getDirectiveArgumentValue(node, "label"); return label?.kind === "StringValue" && label.value.startsWith("SsrStrip"); }, remove: true }), modifiedQuery ); if (modifiedQuery === null) { return of({}); } operation.query = modifiedQuery; return forward(operation); } }; var SSRMultipartLink = class extends ApolloLink { constructor(config = {}) { const combined = ApolloLink.from([ new RemoveMultipartDirectivesLink({ stripDefer: config.stripDefer }), new AccumulateMultipartResponsesLink({ cutoffDelay: config.cutoffDelay || 0 }) ]); super(combined.request); } }; // src/bundleInfo.ts var bundle = { pkg: "@apollo/client-react-streaming" }; var sourceSymbol = Symbol.for("apollo.source_package"); // src/DataTransportAbstraction/WrappedInMemoryCache.tsx var InMemoryCache = class extends InMemoryCache$1 { /** * Information about the current package and it's export names, for use in error messages. * * @internal */ static info = bundle; [sourceSymbol]; constructor(config) { super(config); const info = this.constructor.info; this[sourceSymbol] = `${info.pkg}:InMemoryCache`; } }; // src/DataTransportAbstraction/backpressuredCallback.ts function createBackpressuredCallback() { const queue = []; let push = queue.push.bind(queue); return { push: (value) => push(value), register: (callback) => { if (callback) { push = callback; while (queue.length) { callback(queue.shift()); } } else { push = queue.push.bind(queue); } } }; } var DataTransportContext = /* @__PURE__ */ createContext(null); var CLEAN = {}; function useTransportValue(value) { const dataTransport = useContext(DataTransportContext); if (!dataTransport) throw new Error( "useTransportValue must be used within a streaming-specific ApolloProvider" ); const valueRef = dataTransport.useStaticValueRef(value); const whichResult = useSyncExternalStore( () => () => { }, () => 0 /* client */, () => valueRef.current === CLEAN ? 0 /* client */ : equal(value, valueRef.current) ? 0 /* client */ : 1 /* server */ ); if (whichResult === 0 /* client */) { valueRef.current = CLEAN; } return whichResult === 1 /* server */ ? valueRef.current : value; } var teeToReadableStreamKey = Symbol.for( "apollo.tee.readableStreamController" ); var readFromReadableStreamKey = Symbol.for("apollo.read.readableStream"); function teeToReadableStream(onLinkHit, context) { return Object.assign(context, { [teeToReadableStreamKey]: onLinkHit }); } function readFromReadableStream(readableStream, context) { return Object.assign(context, { [readFromReadableStreamKey]: readableStream }); } var TeeToReadableStreamLink = class extends ApolloLink { constructor() { super((operation, forward) => { const context = operation.getContext(); const onLinkHit = context[teeToReadableStreamKey]; if (onLinkHit) { const controller = onLinkHit(); const tryClose = () => { try { controller.close(); } catch { } }; return new Observable$1((observer) => { let subscribed = true; forward(operation).subscribe({ next(result) { controller.enqueue({ type: "next", value: result }); if (subscribed) { observer.next(result); } }, error(error) { controller.enqueue({ type: "error" }); tryClose(); if (subscribed) { observer.error(error); } }, complete() { controller.enqueue({ type: "completed" }); tryClose(); if (subscribed) { observer.complete(); } } }); return () => { subscribed = false; }; }); } return forward(operation); }); } }; var ReadFromReadableStreamLink = class extends ApolloLink { constructor() { super((operation, forward) => { const context = operation.getContext(); const eventSteam = context[readFromReadableStreamKey]; if (eventSteam) { return new Observable$1((observer) => { let aborted = false; const reader = (() => { try { return eventSteam.getReader(); } catch { } })(); if (!reader) { const subscription = forward(operation).subscribe(observer); return () => subscription.unsubscribe(); } consume(reader).finally(() => { if (!observer.closed && true) { observer.complete(); } }); let onAbort = () => { aborted = true; reader.cancel(); }; return () => onAbort(); async function consume(reader2) { let event = undefined; while (!aborted && !event?.done) { event = await reader2.read(); if (aborted) break; if (event.value) { switch (event.value.type) { case "next": observer.next(event.value.value); break; case "completed": observer.complete(); break; case "error": { observer.error( new Error( "Error from event stream. Redacted for security concerns." ) ); } break; } } } } }); } return forward(operation); }); } }; function serializeOptions(options) { invariant( typeof options.fetchPolicy !== "function", "`nextFetchPolicy` cannot be a function in server-client streaming" ); invariant( typeof options.skipPollAttempt !== "function", "`skipPollAttempt` cannot be used with server-client streaming" ); return { ...options, query: printMinified(options.query) }; } function deserializeOptions(options) { return { ...options, // `gql` memoizes results, but based on the input string. // We parse-stringify-parse here to ensure that our minified query // has the best chance of being the referential same query as the one used in // client-side code. query: gql(print(gql(options.query))) }; } function printMinified(query) { return stripIgnoredCharacters(print(query)); } function getInjectableEventStream() { let controller; const stream = new ReadableStream({ start(c) { controller = c; } }); return [controller, stream]; } function createTransportedQueryPreloader(client, { prepareForReuse = false, notTransportedOptionOverrides = {} } = {}) { return (...[query, options]) => { options = { ...options }; delete options.returnPartialData; delete options.nextFetchPolicy; delete options.pollInterval; const [controller, stream] = getInjectableEventStream(); const transportedQueryRef = createTransportedQueryRef( query, options, crypto.randomUUID(), stream ); const watchQueryOptions = { query, ...options, notifyOnNetworkStatusChange: false, context: skipDataTransport( teeToReadableStream(() => controller, { ...options?.context, // we want to do this even if the query is already running for another reason queryDeduplication: false }) ), ...notTransportedOptionOverrides }; if (watchQueryOptions.fetchPolicy !== "no-cache" && watchQueryOptions.fetchPolicy !== "network-only" && (!prepareForReuse || watchQueryOptions.fetchPolicy !== "cache-and-network")) { watchQueryOptions.fetchPolicy = "network-only"; } if (prepareForReuse) { const internalQueryRef = getInternalQueryRef( client, { query, ...options }, watchQueryOptions ); return Object.assign(transportedQueryRef, wrapQueryRef(internalQueryRef)); } else { const subscription = client.watchQuery(watchQueryOptions).subscribe({ next() { subscription.unsubscribe(); } }); } return transportedQueryRef; }; } function createTransportedQueryRef(query, options, queryKey, stream) { return { $__apollo_queryRef: { options: sanitizeForTransport(serializeOptions({ query, ...options })), queryKey, stream: stream.pipeThrough(new JSONEncodeStream()) } }; } function reviveTransportedQueryRef(queryRef, client) { if (unwrapQueryRef(queryRef)) return; const { $__apollo_queryRef: { options, stream } } = queryRef; const hydratedOptions = deserializeOptions(options); const internalQueryRef = getInternalQueryRef(client, hydratedOptions, { ...hydratedOptions, fetchPolicy: "network-only", context: skipDataTransport( readFromReadableStream(stream.pipeThrough(new JSONDecodeStream()), { ...hydratedOptions.context, queryDeduplication: true }) ) }); Object.assign(queryRef, wrapQueryRef(internalQueryRef)); } function getInternalQueryRef(client, userOptions, initialFetchOptions) { if (__DEV__) { invariant( userOptions.nextFetchPolicy === initialFetchOptions.nextFetchPolicy, "Encountered an unexpected bug in @apollo/client-react-streaming. Please file an issue." ); } const observable = client.watchQuery(userOptions); const optionsAfterCreation = { // context might still be `undefined`, so we need to make sure the property is at least present // `undefined` won't merge in as `applyOptions` uses `compact`, so we use an empty object instead context: {}, ...observable.options }; observable.applyOptions(initialFetchOptions); const internalQueryRef = new InternalQueryReference(observable, { autoDisposeTimeoutMs: client.defaultOptions.react?.suspense?.autoDisposeTimeoutMs }); observable.applyOptions({ ...optionsAfterCreation, fetchPolicy: observable.options.fetchPolicy === initialFetchOptions.fetchPolicy ? ( // restore `userOptions.fetchPolicy` for future fetches optionsAfterCreation.fetchPolicy ) : ( // otherwise `fetchPolicy` was changed from `initialFetchOptions`, `nextFetchPolicy` has been applied and we're not going to touch it observable.options.fetchPolicy ) }); return internalQueryRef; } function isTransportedQueryRef(queryRef) { return !!(queryRef && queryRef.$__apollo_queryRef); } function useWrapTransportedQueryRef(queryRef) { const client = useApolloClient(); const isTransported = isTransportedQueryRef(queryRef); if (isTransported) { reviveTransportedQueryRef(queryRef, client); } const unwrapped = unwrapQueryRef(queryRef); useEffect(() => { if (isTransported) { return unwrapped.softRetain(); } }, [isTransported, unwrapped]); return queryRef; } function sanitizeForTransport(value) { return JSON.parse(JSON.stringify(value)); } var hookWrappers = { useFragment(orig_useFragment) { return wrap(orig_useFragment, ["data", "complete", "missing", "dataState"]); }, useQuery(orig_useQuery) { return wrap( (query, options) => orig_useQuery(query, { // this `as any` call should not be necessary, but for some reason our CI builds are failing without it (even when CI deployments and local builds are fine) ...options, fetchPolicy: "cache-only" }) , ["data", "loading", "networkStatus", "dataState"] ); }, useSuspenseQuery(orig_useSuspenseQuery) { return wrap(orig_useSuspenseQuery, ["data", "networkStatus", "dataState"]); }, useReadQuery(orig_useReadQuery) { return wrap( (queryRef) => { return orig_useReadQuery(useWrapTransportedQueryRef(queryRef)); }, ["data", "networkStatus", "dataState"] ); }, useQueryRefHandlers(orig_useQueryRefHandlers) { return wrap((queryRef) => { return orig_useQueryRefHandlers(useWrapTransportedQueryRef(queryRef)); }, []); }, useSuspenseFragment(orig_useSuspenseFragment) { return wrap(orig_useSuspenseFragment, ["data"]); } }; function wrap(useFn, transportKeys) { return (...args) => { const result = useFn(...args); if (transportKeys.length == 0) { return result; } const forTransport = useMemo(() => { const transport = {}; for (const key of transportKeys) { transport[key] = result[key]; } return transport; }, [result]); const transported = useTransportValue(forTransport); return useMemo( () => ({ ...result, ...transported }), [result, transported] ); }; } // src/assertInstance.ts function assertInstance(value, info, name) { if (value[sourceSymbol] !== `${info.pkg}:${name}`) { throw new Error( `When using \`${name}\` in streaming SSR, you must use the \`${name}\` export provided by \`"${info.pkg}"\`.` ); } } // src/DataTransportAbstraction/WrappedApolloClient.tsx function getQueryManager(client) { return client["queryManager"]; } var wrappers = Symbol.for("apollo.hook.wrappers"); var ApolloClientBase = class extends ApolloClient$1 { /** * Information about the current package and it's export names, for use in error messages. * * @internal */ static info = bundle; [sourceSymbol]; constructor(options) { const warnings = []; if ("ssrMode" in options) { delete options.ssrMode; warnings.push( "The `ssrMode` option is not supported in %s. Please remove it from your %s constructor options." ); } if ("ssrForceFetchDelay" in options) { delete options.ssrForceFetchDelay; warnings.push( "The `ssrForceFetchDelay` option is not supported in %s. Please remove it from your %s constructor options." ); } super( { devtools: { enabled: false, ...options.devtools }, ...options } ); const info = this.constructor.info; this[sourceSymbol] = `${info.pkg}:ApolloClient`; for (const warning of warnings) { console.warn(warning, info.pkg, "ApolloClient"); } assertInstance( this.cache, info, "InMemoryCache" ); this.setLink(this.link); } setLink(newLink) { super.setLink.call( this, ApolloLink.from([ new ReadFromReadableStreamLink(), new TeeToReadableStreamLink(), newLink ]) ); } }; var ApolloClientClientBaseImpl = class extends ApolloClientBase { constructor(options) { super(options); this.onQueryStarted = this.onQueryStarted.bind(this); getQueryManager(this)[wrappers] = hookWrappers; } simulatedStreamingQueries = /* @__PURE__ */ new Map(); onQueryStarted({ options, id }) { const hydratedOptions = deserializeOptions(options); const [controller, stream] = getInjectableEventStream(); const queryManager = getQueryManager(this); queryManager.fetchQuery({ ...hydratedOptions, query: queryManager.transform(hydratedOptions.query), fetchPolicy: "network-only", context: skipDataTransport( readFromReadableStream(stream, { ...hydratedOptions.context, queryDeduplication: true }) ) }); this.simulatedStreamingQueries.set(id, { controller, options: hydratedOptions }); } onQueryProgress = (event) => { const queryInfo = this.simulatedStreamingQueries.get(event.id); if (!queryInfo) return; if (event.type === "error" || event.type === "next" && event.value.errors) { this.simulatedStreamingQueries.delete(event.id); { invariant.debug( "Query failed upstream, will fail it during SSR and rerun it in the browser:", queryInfo.options ); queryInfo.controller.error(new Error("Query failed upstream.")); } } else if (event.type === "completed") { this.simulatedStreamingQueries.delete(event.id); queryInfo.controller.enqueue(event); } else if (event.type === "next") { queryInfo.controller.enqueue(event); } }; /** * Can be called when the stream closed unexpectedly while there might still be unresolved * simulated server-side queries going on. * Those queries will be cancelled and then re-run in the browser. */ rerunSimulatedQueries = () => { for (const [id, queryInfo] of this.simulatedStreamingQueries) { this.simulatedStreamingQueries.delete(id); invariant.debug( "streaming connection closed before server query could be fully transported, rerunning:", queryInfo.options ); this.rerunSimulatedQuery(queryInfo); } }; rerunSimulatedQuery = (queryInfo) => { const queryManager = getQueryManager(this); queryManager.fetchQuery({ ...queryInfo.options, fetchPolicy: "no-cache", query: queryManager.transform(queryInfo.options.query), context: skipDataTransport( teeToReadableStream(() => queryInfo.controller, { ...queryInfo.options.context, queryDeduplication: false }) ) }); }; }; var skipDataTransportKey = Symbol.for("apollo.dataTransport.skip"); function skipDataTransport(context) { return Object.assign(context, { [skipDataTransportKey]: true }); } var ApolloClientSSRImpl = class extends ApolloClientClientBaseImpl { watchQueryQueue = createBackpressuredCallback(); pushEventStream(options) { const id = crypto.randomUUID(); const [controller, eventStream] = getInjectableEventStream(); const streamObservable = new Observable$1((subscriber) => { function consume(event) { const value = event.value; if (value) { subscriber.next({ ...value, id }); } if (event.done) { subscriber.complete(); } else { reader.read().then(consume); } } const reader = eventStream.getReader(); reader.read().then(consume); }); this.watchQueryQueue.push({ event: { type: "started", options: serializeOptions(options), id }, observable: streamObservable }); return controller; } watchQuery(options) { if (!options.context?.[skipDataTransportKey]) { return super.watchQuery({ ...options, context: teeToReadableStream( () => this.pushEventStream( options ), { ...options?.context } ) }); } return super.watchQuery(options); } }; var ApolloClientImplementation = ApolloClientSSRImpl ; var ApolloClient = class extends ApolloClientImplementation { }; // src/DataTransportAbstraction/symbols.ts var ApolloClientSingleton = /* @__PURE__ */ Symbol.for( "ApolloClientSingleton" ); // src/DataTransportAbstraction/testHelpers.ts function resetApolloSingletons() { delete window[ApolloClientSingleton]; } function WrapApolloProvider(TransportProvider) { const WrappedApolloProvider3 = ({ makeClient, children, ...extraProps }) => { const clientRef = useRef(undefined); if (!clientRef.current) { { clientRef.current = makeClient(); } assertInstance( clientRef.current, WrappedApolloProvider3.info, "ApolloClient" ); } return /* @__PURE__ */ React.createElement(ApolloProvider, { client: clientRef.current }, /* @__PURE__ */ React.createElement( TransportProvider, { onQueryEvent: (event) => event.type === "started" ? clientRef.current.onQueryStarted(event) : clientRef.current.onQueryProgress(event), rerunSimulatedQueries: clientRef.current.rerunSimulatedQueries, registerDispatchRequestStarted: clientRef.current.watchQueryQueue?.register, ...extraProps }, children )); }; WrappedApolloProvider3.info = bundle; return WrappedApolloProvider3; } const built_for_ssr = true; export { ApolloClient, DataTransportContext, AccumulateMultipartResponsesLink as DebounceMultipartResponsesLink, InMemoryCache, ReadFromReadableStreamLink, RemoveMultipartDirectivesLink, SSRMultipartLink, TeeToReadableStreamLink, WrapApolloProvider, built_for_ssr, createTransportedQueryPreloader, isTransportedQueryRef, readFromReadableStream, resetApolloSingletons, reviveTransportedQueryRef, skipDataTransport, teeToReadableStream, useWrapTransportedQueryRef }; //# sourceMappingURL=out.js.map //# sourceMappingURL=index.ssr.js.map