@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
JavaScript
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