@apollo/client 4.3.0-alpha.3 → 4.3.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/__cjs/cache/core/types/Cache.d.cts +25 -0
  3. package/__cjs/cache/index.cjs.map +1 -1
  4. package/__cjs/cache/index.d.cts +1 -1
  5. package/__cjs/cache/inmemory/inMemoryCache.cjs +4 -0
  6. package/__cjs/cache/inmemory/inMemoryCache.cjs.map +1 -1
  7. package/__cjs/cache/inmemory/inMemoryCache.d.cts +7 -3
  8. package/__cjs/cache/inmemory/readFromStore.cjs +453 -42
  9. package/__cjs/cache/inmemory/readFromStore.cjs.map +1 -1
  10. package/__cjs/cache/inmemory/readFromStore.d.cts +11 -2
  11. package/__cjs/cache/inmemory/types.d.cts +11 -1
  12. package/__cjs/cache/inmemory/writeToStore.cjs +1 -5
  13. package/__cjs/cache/inmemory/writeToStore.cjs.map +1 -1
  14. package/__cjs/core/QueryInfo.cjs +86 -172
  15. package/__cjs/core/QueryInfo.cjs.map +1 -1
  16. package/__cjs/core/QueryInfo.d.cts +8 -2
  17. package/__cjs/core/QueryManager.cjs +3 -1
  18. package/__cjs/core/QueryManager.cjs.map +1 -1
  19. package/__cjs/incremental/handlers/defer20220824.cjs.map +1 -1
  20. package/__cjs/incremental/handlers/defer20220824.d.cts +2 -4
  21. package/__cjs/incremental/handlers/graphql17Alpha9.cjs +20 -12
  22. package/__cjs/incremental/handlers/graphql17Alpha9.cjs.map +1 -1
  23. package/__cjs/incremental/handlers/graphql17Alpha9.d.cts +5 -5
  24. package/__cjs/incremental/types.d.cts +16 -3
  25. package/__cjs/utilities/caching/sizes.cjs.map +1 -1
  26. package/__cjs/utilities/caching/sizes.d.cts +39 -1
  27. package/__cjs/utilities/internal/StreamArrayState.cjs +27 -0
  28. package/__cjs/utilities/internal/StreamArrayState.cjs.map +1 -0
  29. package/__cjs/utilities/internal/StreamArrayState.d.cts +12 -0
  30. package/__cjs/utilities/internal/constants.cjs +20 -1
  31. package/__cjs/utilities/internal/constants.cjs.map +1 -1
  32. package/__cjs/utilities/internal/constants.d.cts +19 -0
  33. package/__cjs/utilities/internal/getFragmentFromSelection.cjs +1 -1
  34. package/__cjs/utilities/internal/getFragmentFromSelection.cjs.map +1 -1
  35. package/__cjs/utilities/internal/getMemoryInternals.cjs +6 -0
  36. package/__cjs/utilities/internal/getMemoryInternals.cjs.map +1 -1
  37. package/__cjs/utilities/internal/getMemoryInternals.d.cts +4 -0
  38. package/__cjs/utilities/internal/index.cjs +9 -4
  39. package/__cjs/utilities/internal/index.cjs.map +1 -1
  40. package/__cjs/utilities/internal/index.d.cts +5 -3
  41. package/__cjs/utilities/internal/isDeferredFragment.cjs +19 -4
  42. package/__cjs/utilities/internal/isDeferredFragment.cjs.map +1 -1
  43. package/__cjs/utilities/internal/isDeferredFragment.d.cts +6 -1
  44. package/__cjs/utilities/internal/isStreamField.cjs +37 -0
  45. package/__cjs/utilities/internal/isStreamField.cjs.map +1 -0
  46. package/__cjs/utilities/internal/isStreamField.d.cts +9 -0
  47. package/__cjs/utilities/internal/makeStreamInfoTrie.cjs +17 -0
  48. package/__cjs/utilities/internal/makeStreamInfoTrie.cjs.map +1 -0
  49. package/__cjs/utilities/internal/makeStreamInfoTrie.d.cts +8 -0
  50. package/__cjs/utilities/internal/types/DeferInfo.cjs +3 -0
  51. package/__cjs/utilities/internal/types/DeferInfo.cjs.map +1 -0
  52. package/__cjs/utilities/internal/types/DeferInfo.d.cts +10 -0
  53. package/__cjs/utilities/internal/types/StreamInfoTrie.d.cts +2 -0
  54. package/__cjs/version.cjs +1 -1
  55. package/cache/core/types/Cache.d.ts +25 -0
  56. package/cache/core/types/Cache.js.map +1 -1
  57. package/cache/index.d.ts +1 -1
  58. package/cache/index.js.map +1 -1
  59. package/cache/inmemory/inMemoryCache.d.ts +7 -3
  60. package/cache/inmemory/inMemoryCache.js +5 -1
  61. package/cache/inmemory/inMemoryCache.js.map +1 -1
  62. package/cache/inmemory/readFromStore.d.ts +11 -2
  63. package/cache/inmemory/readFromStore.js +454 -43
  64. package/cache/inmemory/readFromStore.js.map +1 -1
  65. package/cache/inmemory/types.d.ts +12 -2
  66. package/cache/inmemory/types.js.map +1 -1
  67. package/cache/inmemory/writeToStore.js +2 -6
  68. package/cache/inmemory/writeToStore.js.map +1 -1
  69. package/core/QueryInfo.d.ts +8 -2
  70. package/core/QueryInfo.js +87 -173
  71. package/core/QueryInfo.js.map +1 -1
  72. package/core/QueryManager.js +3 -1
  73. package/core/QueryManager.js.map +1 -1
  74. package/incremental/handlers/defer20220824.d.ts +2 -4
  75. package/incremental/handlers/defer20220824.js.map +1 -1
  76. package/incremental/handlers/graphql17Alpha9.d.ts +5 -5
  77. package/incremental/handlers/graphql17Alpha9.js +21 -13
  78. package/incremental/handlers/graphql17Alpha9.js.map +1 -1
  79. package/incremental/types.d.ts +16 -3
  80. package/incremental/types.js.map +1 -1
  81. package/package.json +1 -1
  82. package/utilities/caching/sizes.d.ts +39 -1
  83. package/utilities/caching/sizes.js.map +1 -1
  84. package/utilities/internal/StreamArrayState.d.ts +12 -0
  85. package/utilities/internal/StreamArrayState.js +23 -0
  86. package/utilities/internal/StreamArrayState.js.map +1 -0
  87. package/utilities/internal/constants.d.ts +19 -0
  88. package/utilities/internal/constants.js +19 -0
  89. package/utilities/internal/constants.js.map +1 -1
  90. package/utilities/internal/getFragmentFromSelection.js +1 -1
  91. package/utilities/internal/getFragmentFromSelection.js.map +1 -1
  92. package/utilities/internal/getMemoryInternals.d.ts +4 -0
  93. package/utilities/internal/getMemoryInternals.js +6 -0
  94. package/utilities/internal/getMemoryInternals.js.map +1 -1
  95. package/utilities/internal/index.d.ts +5 -3
  96. package/utilities/internal/index.js +4 -2
  97. package/utilities/internal/index.js.map +1 -1
  98. package/utilities/internal/isDeferredFragment.d.ts +7 -2
  99. package/utilities/internal/isDeferredFragment.js +19 -4
  100. package/utilities/internal/isDeferredFragment.js.map +1 -1
  101. package/utilities/internal/isStreamField.d.ts +9 -0
  102. package/utilities/internal/isStreamField.js +34 -0
  103. package/utilities/internal/isStreamField.js.map +1 -0
  104. package/utilities/internal/makeStreamInfoTrie.d.ts +8 -0
  105. package/utilities/internal/makeStreamInfoTrie.js +14 -0
  106. package/utilities/internal/makeStreamInfoTrie.js.map +1 -0
  107. package/utilities/internal/types/DeferInfo.d.ts +10 -0
  108. package/utilities/internal/types/DeferInfo.js +2 -0
  109. package/utilities/internal/types/DeferInfo.js.map +1 -0
  110. package/utilities/internal/types/StreamInfoTrie.d.ts +2 -0
  111. package/utilities/internal/types/StreamInfoTrie.js.map +1 -1
  112. package/version.js +1 -1
  113. package/__cjs/utilities/internal/collectSiblingFields.cjs +0 -61
  114. package/__cjs/utilities/internal/collectSiblingFields.cjs.map +0 -1
  115. package/__cjs/utilities/internal/collectSiblingFields.d.cts +0 -21
  116. package/utilities/internal/collectSiblingFields.d.ts +0 -21
  117. package/utilities/internal/collectSiblingFields.js +0 -58
  118. package/utilities/internal/collectSiblingFields.js.map +0 -1
