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

803 lines (791 loc) 25.7 kB
'use strict'; var client = require('@apollo/client'); var utilities = require('@apollo/client/utilities'); var internal = require('@apollo/client/utilities/internal'); var rxjs = require('rxjs'); var invariant = require('@apollo/client/utilities/invariant'); var React = require('react'); var equality = require('@wry/equality'); var internal$1 = require('@apollo/client/react/internal'); var react = require('@apollo/client/react'); var graphql = require('graphql'); var streamUtils = require('@apollo/client-react-streaming/stream-utils'); var environment = require('@apollo/client/utilities/environment'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var React__default = /*#__PURE__*/_interopDefault(React); // src/AccumulateMultipartResponsesLink.ts var AccumulateMultipartResponsesLink = class extends client.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 = internal.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 utilities.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 client.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 = internal.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 rxjs.of({}); } operation.query = modifiedQuery; return forward(operation); } }; var SSRMultipartLink = class extends client.ApolloLink { constructor(config = {}) { const combined = client.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 client.InMemoryCache { /** * 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__ */ React.createContext(null); var CLEAN = {}; function useTransportValue(value) { const dataTransport = React.useContext(DataTransportContext); if (!dataTransport) throw new Error( "useTransportValue must be used within a streaming-specific ApolloProvider" ); const valueRef = dataTransport.useStaticValueRef(value); const whichResult = React.useSyncExternalStore( () => () => { }, () => 0 /* client */, () => valueRef.current === CLEAN ? 0 /* client */ : equality.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 client.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 client.Observable((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 client.ApolloLink { constructor() { super((operation, forward) => { const context = operation.getContext(); const eventSteam = context[readFromReadableStreamKey]; if (eventSteam) { return new client.Observable((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.invariant( typeof options.fetchPolicy !== "function", "`nextFetchPolicy` cannot be a function in server-client streaming" ); invariant.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: client.gql(utilities.print(client.gql(options.query))) }; } function printMinified(query) { return graphql.stripIgnoredCharacters(utilities.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, internal$1.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 streamUtils.JSONEncodeStream()) } }; } function reviveTransportedQueryRef(queryRef, client) { if (internal$1.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 streamUtils.JSONDecodeStream()), { ...hydratedOptions.context, queryDeduplication: true }) ) }); Object.assign(queryRef, internal$1.wrapQueryRef(internalQueryRef)); } function getInternalQueryRef(client, userOptions, initialFetchOptions) { if (environment.__DEV__) { invariant.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 internal$1.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 = react.useApolloClient(); const isTransported = isTransportedQueryRef(queryRef); if (isTransported) { reviveTransportedQueryRef(queryRef, client); } const unwrapped = internal$1.unwrapQueryRef(queryRef); React.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 = React.useMemo(() => { const transport = {}; for (const key of transportKeys) { transport[key] = result[key]; } return transport; }, [result]); const transported = useTransportValue(forTransport); return React.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 client.ApolloClient { /** * 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, client.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.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.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 client.Observable((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 = React.useRef(undefined); if (!clientRef.current) { { clientRef.current = makeClient(); } assertInstance( clientRef.current, WrappedApolloProvider3.info, "ApolloClient" ); } return /* @__PURE__ */ React__default.default.createElement(react.ApolloProvider, { client: clientRef.current }, /* @__PURE__ */ React__default.default.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; } exports.built_for_ssr = true; exports.ApolloClient = ApolloClient; exports.DataTransportContext = DataTransportContext; exports.DebounceMultipartResponsesLink = AccumulateMultipartResponsesLink; exports.InMemoryCache = InMemoryCache; exports.ReadFromReadableStreamLink = ReadFromReadableStreamLink; exports.RemoveMultipartDirectivesLink = RemoveMultipartDirectivesLink; exports.SSRMultipartLink = SSRMultipartLink; exports.TeeToReadableStreamLink = TeeToReadableStreamLink; exports.WrapApolloProvider = WrapApolloProvider; exports.createTransportedQueryPreloader = createTransportedQueryPreloader; exports.isTransportedQueryRef = isTransportedQueryRef; exports.readFromReadableStream = readFromReadableStream; exports.resetApolloSingletons = resetApolloSingletons; exports.reviveTransportedQueryRef = reviveTransportedQueryRef; exports.skipDataTransport = skipDataTransport; exports.teeToReadableStream = teeToReadableStream; exports.useWrapTransportedQueryRef = useWrapTransportedQueryRef; //# sourceMappingURL=out.js.map //# sourceMappingURL=index.ssr.cjs.map