relay-runtime 20.1.1 → 21.0.0

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 (262) hide show
  1. package/experimental.js +1 -1
  2. package/experimental.js.flow +8 -8
  3. package/handlers/connection/ConnectionHandler.js.flow +5 -5
  4. package/handlers/connection/ConnectionInterface.js.flow +1 -1
  5. package/index.js +1 -1
  6. package/index.js.flow +125 -62
  7. package/lib/experimental.js +3 -3
  8. package/lib/index.js +105 -57
  9. package/lib/multi-actor-environment/ActorIdentifier.js +2 -2
  10. package/lib/multi-actor-environment/MultiActorEnvironment.js +3 -1
  11. package/lib/mutations/commitMutation.js +8 -8
  12. package/lib/mutations/validateMutation.js +4 -4
  13. package/lib/query/GraphQLTag.js +3 -3
  14. package/lib/query/fetchQuery.js +15 -3
  15. package/lib/store/DataChecker.js +38 -4
  16. package/lib/store/NormalizationEngine.js +373 -0
  17. package/lib/store/OperationExecutor.js +172 -113
  18. package/lib/store/RelayConcreteVariables.js +1 -1
  19. package/lib/store/RelayErrorTrie.js +2 -2
  20. package/lib/store/RelayExperimentalGraphResponseTransform.js +8 -8
  21. package/lib/store/RelayModernEnvironment.js +26 -19
  22. package/lib/store/RelayModernRecord.js +18 -8
  23. package/lib/store/RelayModernSelector.js +9 -9
  24. package/lib/store/RelayModernStore.js +152 -43
  25. package/lib/store/RelayPublishQueue.js +1 -1
  26. package/lib/store/RelayReader.js +76 -38
  27. package/lib/store/RelayRecordSource.js +6 -0
  28. package/lib/store/RelayReferenceMarker.js +2 -1
  29. package/lib/store/RelayResponseNormalizer.js +88 -55
  30. package/lib/store/RelayStoreSubscriptions.js +34 -10
  31. package/lib/store/RelayStoreUtils.js +8 -1
  32. package/lib/store/ResolverFragments.js +2 -2
  33. package/lib/store/live-resolvers/LiveResolverCache.js +25 -9
  34. package/lib/store/observeFragmentExperimental.js +17 -1
  35. package/lib/store/observeQueryExperimental.js +2 -2
  36. package/lib/subscription/requestSubscription.js +3 -3
  37. package/lib/util/RelayError.js +3 -0
  38. package/lib/util/RelayFeatureFlags.js +6 -2
  39. package/lib/util/RelayReplaySubject.js +4 -4
  40. package/lib/util/handlePotentialSnapshotErrors.js +2 -2
  41. package/lib/util/stableCopy.js +2 -2
  42. package/llm-docs/api-reference/entrypoint-apis/entrypoint-container.mdx +38 -0
  43. package/llm-docs/api-reference/entrypoint-apis/load-entrypoint.mdx +77 -0
  44. package/llm-docs/api-reference/entrypoint-apis/use-entrypoint-loader.mdx +99 -0
  45. package/llm-docs/api-reference/graphql/graphql-directives.mdx +378 -0
  46. package/llm-docs/api-reference/hooks/_use-lazy-load-query-extra.mdx +16 -0
  47. package/llm-docs/api-reference/hooks/load-query.mdx +84 -0
  48. package/llm-docs/api-reference/hooks/relay-environment-provider.mdx +78 -0
  49. package/llm-docs/api-reference/hooks/use-client-query.mdx +65 -0
  50. package/llm-docs/api-reference/hooks/use-fragment.mdx +69 -0
  51. package/llm-docs/api-reference/hooks/use-lazy-load-query.mdx +62 -0
  52. package/llm-docs/api-reference/hooks/use-mutation.mdx +94 -0
  53. package/llm-docs/api-reference/hooks/use-pagination-fragment.mdx +166 -0
  54. package/llm-docs/api-reference/hooks/use-prefetchable-forward-pagination-fragment.mdx +134 -0
  55. package/llm-docs/api-reference/hooks/use-preloaded-query.mdx +84 -0
  56. package/llm-docs/api-reference/hooks/use-query-loader.mdx +95 -0
  57. package/llm-docs/api-reference/hooks/use-refetchable-fragment.mdx +122 -0
  58. package/llm-docs/api-reference/hooks/use-relay-environment.mdx +37 -0
  59. package/llm-docs/api-reference/hooks/use-subscription.mdx +66 -0
  60. package/llm-docs/api-reference/relay-resolvers/docblock-format.mdx +321 -0
  61. package/llm-docs/api-reference/relay-resolvers/runtime-functions.mdx +94 -0
  62. package/llm-docs/api-reference/relay-runtime/commit-mutation.mdx +65 -0
  63. package/llm-docs/api-reference/relay-runtime/fetch-query.mdx +113 -0
  64. package/llm-docs/api-reference/relay-runtime/field-logger.mdx +170 -0
  65. package/llm-docs/api-reference/relay-runtime/observe-fragment.mdx +92 -0
  66. package/llm-docs/api-reference/relay-runtime/relay-environment.mdx +53 -0
  67. package/llm-docs/api-reference/relay-runtime/request-subscription.mdx +54 -0
  68. package/llm-docs/api-reference/relay-runtime/runtime-configuration.mdx +52 -0
  69. package/llm-docs/api-reference/relay-runtime/store.mdx +734 -0
  70. package/llm-docs/api-reference/relay-runtime/wait-for-fragment-data.mdx +89 -0
  71. package/llm-docs/api-reference/types/CacheConfig.mdx +8 -0
  72. package/llm-docs/api-reference/types/Disposable.mdx +4 -0
  73. package/llm-docs/api-reference/types/GraphQLSubscriptionConfig.mdx +17 -0
  74. package/llm-docs/api-reference/types/MutationConfig.mdx +31 -0
  75. package/llm-docs/api-reference/types/SelectorStoreUpdater.mdx +6 -0
  76. package/llm-docs/api-reference/types/UploadableMap.mdx +3 -0
  77. package/llm-docs/community/learning-resources.mdx +64 -0
  78. package/llm-docs/debugging/declarative-mutation-directives.mdx +34 -0
  79. package/llm-docs/debugging/disallowed-id-types-error.mdx +43 -0
  80. package/llm-docs/debugging/inconsistent-typename-error.mdx +47 -0
  81. package/llm-docs/debugging/relay-devtools.mdx +73 -0
  82. package/llm-docs/debugging/why-null.mdx +116 -0
  83. package/llm-docs/editor-support.mdx +55 -0
  84. package/llm-docs/error-reference/unknown-field.mdx +36 -0
  85. package/llm-docs/getting-started/babel-plugin.mdx +31 -0
  86. package/llm-docs/getting-started/compiler-config.mdx +25 -0
  87. package/llm-docs/getting-started/compiler.mdx +82 -0
  88. package/llm-docs/getting-started/lint-rules.mdx +87 -0
  89. package/llm-docs/getting-started/production.mdx +30 -0
  90. package/llm-docs/getting-started/quick-start.mdx +213 -0
  91. package/llm-docs/glossary/glossary.mdx +1040 -0
  92. package/llm-docs/guided-tour/list-data/advanced-pagination.mdx +157 -0
  93. package/llm-docs/guided-tour/list-data/connections.mdx +81 -0
  94. package/llm-docs/guided-tour/list-data/pagination.mdx +193 -0
  95. package/llm-docs/guided-tour/list-data/rendering-connections.mdx +112 -0
  96. package/llm-docs/guided-tour/list-data/streaming-pagination.mdx +87 -0
  97. package/llm-docs/guided-tour/managing-data-outside-react/retaining-queries.mdx +51 -0
  98. package/llm-docs/guided-tour/refetching/refetching-queries-with-different-data.mdx +337 -0
  99. package/llm-docs/guided-tour/refetching/refreshing-queries.mdx +350 -0
  100. package/llm-docs/guided-tour/rendering/environment.mdx +59 -0
  101. package/llm-docs/guided-tour/rendering/error-states.mdx +295 -0
  102. package/llm-docs/guided-tour/rendering/fragments.mdx +354 -0
  103. package/llm-docs/guided-tour/rendering/loading-states.mdx +245 -0
  104. package/llm-docs/guided-tour/rendering/queries.mdx +261 -0
  105. package/llm-docs/guided-tour/rendering/variables.mdx +233 -0
  106. package/llm-docs/guided-tour/reusing-cached-data/fetch-policies.mdx +56 -0
  107. package/llm-docs/guided-tour/reusing-cached-data/filling-in-missing-data.mdx +102 -0
  108. package/llm-docs/guided-tour/reusing-cached-data/introduction.mdx +22 -0
  109. package/llm-docs/guided-tour/reusing-cached-data/presence-of-data.mdx +93 -0
  110. package/llm-docs/guided-tour/reusing-cached-data/rendering-partially-cached-data.mdx +175 -0
  111. package/llm-docs/guided-tour/reusing-cached-data/staleness-of-data.mdx +116 -0
  112. package/llm-docs/guided-tour/updating-data/client-only-data.mdx +115 -0
  113. package/llm-docs/guided-tour/updating-data/graphql-mutations.mdx +334 -0
  114. package/llm-docs/guided-tour/updating-data/graphql-subscriptions.mdx +279 -0
  115. package/llm-docs/guided-tour/updating-data/imperatively-modifying-linked-fields.mdx +511 -0
  116. package/llm-docs/guided-tour/updating-data/imperatively-modifying-store-data-legacy.mdx +142 -0
  117. package/llm-docs/guided-tour/updating-data/imperatively-modifying-store-data.mdx +275 -0
  118. package/llm-docs/guided-tour/updating-data/introduction.mdx +25 -0
  119. package/llm-docs/guided-tour/updating-data/local-data-updates.mdx +71 -0
  120. package/llm-docs/guided-tour/updating-data/typesafe-updaters-faq.mdx +83 -0
  121. package/llm-docs/guided-tour/updating-data/updating-connections.mdx +592 -0
  122. package/llm-docs/guides/alias-directive.mdx +160 -0
  123. package/llm-docs/guides/catch-directive.mdx +167 -0
  124. package/llm-docs/guides/client-schema-extensions.mdx +208 -0
  125. package/llm-docs/guides/codemods.mdx +66 -0
  126. package/llm-docs/guides/data-driven-dependencies/client-3d.mdx +255 -0
  127. package/llm-docs/guides/data-driven-dependencies/configuration.mdx +127 -0
  128. package/llm-docs/guides/data-driven-dependencies/introduction.mdx +39 -0
  129. package/llm-docs/guides/data-driven-dependencies/server-3d.mdx +664 -0
  130. package/llm-docs/guides/document-comparison.mdx +106 -0
  131. package/llm-docs/guides/graphql-server-specification.mdx +453 -0
  132. package/llm-docs/guides/network-layer.mdx +69 -0
  133. package/llm-docs/guides/persisted-queries.mdx +328 -0
  134. package/llm-docs/guides/relay-resolvers/context.mdx +99 -0
  135. package/llm-docs/guides/relay-resolvers/defining-fields.mdx +151 -0
  136. package/llm-docs/guides/relay-resolvers/defining-types.mdx +164 -0
  137. package/llm-docs/guides/relay-resolvers/deprecated.mdx +27 -0
  138. package/llm-docs/guides/relay-resolvers/derived-fields.mdx +127 -0
  139. package/llm-docs/guides/relay-resolvers/descriptions.mdx +44 -0
  140. package/llm-docs/guides/relay-resolvers/enabling.mdx +41 -0
  141. package/llm-docs/guides/relay-resolvers/errors.mdx +64 -0
  142. package/llm-docs/guides/relay-resolvers/field-arguments.mdx +63 -0
  143. package/llm-docs/guides/relay-resolvers/introduction.mdx +62 -0
  144. package/llm-docs/guides/relay-resolvers/limitations.mdx +30 -0
  145. package/llm-docs/guides/relay-resolvers/live-fields.mdx +164 -0
  146. package/llm-docs/guides/relay-resolvers/return-types.mdx +161 -0
  147. package/llm-docs/guides/relay-resolvers/suspense.mdx +41 -0
  148. package/llm-docs/guides/required-directive.mdx +240 -0
  149. package/llm-docs/guides/semantic-nullability.mdx +93 -0
  150. package/llm-docs/guides/testing-relay-components.mdx +642 -0
  151. package/llm-docs/guides/testing-relay-with-preloaded-queries.mdx +160 -0
  152. package/llm-docs/guides/throw-on-field-error-directive.mdx +58 -0
  153. package/llm-docs/guides/type-emission.mdx +414 -0
  154. package/llm-docs/home.mdx +32 -0
  155. package/llm-docs/principles-and-architecture/architecture-overview.mdx +24 -0
  156. package/llm-docs/principles-and-architecture/compiler-architecture.mdx +106 -0
  157. package/llm-docs/principles-and-architecture/runtime-architecture.mdx +249 -0
  158. package/llm-docs/principles-and-architecture/thinking-in-graphql.mdx +309 -0
  159. package/llm-docs/principles-and-architecture/thinking-in-relay.mdx +104 -0
  160. package/llm-docs/principles-and-architecture/videos.mdx +50 -0
  161. package/llm-docs/tutorial/arrays-lists.mdx +126 -0
  162. package/llm-docs/tutorial/fragments-1.mdx +487 -0
  163. package/llm-docs/tutorial/graphql.mdx +172 -0
  164. package/llm-docs/tutorial/interfaces-polymorphism.mdx +161 -0
  165. package/llm-docs/tutorial/intro.mdx +58 -0
  166. package/llm-docs/tutorial/mutations-updates.mdx +624 -0
  167. package/llm-docs/tutorial/organizing-mutations-queries-and-subscriptions.mdx +13 -0
  168. package/llm-docs/tutorial/queries-1.mdx +267 -0
  169. package/llm-docs/tutorial/queries-2.mdx +389 -0
  170. package/llm-docs/tutorial/refetchable-fragments.mdx +352 -0
  171. package/multi-actor-environment/ActorIdentifier.js.flow +2 -2
  172. package/multi-actor-environment/ActorSpecificEnvironment.js.flow +7 -7
  173. package/multi-actor-environment/ActorUtils.js.flow +1 -1
  174. package/multi-actor-environment/MultiActorEnvironment.js.flow +12 -8
  175. package/multi-actor-environment/MultiActorEnvironmentTypes.js.flow +3 -3
  176. package/mutations/RelayDeclarativeMutationConfig.js.flow +9 -9
  177. package/mutations/RelayRecordProxy.js.flow +8 -11
  178. package/mutations/RelayRecordSourceMutator.js.flow +4 -4
  179. package/mutations/RelayRecordSourceProxy.js.flow +4 -4
  180. package/mutations/RelayRecordSourceSelectorProxy.js.flow +6 -6
  181. package/mutations/applyOptimisticMutation.js.flow +2 -2
  182. package/mutations/commitMutation.js.flow +20 -16
  183. package/mutations/createUpdatableProxy.js.flow +19 -19
  184. package/mutations/readUpdatableFragment.js.flow +3 -3
  185. package/mutations/readUpdatableQuery.js.flow +3 -3
  186. package/mutations/validateMutation.js.flow +7 -7
  187. package/network/RelayNetworkTypes.js.flow +4 -4
  188. package/network/RelayObservable.js.flow +16 -14
  189. package/network/RelayQueryResponseCache.js.flow +3 -3
  190. package/network/wrapNetworkWithLogObserver.js.flow +1 -1
  191. package/package.json +2 -1
  192. package/query/GraphQLTag.js.flow +22 -10
  193. package/query/fetchQuery.js.flow +23 -10
  194. package/query/fetchQuery_DEPRECATED.js.flow +1 -1
  195. package/store/DataChecker.js.flow +43 -9
  196. package/store/NormalizationEngine.js.flow +779 -0
  197. package/store/OperationExecutor.js.flow +173 -70
  198. package/store/RelayConcreteVariables.js.flow +5 -5
  199. package/store/RelayErrorTrie.js.flow +12 -12
  200. package/store/RelayExperimentalGraphResponseHandler.js.flow +3 -3
  201. package/store/RelayExperimentalGraphResponseTransform.js.flow +10 -10
  202. package/store/RelayModernEnvironment.js.flow +41 -26
  203. package/store/RelayModernFragmentSpecResolver.js.flow +1 -1
  204. package/store/RelayModernOperationDescriptor.js.flow +1 -1
  205. package/store/RelayModernRecord.js.flow +44 -20
  206. package/store/RelayModernSelector.js.flow +21 -21
  207. package/store/RelayModernStore.js.flow +219 -58
  208. package/store/RelayOperationTracker.js.flow +2 -2
  209. package/store/RelayOptimisticRecordSource.js.flow +2 -2
  210. package/store/RelayPublishQueue.js.flow +21 -12
  211. package/store/RelayReader.js.flow +129 -57
  212. package/store/RelayRecordSource.js.flow +10 -0
  213. package/store/RelayRecordState.js.flow +1 -1
  214. package/store/RelayReferenceMarker.js.flow +5 -4
  215. package/store/RelayResponseNormalizer.js.flow +125 -57
  216. package/store/RelayStoreSubscriptions.js.flow +52 -8
  217. package/store/RelayStoreTypes.js.flow +153 -64
  218. package/store/RelayStoreUtils.js.flow +15 -7
  219. package/store/ResolverCache.js.flow +2 -2
  220. package/store/ResolverFragments.js.flow +12 -12
  221. package/store/StoreInspector.js.flow +6 -7
  222. package/store/cloneRelayHandleSourceField.js.flow +1 -1
  223. package/store/cloneRelayScalarHandleSourceField.js.flow +1 -1
  224. package/store/createRelayContext.js.flow +1 -1
  225. package/store/createRelayLoggingContext.js.flow +4 -4
  226. package/store/defaultGetDataID.js.flow +2 -2
  227. package/store/isRelayModernEnvironment.js.flow +4 -2
  228. package/store/live-resolvers/LiveResolverCache.js.flow +55 -20
  229. package/store/live-resolvers/LiveResolverSuspenseSentinel.js.flow +3 -3
  230. package/store/live-resolvers/getOutputTypeRecordIDs.js.flow +1 -1
  231. package/store/live-resolvers/isLiveStateValue.js.flow +2 -2
  232. package/store/live-resolvers/resolverDataInjector.js.flow +8 -5
  233. package/store/observeFragmentExperimental.js.flow +49 -20
  234. package/store/observeQueryExperimental.js.flow +5 -5
  235. package/store/readInlineData.js.flow +4 -4
  236. package/store/waitForFragmentExperimental.js.flow +3 -3
  237. package/subscription/requestSubscription.js.flow +7 -7
  238. package/util/NormalizationNode.js.flow +34 -32
  239. package/util/ReaderNode.js.flow +32 -30
  240. package/util/RelayConcreteNode.js.flow +5 -5
  241. package/util/RelayError.js.flow +4 -1
  242. package/util/RelayFeatureFlags.js.flow +21 -1
  243. package/util/RelayProfiler.js.flow +1 -1
  244. package/util/RelayReplaySubject.js.flow +3 -3
  245. package/util/RelayRuntimeTypes.js.flow +11 -11
  246. package/util/createPayloadFor3DField.js.flow +9 -5
  247. package/util/deepFreeze.js.flow +2 -2
  248. package/util/getFragmentIdentifier.js.flow +1 -1
  249. package/util/getPaginationMetadata.js.flow +1 -1
  250. package/util/getPaginationVariables.js.flow +1 -1
  251. package/util/getPendingOperationsForFragment.js.flow +2 -2
  252. package/util/getRefetchMetadata.js.flow +6 -5
  253. package/util/getValueAtPath.js.flow +3 -3
  254. package/util/handlePotentialSnapshotErrors.js.flow +5 -5
  255. package/util/isEmptyObject.js.flow +1 -1
  256. package/util/isPromise.js.flow +2 -2
  257. package/util/isScalarAndEqual.js.flow +1 -1
  258. package/util/recycleNodesInto.js.flow +2 -2
  259. package/util/registerEnvironmentWithDevTools.js.flow +1 -1
  260. package/util/shallowFreeze.js.flow +1 -1
  261. package/util/stableCopy.js.flow +5 -5
  262. package/util/withProvidedVariables.js.flow +14 -10