@@ -1,8 +1,10 @@
1
+ import { equal } from "@wry/equality";
2
+ import { Trie } from "@wry/trie";
1
3
  import { Kind } from "graphql";
2
4
  import { wrap } from "optimism";
3
5
  import { addTypenameToDocument, cacheSizes, canonicalStringify, isReference, } from "@apollo/client/utilities";
4
6
  import { __DEV__ } from "@apollo/client/utilities/environment";
5
- import { DeepMerger, getDefaultValues, getFragmentFromSelection, getMainDefinition, getQueryDefinition, isArray, isField, isNonNullObject, makeReference, maybeDeepFreeze, mergeDeepArray, resultKeyNameFromField, shouldInclude, } from "@apollo/client/utilities/internal";
7
+ import { DeepMerger, getDefaultValues, getFragmentFromSelection, getMainDefinition, getQueryDefinition, handleIncrementalSymbol, isArray, isDeferredFragment, isField, isNonNullObject, isStreamField, makeReference, maybeDeepFreeze, mergeDeepArray, resultKeyNameFromField, shouldInclude, } from "@apollo/client/utilities/internal";
6
8
  import { invariant, newInvariantError, } from "@apollo/client/utilities/invariant";
7
9
  import { MissingFieldError } from "../core/types/common.js";
