@apollo/client
Version:
A fully-featured caching GraphQL client.
303 lines (302 loc) • 12.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GraphQL17Alpha9Handler = void 0;
const trie_1 = require("@wry/trie");
const graphql_1 = require("graphql");
const utilities_1 = require("@apollo/client/utilities");
const internal_1 = require("@apollo/client/utilities/internal");
const internal_2 = require("@apollo/client/utilities/internal");
const invariant_1 = require("@apollo/client/utilities/invariant");
class IncrementalRequest {
hasNext = true;
data = {};
errors = [];
extensions = {};
pendingMap = new Map();
deferredByPathAndLabel = new trie_1.Trie(false, () => ({}));
completedMap = new Map();
_streamInfo = (0, internal_1.makeStreamInfoTrie)();
// `streamPositions` maps `pending.id` to the index that should be set by the
// next `incremental` stream chunk to ensure the streamed array item is placed
// at the correct point in the data array. `this.data` contains cached
// references with the full array so we can't rely on the array length in
// `this.data` to determine where to place item. This also ensures that items
// updated by the cache between a streamed chunk aren't overwritten by merges
// of future stream items from already merged stream items.
streamPositions = {};
deferLabels = new Set();
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
tracksPending = true;
constructor({ query }) {
(0, graphql_1.visit)(query, {
Directive: (node) => {
const name = node.name.value;
if (name === "defer") {
const labelNode = node.arguments?.find((arg) => arg.name.value === "label");
(0, invariant_1.invariant)(labelNode && labelNode.value.kind === "StringValue", 68);
this.deferLabels.add(labelNode.value.value);
}
},
});
}
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
get streamInfo() {
return this._streamInfo["strong"] ? this._streamInfo : undefined;
}
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
markStreamedPendingForTruncation() {
const streamInfo = this.streamInfo;
if (!streamInfo)
return;
for (const pending of this.pendingMap.values()) {
if (pending.id in this.streamPositions) {
streamInfo.lookupArray(pending.path).state.truncate = true;
}
}
}
pendingDeferCount = 0;
/**
* @internal
*
* @privateRemarks
* Important: this function is used as a reactivity dependency in the `readFromStore.prune*` methods.
* It needs to be a different instance for each instance of `IncrementalRequest`,
* so it cannot be a normal class/prototype method, but it has to be an instance property.
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
isDeferPending = (path, label) => {
if (!path) {
return !!this.pendingDeferCount;
}
const found = this.deferredByPathAndLabel.peek(...path, label);
return !!found && this.completedMap.get(found.id) !== true;
};
handle(cacheData = this.data, chunk) {
this.hasNext = chunk.hasNext;
this.data = cacheData;
if (chunk.pending) {
for (const pending of chunk.pending) {
this.pendingMap.set(pending.id, pending);
if (pending.label != null && this.deferLabels.has(pending.label)) {
this.deferredByPathAndLabel.lookup(...pending.path, pending.label).id = pending.id;
this.pendingDeferCount++;
}
if ("data" in chunk) {
const dataAtPath = pending.path.reduce((data, key) => data[key], chunk.data);
if (Array.isArray(dataAtPath)) {
this.streamPositions[pending.id] = dataAtPath.length;
const entry = this._streamInfo.lookupArray(pending.path);
entry.current = {
isFirstChunk: true,
isLastChunk: false,
};
entry.state.streamPosition = dataAtPath.length;
}
}
}
}
if (hasIncrementalChunks(chunk)) {
for (const incremental of chunk.incremental) {
const pending = this.pendingMap.get(incremental.id);
(0, invariant_1.invariant)(pending, 69);
const path = pending.path.concat(incremental.subPath ?? []);
let data;
if ("items" in incremental) {
const items = incremental.items;
const parent = [];
// This creates a sparse array with values set at the indices streamed
// from the server. DeepMerger uses Object.keys and will correctly
// place the values in this array in the correct place
for (let i = 0; i < items.length; i++) {
parent[i + this.streamPositions[pending.id]] = items[i];
}
this.streamPositions[pending.id] += items.length;
const entry = this._streamInfo.lookupArray(path);
entry.current = {
isFirstChunk: false,
isLastChunk: false,
};
entry.state.streamPosition = this.streamPositions[pending.id];
data = parent;
}
else {
data = incremental.data;
// Check if any pending streams added arrays from deferred data so
// that we can update streamPositions with the initial length of the
// array to ensure future streamed items are inserted at the right
// starting index.
this.pendingMap.forEach((pendingItem) => {
if (!(pendingItem.id in this.streamPositions)) {
// Check if this incremental data contains array data for the pending path
// The pending path is absolute, but incremental data is relative to the defer
// E.g., pending.path = ["nestedObject"], pendingItem.path = ["nestedObject", "nestedFriendList"]
// incremental.data = { scalarField: "...", nestedFriendList: [...] }
// So we need the path from pending.path onwards
const relativePath = pendingItem.path.slice(pending.path.length);
const dataAtPath = relativePath.reduce((data, key) => data?.[key], incremental.data);
if (Array.isArray(dataAtPath)) {
this.streamPositions[pendingItem.id] = dataAtPath.length;
}
}
});
}
this.merge({
data,
extensions: incremental.extensions,
errors: incremental.errors,
}, path);
}
}
else {
this.merge(chunk);
}
if ("completed" in chunk && chunk.completed) {
for (const completed of chunk.completed) {
const { path, label } = this.pendingMap.get(completed.id);
const streamPosition = this.streamPositions[completed.id];
this.completedMap.set(completed.id, !completed.errors);
if (label != null && this.deferLabels.has(label) && !completed.errors) {
this.pendingDeferCount--;
}
// Truncate any stream arrays in case the chunk only contains `hasNext`
// and `completed`.
if (streamPosition !== undefined) {
const dataAtPath = path.reduce((data, key) => data?.[key], this.data);
this.merge({ data: dataAtPath.slice(0, streamPosition) }, path);
}
// peek instead of lookup to avoid creating an entry for non-array values
const details = this._streamInfo.peekArray(path);
if (details) {
details.current = {
isFirstChunk: false,
isLastChunk: true,
};
details.state.streamPosition = streamPosition;
}
if (completed.errors) {
this.errors.push(...completed.errors);
}
}
}
const result = { data: this.data };
if ((0, internal_2.isNonEmptyArray)(this.errors)) {
result.errors = this.errors;
}
if (Object.keys(this.extensions).length > 0) {
result.extensions = this.extensions;
}
if (this._streamInfo["strong"]) {
result.extensions = {
...result.extensions,
// Create a new object so we can check for === in QueryInfo to trigger a
// final cache write when emitting a `hasNext: false` by itself.
// We create a `WeakRef`, not a plain object to avoid retaining memory
// in case the `result` or `extensions` stays around longer than the handler
// itself.
[internal_1.streamInfoSymbol]: new WeakRef(this._streamInfo),
};
}
return result;
}
merge(normalized, atPath) {
if (normalized.data !== undefined) {
this.data = new internal_1.DeepMerger({ arrayMerge: "truncate" }).merge(this.data, normalized.data, { atPath });
}
if (normalized.errors) {
this.errors.push(...normalized.errors);
}
Object.assign(this.extensions, normalized.extensions);
}
}
/**
* Provides handling for the incremental delivery specification implemented by
* graphql.js version `17.0.0-alpha.9`.
*/
class GraphQL17Alpha9Handler {
documentTransform = new utilities_1.DocumentTransform(internal_1.addDeferFragmentLabels);
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
isIncrementalResult(result) {
return "hasNext" in result;
}
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
prepareRequest(request) {
if ((0, internal_2.hasDirectives)(["defer", "stream"], request.query)) {
const context = request.context ?? {};
const http = (context.http ??= {});
// https://specs.apollo.dev/incremental/v0.2/
http.accept = [
"multipart/mixed;incrementalSpec=v0.2",
...(http.accept || []),
];
request.context = context;
}
return request;
}
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
extractErrors(result) {
const acc = [];
const push = ({ errors, }) => {
if (errors) {
acc.push(...errors);
}
};
if (this.isIncrementalResult(result)) {
if ("errors" in result) {
push(result);
}
if (hasIncrementalChunks(result)) {
result.incremental.forEach(push);
}
if (hasCompletedChunks(result)) {
result.completed.forEach(push);
}
}
else if ("errors" in result) {
push(result);
}
if (acc.length) {
return acc;
}
}
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
startRequest(opts) {
return new IncrementalRequest(opts);
}
}
exports.GraphQL17Alpha9Handler = GraphQL17Alpha9Handler;
function hasIncrementalChunks(result) {
return (0, internal_2.isNonEmptyArray)(result.incremental);
}
function hasCompletedChunks(result) {
return (0, internal_2.isNonEmptyArray)(result.completed);
}
//# sourceMappingURL=graphql17Alpha9.cjs.map