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

597 lines (588 loc) 19.5 kB
import React2, { cache } from 'react'; import { SimulatePreloadedQuery } from './index.cc.js'; import { wrapQueryRef, unwrapQueryRef, InternalQueryReference } from '@apollo/client/react/internal'; import { ApolloLink, Observable, InMemoryCache as InMemoryCache$1, gql, ApolloClient as ApolloClient$1 } from '@apollo/client'; import { invariant } from '@apollo/client/utilities/invariant'; import { Observable as Observable$1, print } from '@apollo/client/utilities'; import { stripIgnoredCharacters } from 'graphql'; import { JSONEncodeStream, JSONDecodeStream } from '@apollo/client-react-streaming/stream-utils'; import { __DEV__ } from '@apollo/client/utilities/environment'; import { hasDirectives, removeDirectivesFromDocument } from '@apollo/client/utilities/internal'; import { of } from 'rxjs'; // src/registerApolloClient.tsx 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((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((observer) => { let aborted = false; const reader = (() => { try { return eventSteam.getReader(); } catch { } })(); if (!reader) { const subscription = forward(operation).subscribe(observer); return () => subscription.unsubscribe(); } let forwarded = false; consume(reader).finally(() => { if (!observer.closed && !forwarded) { 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": { onAbort(); forwarded = true; const subscription = forward(operation).subscribe(observer); onAbort = () => subscription.unsubscribe(); } break; } } } } }); } return forward(operation); }); } }; // 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`; } }; 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)); } // 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}"\`.` ); } } 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 skipDataTransportKey = Symbol.for("apollo.dataTransport.skip"); function skipDataTransport(context) { return Object.assign(context, { [skipDataTransportKey]: true }); } var ApolloClientRSCImpl = class extends ApolloClientBase { }; var ApolloClientImplementation = ApolloClientRSCImpl; var ApolloClient = class extends ApolloClientImplementation { }; 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 sanitizeForTransport(value) { return JSON.parse(JSON.stringify(value)); } // src/PreloadQuery.tsx async function PreloadQuery({ getClient, children, query, ...transportedOptions }) { const preloader = createTransportedQueryPreloader(await getClient(), { notTransportedOptionOverrides: { fetchPolicy: "no-cache" } }); const queryRef = preloader( query, transportedOptions ); return /* @__PURE__ */ React2.createElement(SimulatePreloadedQuery, { queryRef }, typeof children === "function" ? children(queryRef) : children); } // src/registerApolloClient.tsx var seenWrappers = WeakSet ? /* @__PURE__ */ new WeakSet() : undefined; var seenClients = WeakSet ? /* @__PURE__ */ new WeakSet() : undefined; var checkForStableCache = cache(() => ({})); function registerApolloClient(makeClient) { const getClient = makeGetClient(makeClient); const getPreloadClient = makeGetClient(makeClient); const PreloadQuery2 = makePreloadQuery(getPreloadClient); return { getClient, query: async (...args) => { if (checkForStableCache() !== checkForStableCache()) { console.warn( ` The \`query\` shortcut returned from \`registerApolloClient\` should not be used in Server Action or Middleware environments. Calling it multiple times in those environments would create multiple independent \`ApolloClient\` instances. Please create a single \`ApolloClient\` instance by calling \`getClient()\` at the beginning of your Server Action or Middleware function and then call \`client.query\` multiple times instead. `.trim() ); } return (await getClient()).query(...args); }, PreloadQuery: PreloadQuery2 }; } function makeGetClient(makeClient) { function makeWrappedClient() { return { client: makeClient() }; } const cachedMakeWrappedClient = cache(makeWrappedClient); function getClient() { if (arguments.length) { throw new Error( ` You cannot pass arguments into \`getClient\`. Passing arguments to \`getClient\` returns a different instance of Apollo Client each time it is called with different arguments, potentially resulting in duplicate requests and a non-functional cache. `.trim() ); } const wrapper = cachedMakeWrappedClient(); if (seenWrappers && seenClients) { if (!seenWrappers.has(wrapper)) { if (seenClients.has(wrapper.client)) { console.warn( ` Multiple calls to \`getClient\` for different requests returned the same client instance. This means that private user data could accidentally be shared between requests. This happens, for example, if you create a global \`ApolloClient\` instance and your \`makeClient\` implementation just looks like \`() => client\`. Always call \`new ApolloClient\` **inside** your \`makeClient\` function and return a new instance every time \`makeClient\` is called. `.trim() ); } seenWrappers.add(wrapper); seenClients.add(wrapper.client); } } return wrapper.client; } return getClient; } function makePreloadQuery(getClient) { return function PreloadQuery2(props) { return /* @__PURE__ */ React2.createElement(PreloadQuery, { getClient, ...props }); }; } 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$1((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); } }; const built_for_rsc = true; export { ApolloClient, AccumulateMultipartResponsesLink as DebounceMultipartResponsesLink, InMemoryCache, ReadFromReadableStreamLink, RemoveMultipartDirectivesLink, SSRMultipartLink, TeeToReadableStreamLink, built_for_rsc, createTransportedQueryPreloader, isTransportedQueryRef, readFromReadableStream, registerApolloClient, reviveTransportedQueryRef, teeToReadableStream }; //# sourceMappingURL=out.js.map //# sourceMappingURL=index.rsc.js.map