8
10
  import { maybeDependOnExistenceOfEntity, supportsResultCaching, } from "./entityStore.js";
@@ -15,8 +17,11 @@ export class StoreReader {
15
17
  executeSelectionSet;
16
18
  // cached version of executeSubSelectedArray
17
19
  executeSubSelectedArray;
20
+ prunePartialStreamArray;
21
+ prunePartialBoundaries;
18
22
  config;
19
23
  knownResults = new WeakMap();
24
+ keyMaker = new Trie();
20
25
  constructor(config) {
21
26
  this.config = config;
22
27
  // memoized functions in this class will be "garbage-collected"
@@ -59,47 +64,143 @@ export class StoreReader {
59
64
  }
60
65
  },
61
66
  });
67
+ this.prunePartialBoundaries = wrap((options) => this.prunePartialBoundariesImpl(options), {
68
+ max: cacheSizes["inMemoryCache.prunePartialBoundaries"] ||
69
+ 20000 /* defaultCacheSizes["inMemoryCache.prunePartialBoundaries"] */,
70
+ makeCacheKey: ({ boundaries, context, selectionSet }) => {
71
+ if (supportsResultCaching(context.store)) {
72
+ return this.keyMaker.lookup(selectionSet, boundaries, context.streamInfo, context.deferInfo);
73
+ }
74
+ },
75
+ });
76
+ this.prunePartialStreamArray = wrap((options) => {
77
+ const { field, context, path } = options;
78
+ if (isStreamField(field, context.variables)) {
79
+ context.streamInfo?.lookupArray(path).state.depend();
80
+ }
81
+ return this.prunePartialStreamArrayImpl(options);
82
+ }, {
83
+ max: cacheSizes["inMemoryCache.prunePartialStreamArray"] ||
84
+ 20000 /* defaultCacheSizes["inMemoryCache.prunePartialStreamArray"] */,
85
+ makeCacheKey: ({ field, context, boundaries }) => {
86
+ if (supportsResultCaching(context.store)) {
87
+ return this.keyMaker.lookup(field, boundaries, context.streamInfo, context.deferInfo);
88
+ }
89
+ },
90
+ });
62
91
  }