@@ -0,0 +1,779 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ * @oncall relay
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import type {GraphQLResponseWithData} from '../network/RelayNetworkTypes';
15
+ import type {
16
+ NormalizationLinkedField,
17
+ NormalizationOperation,
18
+ NormalizationRootNode,
19
+ } from '../util/NormalizationNode';
20
+ import type {Variables} from '../util/RelayRuntimeTypes';
21
+ import type {NormalizationOptions} from './RelayResponseNormalizer';
22
+ import type {
23
+ DeferPlaceholder,
24
+ FollowupPayload,
25
+ HandleFieldPayload,
26
+ IncrementalDataPlaceholder,
27
+ ModuleImportPayload,
28
+ MutableRecordSource,
29
+ NormalizationSelector,
30
+ NormalizeResponseFunction,
31
+ OperationLoader,
32
+ Record,
33
+ RecordSourceProxy,
34
+ RelayResponsePayload,
35
+ StreamPlaceholder,
36
+ } from './RelayStoreTypes';
37
+
38
+ const {stableCopy} = require('../util/stableCopy');
39
+ const {generateClientID} = require('./ClientID');
40
+ const defaultGetDataID = require('./defaultGetDataID');
41
+ const RelayModernRecord = require('./RelayModernRecord');
42
+ const {createNormalizationSelector} = require('./RelayModernSelector');
43
+ const {ROOT_ID, ROOT_TYPE, getStorageKey} = require('./RelayStoreUtils');
44
+
45
+ function err(message: string): Error {
46
+ const e = new Error(message);
47
+ void e.stack;
48
+ return e;
49
+ }
50
+
51
+ function stableStringify(value: unknown): string {
52
+ return JSON.stringify(stableCopy(value)) ?? '';
53
+ }
54
+
55
+ export type NormalizationResult = Readonly<{
56
+ payloads: ReadonlyArray<RelayResponsePayload>,
57
+ // Each pending module resolves to its own NormalizationResult so the caller
58
+ // can recursively drain nested incremental data (extra payloads + further
59
+ // pending modules) produced by an async @module load. Mirrors the recursion
60
+ // that OperationExecutor performs synchronously via `_processPayloadFollowups`.
61
+ pendingModules: ReadonlyArray<Promise<NormalizationResult>>,
62
+ }>;
63
+
64
+ type Config = Readonly<{
65
+ getDataID?: (fieldValue: {+[string]: unknown}, typeName: string) => unknown,
66
+ normalizeResponse: NormalizeResponseFunction,
67
+ operation: NormalizationOperation,
68
+ operationLoader?: ?OperationLoader,
69
+ treatMissingFieldsAsNull?: boolean,
70
+ variables: Variables,
71
+ }>;
72
+
73
+ type ParentEntry = {
74
+ fieldPayloads: Array<HandleFieldPayload>,
75
+ record: Record,
76
+ };
77
+
78
+ /**
79
+ * Per-request normalization engine. Normalizes raw server responses and
80
+ * returns pre-normalized RelayResponsePayload objects (with isPreNormalized:
81
+ * true) that OperationExecutor can commit directly to the store, bypassing
82
+ * the normalization pass it would otherwise perform.
83
+ *
84
+ * Handles initial responses, @defer chunks, @stream items, and @module
85
+ * followups. Tracks per-request state for incremental delivery including
86
+ * placeholder registration, response buffering, and parent record caching.
87
+ *
88
+ * Returns results synchronously via arrays. Async @module loads are returned
89
+ * as Promises — the caller bridges them to the sink.
90
+ */
91
+ class NormalizationEngine {
92
+ _normalizeResponse: NormalizeResponseFunction;
93
+ _operationLoader: ?OperationLoader;
94
+ _options: NormalizationOptions;
95
+ _rootSelector: NormalizationSelector;
96
+ _useExecTimeResolvers: boolean;
97
+
98
+ // Per-request incremental delivery state
99
+ _placeholders: Map<string, IncrementalDataPlaceholder>;
100
+ _bufferedResponses: Map<string, Array<GraphQLResponseWithData>>;
101
+ _parentRecords: Map<string, ParentEntry>;
102
+ _serverComplete: boolean;
103
+
104
+ constructor(config: Config) {
105
+ this._normalizeResponse = config.normalizeResponse;
106
+ this._operationLoader = config.operationLoader ?? null;
107
+ this._rootSelector = {
108
+ dataID: ROOT_ID,
109
+ node: config.operation,
110
+ variables: config.variables,
111
+ };
112
+ this._options = {
113
+ deferDeduplicatedFields: false,
114
+ getDataID: config.getDataID ?? defaultGetDataID,
115
+ log: null,
116
+ path: [],
117
+ treatMissingFieldsAsNull: config.treatMissingFieldsAsNull ?? false,
118
+ };
119
+ this._useExecTimeResolvers =
120
+ config.operation.use_exec_time_resolvers ??
121
+ config.operation.exec_time_resolvers_enabled_provider?.get() === true ??
122
+ false;
123
+ this._placeholders = new Map();
124
+ this._bufferedResponses = new Map();
125
+ this._parentRecords = new Map();
126
+ this._serverComplete = false;
127
+ }
128
+
129
+ /**
130
+ * Process an initial (non-incremental) server response. Normalizes the
131
+ * response, registers any incremental placeholders, processes @module
132
+ * followups, and flushes buffered responses.
133
+ *
134
+ * Returns an array of payloads (the primary result plus any flushed
135
+ * buffered responses) and an array of Promises for async @module loads.
136
+ */
137
+ processResponse(response: GraphQLResponseWithData): NormalizationResult {
138
+ const payload = this._normalizeResponse(
139
+ response,
140
+ this._rootSelector,
141
+ ROOT_TYPE,
142
+ this._options,
143
+ this._useExecTimeResolvers,
144
+ );
145
+
146
+ const extraPayloads: Array<RelayResponsePayload> = [];
147
+ const pendingModules: Array<Promise<NormalizationResult>> = [];
148
+
149
+ // Register incremental placeholders and flush any buffered responses
150
+ if (
151
+ payload.incrementalPlaceholders != null &&
152
+ payload.incrementalPlaceholders.length > 0
153
+ ) {
154
+ this._registerPlaceholders(
155
+ payload.incrementalPlaceholders,
156
+ payload.source,
157
+ payload.fieldPayloads,
158
+ extraPayloads,
159
+ pendingModules,
160
+ );
161
+ }
162
+
163
+ // Process module followups
164
+ if (
165
+ payload.followupPayloads != null &&
166
+ payload.followupPayloads.length > 0
167
+ ) {
168
+ this._processFollowups(
169
+ payload.followupPayloads,
170
+ extraPayloads,
171
+ pendingModules,
172
+ );
173
+ }
174
+
175
+ const primaryPayload: RelayResponsePayload = {
176
+ ...payload,
177
+ followupPayloads: null,
178
+ incrementalPlaceholders: null,
179
+ isFinal: this._computeIsFinal(payload.isFinal),
180
+ isPreNormalized: true,
181
+ };
182
+
183
+ return {
184
+ payloads: [primaryPayload, ...extraPayloads],
185
+ pendingModules,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Process an incremental response (@defer chunk or @stream item).
191
+ * Matches the response to a registered placeholder by label + path.
192
+ *
193
+ * Returns null if the response was buffered (no placeholder yet).
194
+ * Otherwise returns payloads array and pending module Promises.
195
+ */
196
+ processIncrementalResponse(
197
+ response: GraphQLResponseWithData,
198
+ ): ?NormalizationResult {
199
+ const label: ?string = response.label;
200
+ const path: ?ReadonlyArray<string | number> = response.path;
201
+
202
+ if (label == null || path == null) {
203
+ throw err(
204
+ 'NormalizationEngine: Expected incremental response to have ' +
205
+ '`label` and `path` properties.',
206
+ );
207
+ }
208
+
209
+ const isDefer = label.indexOf('$defer$') !== -1;
210
+ const pathKey = isDefer
211
+ ? path.map(String).join('.')
212
+ : path.slice(0, -2).map(String).join('.');
213
+ const key = makeKey(label, pathKey);
214
+
215
+ const placeholder = this._placeholders.get(key);
216
+
217
+ if (placeholder == null) {
218
+ // Buffer: response arrived before placeholder was registered
219
+ let buffer = this._bufferedResponses.get(key);
220
+ if (buffer == null) {
221
+ buffer = [];
222
+ this._bufferedResponses.set(key, buffer);
223
+ }
224
+ buffer.push(response);
225
+ return null;
226
+ }
227
+
228
+ if (placeholder.kind === 'defer') {
229
+ return this._processDefer(response, path, placeholder);
230
+ } else {
231
+ return this._processStream(response, path, placeholder);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Mark the server stream as complete for isFinal computation.
237
+ */
238
+ setServerComplete(): void {
239
+ this._serverComplete = true;
240
+ }
241
+
242
+ /**
243
+ * Whether the server is complete AND all incremental data has been received.
244
+ */
245
+ isFinal(): boolean {
246
+ return (
247
+ this._serverComplete &&
248
+ this._bufferedResponses.size === 0 &&
249
+ this._placeholders.size === 0
250
+ );
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Private: @defer handling
255
+ // ---------------------------------------------------------------------------
256
+
257
+ _processDefer(
258
+ response: GraphQLResponseWithData,
259
+ _path: ReadonlyArray<unknown>,
260
+ placeholder: DeferPlaceholder,
261
+ ): NormalizationResult {
262
+ const payload = this._normalizeResponse(
263
+ response,
264
+ placeholder.selector,
265
+ placeholder.typeName,
266
+ {
267
+ ...this._options,
268
+ deferDeduplicatedFields: true,
269
+ path: placeholder.path,
270
+ },
271
+ this._useExecTimeResolvers,
272
+ );
273
+
274
+ const extraPayloads: Array<RelayResponsePayload> = [];
275
+ const pendingModules: Array<Promise<NormalizationResult>> = [];
276
+
277
+ // Register nested placeholders (recursive @defer)
278
+ if (
279
+ payload.incrementalPlaceholders != null &&
280
+ payload.incrementalPlaceholders.length > 0
281
+ ) {
282
+ this._registerPlaceholders(
283
+ payload.incrementalPlaceholders,
284
+ payload.source,
285
+ payload.fieldPayloads,
286
+ extraPayloads,
287
+ pendingModules,
288
+ );
289
+ }
290
+
291
+ // Process nested followups (recursive @module)
292
+ if (
293
+ payload.followupPayloads != null &&
294
+ payload.followupPayloads.length > 0
295
+ ) {
296
+ this._processFollowups(
297
+ payload.followupPayloads,
298
+ extraPayloads,
299
+ pendingModules,
300
+ );
301
+ }
302
+
303
+ // Replay handle field payloads from parent
304
+ // TODO(skyyao): emit parent fieldPayloads as a separate payload (with empty
305
+ // source) like OperationExecutor._processDeferResponse so handles run
306
+ // against the just-committed updated parent record.
307
+ const parentID = placeholder.selector.dataID;
308
+ const parentEntry = this._parentRecords.get(parentID);
309
+ let fieldPayloads = payload.fieldPayloads;
310
+ if (parentEntry != null && parentEntry.fieldPayloads.length > 0) {
311
+ fieldPayloads = (fieldPayloads ?? []).concat(parentEntry.fieldPayloads);
312
+ }
313
+
314
+ const primaryPayload: RelayResponsePayload = {
315
+ ...payload,
316
+ fieldPayloads,
317
+ followupPayloads: null,
318
+ incrementalPlaceholders: null,
319
+ isFinal: this._computeIsFinal(payload.isFinal),
320
+ isPreNormalized: true,
321
+ };
322
+
323
+ return {
324
+ payloads: [primaryPayload, ...extraPayloads],
325
+ pendingModules,
326
+ };
327
+ }
328
+
329
+ // ---------------------------------------------------------------------------
330
+ // Private: @stream handling
331
+ // ---------------------------------------------------------------------------
332
+
333
+ _processStream(
334
+ response: GraphQLResponseWithData,
335
+ path: ReadonlyArray<unknown>,
336
+ placeholder: StreamPlaceholder,
337
+ ): NormalizationResult {
338
+ const {node, parentID, variables} = placeholder;
339
+
340
+ // Find the LinkedField where @stream was applied
341
+ const field = node.selections[0];
342
+ if (
343
+ field == null ||
344
+ field.kind !== 'LinkedField' ||
345
+ field.plural !== true
346
+ ) {
347
+ throw err(
348
+ 'NormalizationEngine: Expected @stream to be used on a plural field.',
349
+ );
350
+ }
351
+
352
+ const {
353
+ fieldPayloads,
354
+ itemID,
355
+ itemIndex,
356
+ prevIDs,
357
+ relayPayload,
358
+ storageKey,
359
+ } = this._normalizeStreamItem(
360
+ response,
361
+ parentID,
362
+ field,
363
+ variables,
364
+ path,
365
+ placeholder.path,
366
+ );
367
+
368
+ // Build store updater for concurrent modification detection
369
+ const storeUpdater = (store: RecordSourceProxy) => {
370
+ const currentParentRecord = store.get(parentID);
371
+ if (currentParentRecord == null) {
372
+ return;
373
+ }
374
+ const currentItems = currentParentRecord.getLinkedRecords(storageKey);
375
+ if (currentItems == null) {
376
+ return;
377
+ }
378
+ if (
379
+ currentItems.length !== prevIDs.length ||
380
+ currentItems.some(
381
+ (item, i) => prevIDs[i] !== (item && item.getDataID()),
382
+ )
383
+ ) {
384
+ return; // Concurrent modification -- drop stale stream data
385
+ }
386
+ const nextItems = [...currentItems];
387
+ nextItems[itemIndex] = store.get(itemID);
388
+ currentParentRecord.setLinkedRecords(nextItems, storageKey);
389
+ };
390
+
391
+ // Replay handle field payloads from parent
392
+ // TODO(skyyao): emit parent fieldPayloads as a separate payload (with empty
393
+ // source) like OperationExecutor._processStreamResponse so handles run
394
+ // against the just-committed updated parent record.
395
+ let mergedFieldPayloads = relayPayload.fieldPayloads;
396
+ if (fieldPayloads.length > 0) {
397
+ mergedFieldPayloads = (mergedFieldPayloads ?? []).concat(fieldPayloads);
398
+ }
399
+
400
+ return {
401
+ payloads: [
402
+ {
403
+ ...relayPayload,
404
+ fieldPayloads: mergedFieldPayloads,
405
+ followupPayloads: null,
406
+ incrementalPlaceholders: null,
407
+ isFinal: this._computeIsFinal(relayPayload.isFinal),
408
+ isPreNormalized: true,
409
+ storeUpdater,
410
+ },
411
+ ],
412
+ pendingModules: [],
413
+ };
414
+ }
415
+
416
+ _normalizeStreamItem(
417
+ response: GraphQLResponseWithData,
418
+ parentID: string,
419
+ field: NormalizationLinkedField,
420
+ variables: Variables,
421
+ path: ReadonlyArray<unknown>,
422
+ normalizationPath: ReadonlyArray<string>,
423
+ ): {
424
+ fieldPayloads: Array<HandleFieldPayload>,
425
+ itemID: string,
426
+ itemIndex: number,
427
+ prevIDs: Array<?string>,
428
+ relayPayload: RelayResponsePayload,
429
+ storageKey: string,
430
+ } {
431
+ const {data} = response;
432
+ if (typeof data !== 'object') {
433
+ throw err(
434
+ 'NormalizationEngine: Expected the GraphQL @stream payload `data` ' +
435
+ 'value to be an object.',
436
+ );
437
+ }
438
+ const responseKey = field.alias ?? field.name;
439
+ const storageKey = getStorageKey(field, variables);
440
+
441
+ const parentEntry = this._parentRecords.get(parentID);
442
+ if (parentEntry == null) {
443
+ throw err(
444
+ 'NormalizationEngine: Expected the parent record `' +
445
+ parentID +
446
+ '` for @stream data to exist.',
447
+ );
448
+ }
449
+ const {fieldPayloads, record: parentRecord} = parentEntry;
450
+
451
+ const prevIDs = RelayModernRecord.getLinkedRecordIDs(
452
+ parentRecord,
453
+ storageKey,
454
+ );
455
+ if (prevIDs == null) {
456
+ throw err(
457
+ 'NormalizationEngine: Expected record `' +
458
+ parentID +
459
+ '` to have fetched field `' +
460
+ field.name +
461
+ '` with @stream.',
462
+ );
463
+ }
464
+
465
+ const finalPathEntry = path[path.length - 1];
466
+ const itemIndex = parseInt(finalPathEntry, 10);
467
+ if (itemIndex !== finalPathEntry || itemIndex < 0) {
468
+ throw err(
469
+ 'NormalizationEngine: Expected path for @stream to end in a ' +
470
+ 'positive integer index, got `' +
471
+ String(finalPathEntry) +
472
+ '`',
473
+ );
474
+ }
475
+
476
+ const typeName = field.concreteType ?? (data as $FlowFixMe).__typename;
477
+ if (typeof typeName !== 'string') {
478
+ throw err(
479
+ 'NormalizationEngine: Expected @stream field `' +
480
+ field.name +
481
+ '` to have a __typename.',
482
+ );
483
+ }
484
+
485
+ const getDataID = this._options.getDataID;
486
+ const itemID =
487
+ (typeof getDataID === 'function'
488
+ ? getDataID(data as $FlowFixMe, typeName)
489
+ : null) ??
490
+ prevIDs?.[itemIndex] ??
491
+ generateClientID(parentID, storageKey, itemIndex);
492
+ if (typeof itemID !== 'string') {
493
+ throw err(
494
+ 'NormalizationEngine: Expected id of elements of field `' +
495
+ storageKey +
496
+ '` to be strings.',
497
+ );
498
+ }
499
+
500
+ const selector = createNormalizationSelector(field, itemID, variables);
501
+
502
+ const nextParentRecord = RelayModernRecord.clone(parentRecord);
503
+ const nextIDs = [...prevIDs];
504
+ nextIDs[itemIndex] = itemID;
505
+ RelayModernRecord.setLinkedRecordIDs(nextParentRecord, storageKey, nextIDs);
506
+ this._parentRecords.set(parentID, {
507
+ fieldPayloads,
508
+ record: nextParentRecord,
509
+ });
510
+
511
+ const relayPayload = this._normalizeResponse(
512
+ response,
513
+ selector,
514
+ typeName,
515
+ {
516
+ ...this._options,
517
+ path: [...normalizationPath, responseKey, String(itemIndex)],
518
+ },
519
+ this._useExecTimeResolvers,
520
+ );
521
+
522
+ return {
523
+ fieldPayloads,
524
+ itemID,
525
+ itemIndex,
526
+ prevIDs,
527
+ relayPayload,
528
+ storageKey,
529
+ };
530
+ }
531
+
532
+ // ---------------------------------------------------------------------------
533
+ // Private: placeholder registration
534
+ // ---------------------------------------------------------------------------
535
+
536
+ _registerPlaceholders(
537
+ placeholders: ReadonlyArray<IncrementalDataPlaceholder>,
538
+ source: MutableRecordSource,
539
+ fieldPayloads: ?ReadonlyArray<HandleFieldPayload>,
540
+ outPayloads: Array<RelayResponsePayload>,
541
+ outPendingModules: Array<Promise<NormalizationResult>>,
542
+ ): void {
543
+ for (let i = 0; i < placeholders.length; i++) {
544
+ const placeholder = placeholders[i];
545
+ const {label, path} = placeholder;
546
+ const pathKey = path.map(String).join('.');
547
+ const key = makeKey(label, pathKey);
548
+ this._placeholders.set(key, placeholder);
549
+
550
+ // Cache parent record for @stream concurrent modification detection
551
+ // and for @defer handle field replay
552
+ let parentID: string;
553
+ if (placeholder.kind === 'stream') {
554
+ parentID = placeholder.parentID;
555
+ } else {
556
+ parentID = placeholder.selector.dataID;
557
+ }
558
+
559
+ const parentRecord = source.get(parentID);
560
+ if (parentRecord == null) {
561
+ throw err(
562
+ 'NormalizationEngine: Expected record `' + parentID + '` to exist.',
563
+ );
564
+ }
565
+
566
+ const parentPayloads = (fieldPayloads ?? []).filter(
567
+ (fieldPayload: HandleFieldPayload) => {
568
+ const fieldID = generateClientID(
569
+ fieldPayload.dataID,
570
+ fieldPayload.fieldKey,
571
+ );
572
+ return fieldPayload.dataID === parentID || fieldID === parentID;
573
+ },
574
+ );
575
+
576
+ const previousEntry = this._parentRecords.get(parentID);
577
+ if (previousEntry != null) {
578
+ const nextRecord = RelayModernRecord.update(
579
+ previousEntry.record,
580
+ parentRecord,
581
+ );
582
+ const handlePayloads = new Map<string, HandleFieldPayload>();
583
+ // TODO(skyyao): use cheaper structural key (dataID + fieldKey + handleKey)
584
+ // to skip serializing args/handleArgs; apply the same optimization to
585
+ // OperationExecutor.js:1317.
586
+ for (let j = 0; j < previousEntry.fieldPayloads.length; j++) {
587
+ const p = previousEntry.fieldPayloads[j];
588
+ handlePayloads.set(stableStringify(p), p);
589
+ }
590
+ for (let j = 0; j < parentPayloads.length; j++) {
591
+ const p = parentPayloads[j];
592
+ handlePayloads.set(stableStringify(p), p);
593
+ }
594
+ this._parentRecords.set(parentID, {
595
+ fieldPayloads: Array.from(handlePayloads.values()),
596
+ record: nextRecord,
597
+ });
598
+ } else {
599
+ this._parentRecords.set(parentID, {
600
+ fieldPayloads: parentPayloads,
601
+ record: parentRecord,
602
+ });
603
+ }
604
+
605
+ // Flush any buffered responses that arrived before this placeholder
606
+ const buffered = this._bufferedResponses.get(key);
607
+ if (buffered != null) {
608
+ this._bufferedResponses.delete(key);
609
+ for (let j = 0; j < buffered.length; j++) {
610
+ const resp = buffered[j];
611
+ let result: ?NormalizationResult;
612
+ if (placeholder.kind === 'defer') {
613
+ result = this._processDefer(resp, resp.path ?? [], placeholder);
614
+ } else {
615
+ result = this._processStream(resp, resp.path ?? [], placeholder);
616
+ }
617
+ if (result != null) {
618
+ outPayloads.push(...result.payloads);
619
+ outPendingModules.push(...result.pendingModules);
620
+ }
621
+ }
622
+ }
623
+ }
624
+ }
625
+
626
+ // ---------------------------------------------------------------------------
627
+ // Private: @module followup handling
628
+ // ---------------------------------------------------------------------------
629
+
630
+ _processFollowups(
631
+ followups: ReadonlyArray<FollowupPayload>,
632
+ outPayloads: Array<RelayResponsePayload>,
633
+ outPendingModules: Array<Promise<NormalizationResult>>,
634
+ ): void {
635
+ for (let i = 0; i < followups.length; i++) {
636
+ const followup = followups[i];
637
+ if (followup.kind === 'ModuleImportPayload') {
638
+ this._processModuleImport(followup, outPayloads, outPendingModules);
639
+ }
640
+ }
641
+ }
642
+
643
+ _processModuleImport(
644
+ followup: ModuleImportPayload,
645
+ outPayloads: Array<RelayResponsePayload>,
646
+ outPendingModules: Array<Promise<NormalizationResult>>,
647
+ ): void {
648
+ const operationLoader = this._operationLoader;
649
+ if (operationLoader == null) {
650
+ return;
651
+ }
652
+
653
+ // Try sync first
654
+ const node: ?NormalizationRootNode = operationLoader.get(
655
+ followup.operationReference,
656
+ );
657
+ if (node != null) {
658
+ const result = this._normalizeFollowup(followup, node);
659
+ outPayloads.push(...result.payloads);
660
+ outPendingModules.push(...result.pendingModules);
661
+ return;
662
+ }
663
+
664
+ // Async: return a Promise that resolves to the full NormalizationResult,
665
+ // preserving any nested payloads and further pending modules produced by
666
+ // the followup. The caller drains pendingModules recursively (see
667
+ // ReactiveQueryExecutionNode_EXPERIMENTAL.executeWithNetwork) — this
668
+ // mirrors OperationExecutor's recursive `_processPayloadFollowups`.
669
+ const emptyResult: NormalizationResult = {
670
+ payloads: [],
671
+ pendingModules: [],
672
+ };
673
+ outPendingModules.push(
674
+ operationLoader
675
+ .load(followup.operationReference)
676
+ .then((loadedNode: ?NormalizationRootNode) =>
677
+ loadedNode != null
678
+ ? this._normalizeFollowup(followup, loadedNode)
679
+ : emptyResult,
680
+ )
681
+ // TODO(skyyao): surface async @module load errors instead of silently
682
+ // swallowing — OperationExecutor terminates execution via sink.error().
683
+ .catch((_error: unknown) => emptyResult),
684
+ );
685
+ }
686
+
687
+ _normalizeFollowup(
688
+ followup: ModuleImportPayload,
689
+ node: NormalizationRootNode,
690
+ ): NormalizationResult {
691
+ const operationNode =
692
+ node.kind === 'SplitOperation' ? node : node.operation;
693
+ // TODO(skyyao): for SplitOperation followups, bind variables via
694
+ // getLocalVariables(followup.variables, operationNode.argumentDefinitions,
695
+ // followup.args) like OperationExecutor._normalizeFollowupPayload — current
696
+ // pass-through breaks @module argument binding.
697
+ // TODO(skyyao): propagate is_final extension on the synthetic response when
698
+ // in loading_final state, to enable nested defer/3D processing on
699
+ // non-streaming server mode (see OperationExecutor.js:864-870).
700
+ const selector = createNormalizationSelector(
701
+ operationNode,
702
+ followup.dataID,
703
+ followup.variables,
704
+ );
705
+
706
+ const payload = this._normalizeResponse(
707
+ {data: followup.data} as $FlowFixMe,
708
+ selector,
709
+ followup.typeName,
710
+ {
711
+ ...this._options,
712
+ path: followup.path,
713
+ },
714
+ this._useExecTimeResolvers,
715
+ );
716
+
717
+ const extraPayloads: Array<RelayResponsePayload> = [];
718
+ const pendingModules: Array<Promise<NormalizationResult>> = [];
719
+
720
+ // Register nested placeholders
721
+ if (
722
+ payload.incrementalPlaceholders != null &&
723
+ payload.incrementalPlaceholders.length > 0
724
+ ) {
725
+ this._registerPlaceholders(
726
+ payload.incrementalPlaceholders,
727
+ payload.source,
728
+ payload.fieldPayloads,
729
+ extraPayloads,
730
+ pendingModules,
731
+ );
732
+ }
733
+
734
+ // Process nested followups
735
+ if (
736
+ payload.followupPayloads != null &&
737
+ payload.followupPayloads.length > 0
738
+ ) {
739
+ this._processFollowups(
740
+ payload.followupPayloads,
741
+ extraPayloads,
742
+ pendingModules,
743
+ );
744
+ }
745
+
746
+ const primaryPayload: RelayResponsePayload = {
747
+ ...payload,
748
+ followupPayloads: null,
749
+ incrementalPlaceholders: null,
750
+ isFinal: this._computeIsFinal(payload.isFinal),
751
+ isPreNormalized: true,
752
+ };
753
+
754
+ return {
755
+ payloads: [primaryPayload, ...extraPayloads],
756
+ pendingModules,
757
+ };
758
+ }
759
+
760
+ // ---------------------------------------------------------------------------
761
+ // Private: completion tracking
762
+ // ---------------------------------------------------------------------------
763
+
764
+ _computeIsFinal(serverIsFinal: boolean): boolean {
765
+ return (
766
+ (serverIsFinal || this._serverComplete) &&
767
+ this._bufferedResponses.size === 0
768
+ );
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Build a unique key for matching incremental responses to placeholders.
774
+ */
775
+ function makeKey(label: string, pathKey: string): string {
776
+ return `${label}::${pathKey}`;
777
+ }
778
+
779
+ module.exports = NormalizationEngine;