63
- /**
64
- * Given a store and a query, return as much of the result as possible and
65
- * identify if any data was missing from the store.
66
- */
67
- diffQueryAgainstStore({ store, query, rootId = "ROOT_QUERY", variables, returnPartialData = true, }) {
92
+ diffQueryAgainstStore({ store, query, rootId = "ROOT_QUERY", variables, returnPartialData = true, [handleIncrementalSymbol]: handleIncremental, }) {
68
93
  const policies = this.config.cache.policies;
69
94
  variables = {
70
95
  ...getDefaultValues(getQueryDefinition(query)),
71
96
  ...variables,
72
97
  };
73
98
  const rootRef = makeReference(rootId);
74
- const execResult = this.executeSelectionSet({
99
+ const context = {
100
+ store,
101
+ query,
102
+ policies,
103
+ variables,
104
+ varString: canonicalStringify(variables),
105
+ ...extractFragmentContext(query, this.config.fragments),
106
+ ...(typeof handleIncremental === "object" ? handleIncremental : (undefined)),
107
+ };
108
+ let execResult = this.executeSelectionSet({
75
109
  selectionSet: getMainDefinition(query).selectionSet,
76
110
  objectOrReference: rootRef,
77
111
  enclosingRef: rootRef,
78
- context: {
79
- store,
80
- query,
81
- policies,
82
- variables,
83
- varString: canonicalStringify(variables),
84
- ...extractFragmentContext(query, this.config.fragments),
85
- },
112
+ context,
86
113
  });
87
- let missing;
88
- if (execResult.missing) {
89
- missing = new MissingFieldError(firstMissing(execResult.missing), execResult.missing, query, variables);
114
+ // Since executeSelectionSet doesn't know about returnPartialData, we need
115
+ // to perform a 2nd pass over the result to prune any fields inside
116
+ // partial defer boundaries. The "deferPartial" data state tells us that the
117
+ // only part of the result that contributed to its partiality is data inside
118
+ // a defer boundary.
119
+ if (handleIncremental &&
120
+ (execResult.dataState === "deferPartial" ||
121
+ execResult.dataState === "streamPartial" ||
122
+ // If the last cache write repaired a partial @stream array to a
123
+ // complete array, the stream array might contain stale entries after
124
+ // the last written value. We only want to deliver the results up to
125
+ // the index the network wrote so we need to prune it too.
126
+ context.streamInfo ||
127
+ // The network hasn't delivered these @defer boundaries yet, so prune
128
+ // the (possibly complete) cached data sitting at them.
129
+ context.deferInfo)) {
130
+ const pruned = this.prunePartialBoundaries({
131
+ selectionSet: getMainDefinition(query).selectionSet,
132
+ data: execResult.result,
133
+ boundaries: execResult.partialBoundaries,
134
+ context,
135
+ path: [],
136
+ });
137
+ const changed = execResult.result !== pruned;
138
+ // It's possible that pruning didn't actually change the result which can
139
+ // happen if a defer boundary is misclassified as "deferPartial" instead
140
+ // of "streaming" (sibling defer boundaries with overlapping selection
141
+ // sets, one of which is complete). In this case, pruning corrects the
142
+ // dataState to streaming instead of leaving it as partial. If we tolerate
143
+ // partial results and pruning changed the result by dropping fields, then
144
+ // we want to keep the original execResult which contains the partial
145
+ // data.
146
+ if (!changed || !returnPartialData) {
147
+ const { dataState } = execResult;
148
+ // Omit `missing` property since pruning puts it in a state that doesn't
149
+ // report missing fields.
150
+ execResult = {
151
+ result: pruned,
152
+ partialBoundaries: execResult.partialBoundaries,
153
+ dataState: dataState === "deferPartial" ? "streaming"
154
+ : dataState === "streamPartial" ? "complete"
155
+ // A cached @defer boundary the network hasn't delivered was
156
+ // pruned from an otherwise complete result, so we're still
157
+ // streaming.
158
+ : context.deferInfo && changed ? "streaming"
159
+ : dataState,
160
+ };
161
+ }
90
162
  }
91
- const complete = !missing;
92
- const { result } = execResult;
93
- return {
94
- result: complete ? result
95
- : returnPartialData ?
96
- Object.keys(result).length === 0 ?
97
- null
98
- : result
99
- : null,
163
+ let { result, dataState, missing } = execResult;
164
+ // Evaluate this condition before we start mucking with dataState for the
165
+ // publicly returned value
166
+ const includeMissing = !!missing &&
167
+ // We don't need to report missing fields inside defer boundaries since
168
+ // the "streaming" dataState tells us that the only missing fields in
169
+ // the object is inside a defer boundary.
170
+ (dataState !== "streaming" || !handleIncremental);
171
+ // If we get all root @defer boundaries with an empty result, report it as
172
+ // empty instead of streaming.
173
+ if (dataState === "streaming" && Object.keys(result).length === 0) {
174
+ dataState = "empty";
175
+ }
176
+ let missingError;
177
+ if (dataState === "deferPartial" ||
178
+ dataState === "streamPartial" ||
179
+ (dataState === "streaming" && !handleIncremental)) {
180
+ dataState = "partial";
181
+ }
182
+ if (dataState === "partial" && !returnPartialData) {
183
+ dataState = "empty";
184
+ }
185
+ const complete = dataState === "complete";
186
+ const keepResult = complete ||
187
+ dataState === "streaming" ||
188
+ (returnPartialData && Object.keys(result).length);
189
+ const diffResult = {
190
+ result: keepResult ? result : null,
100
191
  complete,
101
- missing,
192
+ get missing() {
193
+ if (includeMissing) {
194
+ missingError ||= new MissingFieldError(firstMissing(missing), missing, query, variables);
195
+ }
196
+ return missingError;
197
+ },
102
198
  };
199
+ if (handleIncremental) {
200
+ diffResult.dataState =
201
+ dataState;
202
+ }
203
+ return diffResult;
103
204
  }
104
205
  isFresh(result, parent, selectionSet, context) {
105
206
  if (supportsResultCaching(context.store) &&
@@ -118,14 +219,18 @@ export class StoreReader {
118
219
  !context.store.has(objectOrReference.__ref)) {
119
220
  return {
120
221
  result: {},
222
+ dataState: "empty",
121
223
  missing: `Dangling reference to missing ${objectOrReference.__ref} object`,
224
+ partialBoundaries: new PartialBoundaries(),
122
225
  };
123
226
  }
124
227
  const { variables, policies, store } = context;
125
228
  const typename = store.getFieldValue(objectOrReference, "__typename");
126
229
  const objectsToMerge = [];
230
+ let dataState;
127
231
  let missing;
128
232
  const missingMerger = new DeepMerger();
233
+ const partialBoundaries = new PartialBoundaries();
129
234
  if (typeof typename === "string" && !policies.rootIdsByTypename[typename]) {
130
235
  // Ensure we always include a default value for the __typename
131
236
  // field, if we have one. Note that this field can be overridden by other
@@ -138,7 +243,7 @@ export class StoreReader {
138
243
  [resultName]: result.missing,
139
244
  });
140
245
  }
141
- return result.result;
246
+ return result;
142
247
  }
143
248
  const workSet = new Set(selectionSet.selections);
144
249
  workSet.forEach((selection) => {
@@ -156,25 +261,39 @@ export class StoreReader {
156
261
  const resultName = resultKeyNameFromField(selection);
157
262
  if (fieldValue === void 0) {
158
263
  if (!addTypenameToDocument.added(selection)) {
264
+ const id = isReference(objectOrReference) ? objectOrReference.__ref
265
+ : objectOrReference ? policies.identify(objectOrReference)[0]
266
+ : undefined;
159
267
  missing = missingMerger.merge(missing, {
160
- [resultName]: `Can't find field '${selection.name.value}' on ${isReference(objectOrReference) ?
161
- objectOrReference.__ref + " object"
162
- : "object " + JSON.stringify(objectOrReference, null, 2)}`,
268
+ [resultName]: `Can't find field '${selection.name.value}' on ${id ?
269
+ `${id} object`
270
+ : `object ${JSON.stringify(objectOrReference || {}, null, 2)}`}`,
163
271
  });
272
+ dataState = mergeDataState(dataState, "empty");
164
273
  }
165
274
  }
166
275
  else if (isArray(fieldValue)) {
167
276
  if (fieldValue.length > 0) {
168
- fieldValue = handleMissing(this.executeSubSelectedArray({
277
+ const execResult = handleMissing(this.executeSubSelectedArray({
169
278
  field: selection,
170
279
  array: fieldValue,
171
280
  enclosingRef,
172
281
  context,
173
282
  }), resultName);
283
+ fieldValue = execResult.result;
284
+ dataState = mergeDataState(dataState, execResult.dataState);
285
+ partialBoundaries.set(resultName, execResult.partialBoundaries);
286
+ }
287
+ else {
288
+ dataState = mergeDataState(dataState, "complete");
174
289
  }
175
290
  }
176
291
  else if (!selection.selectionSet) {
177
- // do nothing
292
+ // Auto-inserted __typename should not affect dataState (see empty
293
+ // @defer boundaries, which must stay "empty" → parent "streaming").
294
+ if (!addTypenameToDocument.added(selection)) {
295
+ dataState = mergeDataState(dataState, "complete");
296
+ }
178
297
  }
179
298
  else if (fieldValue != null) {
180
299
  if (__DEV__) {
@@ -189,12 +308,28 @@ export class StoreReader {
189
308
  // In this case, because we know the field has a selection set,
190
309
  // it must be trying to query a GraphQLObjectType, which is why
191
310
  // fieldValue must be != null.
192
- fieldValue = handleMissing(this.executeSelectionSet({
311
+ const execResult = handleMissing(this.executeSelectionSet({
193
312
  selectionSet: selection.selectionSet,
194
313
  objectOrReference: fieldValue,
195
314
  enclosingRef: isReference(fieldValue) ? fieldValue : enclosingRef,
196
315
  context,
197
316
  }), resultName);
317
+ fieldValue = execResult.result;
318
+ partialBoundaries.set(resultName, execResult.partialBoundaries);
319
+ // If the object's fields resolved to an "empty" dataState (e.g. no
320
+ // field resolved with a non-undefined value), but the fieldValue
321
+ // object itself is present, this object should be considered
322
+ // partial instead of empty. This also ensures defer boundaries that
323
+ // select this object remain as partial defer boundaries rather than
324
+ // mistakenly get reported as streaming. This is especially necessary
325
+ // when combined with GraphQL Codegen which generates its type and
326
+ // relies on the outer object to be absent when its fields haven't
327
+ // streamed in. Reporting the defer boundary as "streaming" instead of
328
+ // "partial" would otherwise have the potential to cause runtime
329
+ // crashes since the runtime values and types would not line up
330
+ // properly (types expect object to be undefined, but its instead
331
+ // present without its fields)
332
+ dataState = mergeDataState(dataState, execResult.dataState === "empty" ? "partial" : execResult.dataState);
198
333
  }
199
334
  if (fieldValue !== void 0) {
200
335
  objectsToMerge.push({ [resultName]: fieldValue });
@@ -206,12 +341,56 @@ export class StoreReader {
206
341
  throw newInvariantError(113, selection.name.value);
207
342
  }
208
343
  if (fragment && policies.fragmentMatches(fragment, typename)) {
209
- fragment.selectionSet.selections.forEach(workSet.add, workSet);
344
+ const isDeferBoundary = isDeferredFragment(selection, context.variables);
345
+ // Prior to 4.3, this branch just flattened the fragment's
346
+ // selectionSet into the existing workSet so that it continued
347
+ // iterating as if the fragment didn't exist. The cache is
348
+ // incremental aware as of 4.3 and as such, we need to resolve the
349
+ // per-fragment selection set so that we can properly strip partial
350
+ // defer fragment data when returnPartialData is false. We need to
351
+ // call execSelectionSetImpl directly (non-cached version) so that we
352
+ // scope the dataState correctly for its fields. Using the cached
353
+ // executeSelectionSet can result in cache poisoning when combined
354
+ // with the fragment registry where it might cache either a) an error
355
+ // thrown when a registered fragment references a named fragment that
356
+ // the query is expected to supply and doesn't or b) resolve to the
357
+ // wrong data result when combined with queries that provide different
358
+ // implementations of the same fragment (see inmemory/fragmentRegistry and
359
+ // cache.diff/incremental tests which provide guards against this
360
+ // behavior).
361
+ const execResult = this.execSelectionSetImpl({
362
+ selectionSet: fragment.selectionSet,
363
+ objectOrReference,
364
+ enclosingRef,
365
+ context,
366
+ });
367
+ const { result, dataState: nextDataState } = execResult;
368
+ partialBoundaries.merge(execResult.partialBoundaries);
369
+ if (result !== void 0) {
370
+ objectsToMerge.push(result);
371
+ }
372
+ if (execResult.missing) {
373
+ missing = missingMerger.merge(missing, execResult.missing);
374
+ }
375
+ if (isDeferBoundary && nextDataState === "partial") {
376
+ partialBoundaries.add(selection);
377
+ }
378
+ dataState = mergeDataState(dataState, isDeferBoundary ?
379
+ nextDataState === "empty" ? "streaming"
380
+ : nextDataState === "partial" ? "deferPartial"
381
+ : nextDataState
382
+ : nextDataState);
210
383
  }
211
384
  }
212
385
  });
386
+ dataState ||= "complete";
213
387
  const result = mergeDeepArray(objectsToMerge);
214
- const finalResult = { result, missing };
388
+ const finalResult = {
389
+ result,
390
+ missing,
391
+ dataState,
392
+ partialBoundaries,
393
+ };
215
394
  const frozen = maybeDeepFreeze(finalResult);
216
395
  // Store this result with its selection set so that we can quickly
217
396
  // recognize it again in the StoreReader#isFresh method.
@@ -222,8 +401,11 @@ export class StoreReader {
222
401
  }
223
402
  // Uncached version of executeSubSelectedArray.
224
403
  execSubSelectedArrayImpl({ field, array, enclosingRef, context, }) {
404
+ let dataState = "complete";
225
405
  let missing;
226
406
  let missingMerger = new DeepMerger();
407
+ const partialBoundaries = new PartialBoundaries();
408
+ const isStreamed = isStreamField(field, context.variables);
227
409
  function handleMissing(childResult, i) {
228
410
  if (childResult.missing) {
229
411
  missing = missingMerger.merge(missing, { [i]: childResult.missing });
@@ -238,23 +420,36 @@ export class StoreReader {
238
420
  if (item === null) {
239
421
  return null;
240
422
  }
423
+ let execResult;
241
424
  // This is a nested array, recurse
242
425
  if (isArray(item)) {
243
- return handleMissing(this.executeSubSelectedArray({
426
+ execResult = this.executeSubSelectedArray({
244
427
  field,
245
428
  array: item,
246
429
  enclosingRef,
247
430
  context,
248
- }), i);
431
+ });
249
432
  }
250
- // This is an object, run the selection set on it
251
- if (field.selectionSet) {
252
- return handleMissing(this.executeSelectionSet({
433
+ else if (field.selectionSet) {
434
+ execResult = this.executeSelectionSet({
253
435
  selectionSet: field.selectionSet,
254
436
  objectOrReference: item,
255
437
  enclosingRef: isReference(item) ? item : enclosingRef,
256
438
  context,
257
- }), i);
439
+ });
440
+ }
441
+ if (execResult) {
442
+ const { dataState: nextDataState } = execResult;
443
+ partialBoundaries.set(i, nextDataState === "partial" ?
444
+ // avoid mutating the execResult partialBoundaries object
445
+ execResult.partialBoundaries.clone().add(field)
446
+ : execResult.partialBoundaries);
447
+ dataState = mergeDataState(dataState, isStreamed ?
448
+ nextDataState === "partial" ?
449
+ "streamPartial"
450
+ : nextDataState
451
+ : nextDataState);
452
+ return handleMissing(execResult, i);
258
453
  }
259
454
  if (__DEV__) {
260
455
  assertSelectionSetForIdValue(context.store, field, item);
@@ -263,9 +458,144 @@ export class StoreReader {
263
458
  });
264
459
  return {
265
460
  result: array,
461
+ dataState,
266
462
  missing,
463
+ partialBoundaries,
267
464
  };
268
465
  }
466
+ prunePartialBoundariesImpl({ boundaries, context, data, path, selectionSet, }) {
467
+ const { variables, lookupFragment, policies } = context;
468
+ if (data == null || !boundaries)
469
+ return data;
470
+ const merger = new DeepMerger();
471
+ let changed = false;
472
+ const result = {};
473
+ // __typename might not be part of the selection set, so preserve it when
474
+ // available, otherwise it gets removed since it's never visited when
475
+ // iterating the selection set.
476
+ if (Object.hasOwn(data, "__typename")) {
477
+ result.__typename = data.__typename;
478
+ }
479
+ const workSet = new Set(selectionSet.selections);
480
+ workSet.forEach((selection) => {
481
+ if (!shouldInclude(selection, variables))
482
+ return;
483
+ if (isField(selection)) {
484
+ const resultName = resultKeyNameFromField(selection);
485
+ if (!Object.hasOwn(data, resultName)) {
486
+ return;
487
+ }
488
+ const fieldValue = data[resultName];
489
+ if (Array.isArray(fieldValue)) {
490
+ const pruned = this.prunePartialStreamArray({
491
+ field: selection,
492
+ array: fieldValue,
493
+ boundaries: boundaries.getChild(resultName),
494
+ context,
495
+ path: path.concat(resultName),
496
+ });
497
+ changed ||= pruned !== fieldValue;
498
+ result[resultName] = pruned;
499
+ }
500
+ else if (!selection.selectionSet) {
501
+ result[resultName] = fieldValue;
502
+ }
503
+ else {
504
+ const pruned = this.prunePartialBoundaries({
505
+ data: fieldValue,
506
+ selectionSet: selection.selectionSet,
507
+ boundaries: boundaries.getChild(resultName),
508
+ context,
509
+ path: path.concat(resultName),
510
+ });
511
+ changed ||= pruned !== fieldValue;
512
+ // A response key can be selected by more than one selection (e.g. a
513
+ // field and an overlapping fragment), so merge their kept fields.
514
+ result[resultName] =
515
+ Object.hasOwn(result, resultName) ?
516
+ merger.merge(result[resultName], pruned)
517
+ : pruned;
518
+ }
519
+ return;
520
+ }
521
+ // Note: we do NOT set `changed` to true anywhere in this branch of the
522
+ // conditional, despite the fact that we might have encountered a
523
+ // partial @defer boundary. Dropping a fragment does not guarantee keys
524
+ // are actually dropped which can happen when overlapping sibling
525
+ // selections contribute to the construction of the object. The final
526
+ // Object.keys(result).length check actually detects whether keys were
527
+ // dropped or not.
528
+ const fragment = getFragmentFromSelection(selection, lookupFragment);
529
+ if (fragment &&
530
+ policies.fragmentMatches(fragment, data.__typename) &&
531
+ !boundaries.has(selection) &&
532
+ !context.deferInfo?.peekArray(path)) {
533
+ fragment.selectionSet.selections.forEach(workSet.add, workSet);
534
+ }
535
+ });
536
+ if (Object.keys(result).length !== Object.keys(data).length) {
537
+ changed = true;
538
+ }
539
+ else if (changed && boundaries.hasSelections()) {
540
+ // Overlapping siblings may rebuild the same fields under a new object
541
+ // identity (e.g. changed === true) after a partial @defer is skipped.
542
+ // We perform a deep equality check to verify whether anything was
543
+ // actually dropped by the partial @defer fragment.
544
+ changed = !equal(result, data);
545
+ }
546
+ return changed ? result : data;
547
+ }
548
+ prunePartialStreamArrayImpl({ field, array, boundaries, context, path, }) {
549
+ if (!boundaries)
550
+ return array;
551
+ let changed = false;
552
+ let pruned = [];
553
+ const state = context.streamInfo?.peekArray(path)?.state;
554
+ const length = Math.min(array.length, state?.truncate ? state.streamPosition : Number.MAX_SAFE_INTEGER);
555
+ for (let i = 0; i < length; i++) {
556
+ const item = array[i];
557
+ let prunedItem = item;
558
+ const boundary = boundaries.getChild(i);
559
+ if (boundary?.has(field)) {
560
+ // The presence of streamInfo determines how we truncate partial
561
+ // stream arrays. Stream info is only given to cache.diff during
562
+ // in-flight requests so we want keep items in the array equal to the
563
+ // total that have streamed in (this is represented by streamPosition
564
+ // above). For all other cache reads, partial stream boundaries are
565
+ // pruned back to an empty array.
566
+ if (state) {
567
+ state.truncate = true;
568
+ pruned = pruned.slice(0, state.streamPosition);
569
+ }
570
+ else {
571
+ pruned = [];
572
+ }
573
+ break;
574
+ }
575
+ if (Array.isArray(item)) {
576
+ prunedItem = this.prunePartialStreamArray({
577
+ field,
578
+ array: item,
579
+ boundaries: boundaries.getChild(i),
580
+ context,
581
+ path: path.concat(i),
582
+ });
583
+ }
584
+ else if (field.selectionSet) {
585
+ prunedItem = this.prunePartialBoundaries({
586
+ data: item,
587
+ selectionSet: field.selectionSet,
588
+ boundaries: boundaries.getChild(i),
589
+ context,
590
+ path: path.concat(i),
591
+ });
592
+ }
593
+ pruned.push(prunedItem);
594
+ changed ||= prunedItem !== item;
595
+ }
596
+ changed ||= pruned.length !== array.length;
597
+ return changed ? pruned : array;
598
+ }
269
599
  }
270
600
  function firstMissing(tree) {
271
601
  try {
@@ -295,4 +625,85 @@ function assertSelectionSetForIdValue(store, field, fieldValue) {
295
625
  });
296
626
  }
297
627
  }
628
+ // We deliberately leave `returnPartialData` out of `executeSelectionSet`'s
629
+ // cache key. `isFresh` runs during writes and cannot provide a reliable value
630
+ // for this option, so including it would prevent a reliable cache hit.
631
+ //
632
+ // When `returnPartialData` is false, `diffQueryAgainstStore` prunes data from
633
+ // partial @defer boundaries after reading the cached result. `PartialBoundaries`
634
+ // records the selection paths needed for that pass, including empty nodes along
635
+ // a path. Overlapping non-deferred selections must still be rebuilt so fields
636
+ // contributed only by a partial deferred sibling are removed. The prune pass
637
+ // can then skip unrelated result branches.
638
+ class PartialBoundaries {
639
+ selections = new Set();
640
+ children = new Map();
641
+ add(selection) {
642
+ this.selections.add(selection);
643
+ return this;
644
+ }
645
+ has(selection) {
646
+ return this.selections.has(selection);
647
+ }
648
+ hasSelections() {
649
+ return this.selections.size > 0;
650
+ }
651
+ getChild(key) {
652
+ return this.children.get(key);
653
+ }
654
+ clone() {
655
+ return new PartialBoundaries().merge(this);
656
+ }
657
+ set(key, boundary) {
658
+ const child = this.getChild(key);
659
+ this.children.set(key,
660
+ // Create a new PartialBoundaries instance to avoid mutating any cached
661
+ // execResult partialBoundaries objects
662
+ child ? child.clone().merge(boundary) : boundary);
663
+ }
664
+ merge(boundaries) {
665
+ boundaries.selections.forEach((selection) => this.add(selection));
666
+ boundaries.children.forEach((child, key) => this.set(key, child));
667
+ return this;
668
+ }
669
+ }
670
+ // Describes the data state transitions that change the running state when it's
671
+ // combined with the next data state. Omitted object values represent
672
+ // "impossible" merges where the data state should remain the same.
673
+ const DATA_STATE_MERGES = {
674
+ empty: {
675
+ complete: "partial",
676
+ deferPartial: "partial",
677
+ streaming: "partial",
678
+ streamPartial: "partial",
679
+ },
680
+ deferPartial: {
681
+ empty: "partial",
682
+ },
683
+ streamPartial: {
684
+ deferPartial: "deferPartial",
685
+ empty: "partial",
686
+ },
687
+ streaming: {
688
+ deferPartial: "deferPartial",
689
+ empty: "partial",
690
+ streamPartial: "streamPartial",
691
+ },
692
+ complete: {
693
+ deferPartial: "deferPartial",
694
+ streaming: "streaming",
695
+ streamPartial: "streamPartial",
696
+ empty: "partial",
697
+ },
698
+ partial: {},
699
+ };
700
+ function mergeDataState(current, next) {
701
+ if (next === "partial") {
702
+ return "partial";
703
+ }
704
+ if (!current || current === next) {
705
+ return next;
706
+ }
707
+ return DATA_STATE_MERGES[current][next] || current;
708
+ }
298
709
  //# sourceMappingURL=readFromStore.js.map