relay-runtime 11.0.2 → 13.0.0-rc.2

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 (219) hide show
  1. package/README.md +67 -0
  2. package/handlers/RelayDefaultHandlerProvider.js.flow +3 -3
  3. package/handlers/connection/ConnectionHandler.js.flow +9 -18
  4. package/handlers/connection/ConnectionInterface.js.flow +1 -1
  5. package/handlers/connection/MutationHandlers.js.flow +8 -12
  6. package/index.js +1 -1
  7. package/index.js.flow +57 -35
  8. package/lib/handlers/RelayDefaultHandlerProvider.js +1 -1
  9. package/lib/handlers/connection/ConnectionHandler.js +13 -19
  10. package/lib/handlers/connection/ConnectionInterface.js +1 -1
  11. package/lib/handlers/connection/MutationHandlers.js +4 -7
  12. package/lib/index.js +59 -44
  13. package/lib/multi-actor-environment/ActorIdentifier.js +12 -2
  14. package/lib/multi-actor-environment/ActorSpecificEnvironment.js +64 -20
  15. package/lib/multi-actor-environment/ActorUtils.js +27 -0
  16. package/lib/multi-actor-environment/MultiActorEnvironment.js +324 -61
  17. package/lib/multi-actor-environment/MultiActorEnvironmentTypes.js +1 -1
  18. package/lib/multi-actor-environment/index.js +6 -2
  19. package/lib/mutations/RelayDeclarativeMutationConfig.js +5 -2
  20. package/lib/mutations/RelayRecordProxy.js +4 -3
  21. package/lib/mutations/RelayRecordSourceMutator.js +4 -3
  22. package/lib/mutations/RelayRecordSourceProxy.js +13 -5
  23. package/lib/mutations/RelayRecordSourceSelectorProxy.js +19 -6
  24. package/lib/mutations/applyOptimisticMutation.js +7 -7
  25. package/lib/mutations/commitLocalUpdate.js +1 -1
  26. package/lib/mutations/commitMutation.js +15 -11
  27. package/lib/mutations/readUpdatableQuery_EXPERIMENTAL.js +242 -0
  28. package/lib/mutations/validateMutation.js +11 -6
  29. package/lib/network/ConvertToExecuteFunction.js +3 -2
  30. package/lib/network/RelayNetwork.js +4 -3
  31. package/lib/network/RelayNetworkTypes.js +1 -1
  32. package/lib/network/RelayObservable.js +1 -1
  33. package/lib/network/RelayQueryResponseCache.js +22 -6
  34. package/lib/network/wrapNetworkWithLogObserver.js +79 -0
  35. package/lib/query/GraphQLTag.js +3 -2
  36. package/lib/query/PreloadableQueryRegistry.js +1 -1
  37. package/lib/query/fetchQuery.js +7 -6
  38. package/lib/query/fetchQueryInternal.js +1 -1
  39. package/lib/query/fetchQuery_DEPRECATED.js +3 -2
  40. package/lib/store/ClientID.js +8 -2
  41. package/lib/store/DataChecker.js +124 -55
  42. package/lib/store/OperationExecutor.js +489 -215
  43. package/lib/store/RelayConcreteVariables.js +27 -9
  44. package/lib/store/RelayExperimentalGraphResponseHandler.js +153 -0
  45. package/lib/store/RelayExperimentalGraphResponseTransform.js +391 -0
  46. package/lib/store/RelayModernEnvironment.js +100 -120
  47. package/lib/store/RelayModernFragmentSpecResolver.js +53 -27
  48. package/lib/store/RelayModernOperationDescriptor.js +3 -2
  49. package/lib/store/RelayModernRecord.js +48 -13
  50. package/lib/store/RelayModernSelector.js +15 -9
  51. package/lib/store/RelayModernStore.js +56 -23
  52. package/lib/store/RelayOperationTracker.js +34 -24
  53. package/lib/store/RelayOptimisticRecordSource.js +1 -1
  54. package/lib/store/RelayPublishQueue.js +35 -11
  55. package/lib/store/RelayReader.js +257 -72
  56. package/lib/store/RelayRecordSource.js +88 -4
  57. package/lib/store/RelayRecordState.js +1 -1
  58. package/lib/store/RelayReferenceMarker.js +34 -22
  59. package/lib/store/RelayResponseNormalizer.js +172 -96
  60. package/lib/store/RelayStoreReactFlightUtils.js +5 -11
  61. package/lib/store/RelayStoreSubscriptions.js +15 -10
  62. package/lib/store/RelayStoreTypes.js +1 -1
  63. package/lib/store/RelayStoreUtils.js +13 -8
  64. package/lib/store/ResolverCache.js +213 -0
  65. package/lib/store/ResolverFragments.js +10 -6
  66. package/lib/store/StoreInspector.js +1 -1
  67. package/lib/store/TypeID.js +1 -1
  68. package/lib/store/ViewerPattern.js +1 -1
  69. package/lib/store/cloneRelayHandleSourceField.js +6 -5
  70. package/lib/store/cloneRelayScalarHandleSourceField.js +6 -5
  71. package/lib/store/createFragmentSpecResolver.js +1 -1
  72. package/lib/store/createRelayContext.js +5 -3
  73. package/lib/store/defaultGetDataID.js +1 -1
  74. package/lib/store/defaultRequiredFieldLogger.js +1 -1
  75. package/lib/store/hasOverlappingIDs.js +1 -1
  76. package/lib/store/isRelayModernEnvironment.js +1 -1
  77. package/lib/store/normalizeRelayPayload.js +1 -1
  78. package/lib/store/readInlineData.js +7 -3
  79. package/lib/subscription/requestSubscription.js +32 -34
  80. package/lib/util/JSResourceTypes.flow.js +1 -1
  81. package/lib/util/NormalizationNode.js +1 -1
  82. package/lib/util/ReaderNode.js +1 -1
  83. package/lib/util/RelayConcreteNode.js +3 -1
  84. package/lib/util/RelayDefaultHandleKey.js +1 -1
  85. package/lib/util/RelayError.js +1 -1
  86. package/lib/util/RelayFeatureFlags.js +10 -7
  87. package/lib/util/RelayProfiler.js +1 -1
  88. package/lib/util/RelayReplaySubject.js +22 -7
  89. package/lib/util/RelayRuntimeTypes.js +1 -7
  90. package/lib/util/StringInterner.js +71 -0
  91. package/lib/util/createPayloadFor3DField.js +1 -1
  92. package/lib/util/deepFreeze.js +1 -1
  93. package/lib/util/generateID.js +1 -1
  94. package/lib/util/getAllRootVariables.js +29 -0
  95. package/lib/util/getFragmentIdentifier.js +16 -8
  96. package/lib/util/getOperation.js +3 -2
  97. package/lib/util/getPaginationMetadata.js +41 -0
  98. package/lib/util/getPaginationVariables.js +66 -0
  99. package/lib/util/getPendingOperationsForFragment.js +55 -0
  100. package/lib/util/getRefetchMetadata.js +36 -0
  101. package/lib/util/getRelayHandleKey.js +3 -3
  102. package/lib/util/getRequestIdentifier.js +3 -3
  103. package/lib/util/getValueAtPath.js +51 -0
  104. package/lib/util/isEmptyObject.js +2 -2
  105. package/lib/util/isPromise.js +1 -1
  106. package/lib/util/isScalarAndEqual.js +1 -1
  107. package/lib/util/recycleNodesInto.js +1 -1
  108. package/lib/util/registerEnvironmentWithDevTools.js +26 -0
  109. package/lib/util/reportMissingRequiredFields.js +1 -1
  110. package/lib/util/resolveImmediate.js +1 -1
  111. package/lib/util/stableCopy.js +1 -1
  112. package/lib/util/withDuration.js +31 -0
  113. package/multi-actor-environment/ActorIdentifier.js.flow +18 -2
  114. package/multi-actor-environment/ActorSpecificEnvironment.js.flow +94 -58
  115. package/multi-actor-environment/ActorUtils.js.flow +33 -0
  116. package/multi-actor-environment/MultiActorEnvironment.js.flow +366 -93
  117. package/multi-actor-environment/MultiActorEnvironmentTypes.js.flow +88 -23
  118. package/multi-actor-environment/index.js.flow +3 -1
  119. package/mutations/RelayDeclarativeMutationConfig.js.flow +33 -27
  120. package/mutations/RelayRecordProxy.js.flow +5 -6
  121. package/mutations/RelayRecordSourceMutator.js.flow +5 -7
  122. package/mutations/RelayRecordSourceProxy.js.flow +20 -11
  123. package/mutations/RelayRecordSourceSelectorProxy.js.flow +23 -8
  124. package/mutations/applyOptimisticMutation.js.flow +14 -15
  125. package/mutations/commitLocalUpdate.js.flow +2 -2
  126. package/mutations/commitMutation.js.flow +36 -47
  127. package/mutations/readUpdatableQuery_EXPERIMENTAL.js.flow +318 -0
  128. package/mutations/validateMutation.js.flow +27 -17
  129. package/network/ConvertToExecuteFunction.js.flow +3 -3
  130. package/network/RelayNetwork.js.flow +5 -6
  131. package/network/RelayNetworkTypes.js.flow +1 -1
  132. package/network/RelayObservable.js.flow +2 -2
  133. package/network/RelayQueryResponseCache.js.flow +35 -22
  134. package/network/wrapNetworkWithLogObserver.js.flow +99 -0
  135. package/package.json +2 -2
  136. package/query/GraphQLTag.js.flow +11 -11
  137. package/query/PreloadableQueryRegistry.js.flow +5 -3
  138. package/query/fetchQuery.js.flow +19 -19
  139. package/query/fetchQueryInternal.js.flow +7 -10
  140. package/query/fetchQuery_DEPRECATED.js.flow +7 -7
  141. package/relay-runtime.js +2 -2
  142. package/relay-runtime.min.js +2 -2
  143. package/store/ClientID.js.flow +15 -4
  144. package/store/DataChecker.js.flow +142 -60
  145. package/store/OperationExecutor.js.flow +575 -320
  146. package/store/RelayConcreteVariables.js.flow +28 -9
  147. package/store/RelayExperimentalGraphResponseHandler.js.flow +121 -0
  148. package/store/RelayExperimentalGraphResponseTransform.js.flow +470 -0
  149. package/store/RelayModernEnvironment.js.flow +91 -115
  150. package/store/RelayModernFragmentSpecResolver.js.flow +56 -32
  151. package/store/RelayModernOperationDescriptor.js.flow +13 -8
  152. package/store/RelayModernRecord.js.flow +68 -12
  153. package/store/RelayModernSelector.js.flow +25 -15
  154. package/store/RelayModernStore.js.flow +67 -32
  155. package/store/RelayOperationTracker.js.flow +60 -44
  156. package/store/RelayOptimisticRecordSource.js.flow +3 -3
  157. package/store/RelayPublishQueue.js.flow +74 -32
  158. package/store/RelayReader.js.flow +319 -100
  159. package/store/RelayRecordSource.js.flow +73 -7
  160. package/store/RelayRecordState.js.flow +1 -1
  161. package/store/RelayReferenceMarker.js.flow +41 -27
  162. package/store/RelayResponseNormalizer.js.flow +204 -86
  163. package/store/RelayStoreReactFlightUtils.js.flow +5 -12
  164. package/store/RelayStoreSubscriptions.js.flow +20 -12
  165. package/store/RelayStoreTypes.js.flow +200 -41
  166. package/store/RelayStoreUtils.js.flow +25 -12
  167. package/store/ResolverCache.js.flow +249 -0
  168. package/store/ResolverFragments.js.flow +16 -20
  169. package/store/StoreInspector.js.flow +3 -3
  170. package/store/TypeID.js.flow +2 -2
  171. package/store/ViewerPattern.js.flow +3 -3
  172. package/store/cloneRelayHandleSourceField.js.flow +6 -7
  173. package/store/cloneRelayScalarHandleSourceField.js.flow +6 -7
  174. package/store/createFragmentSpecResolver.js.flow +4 -5
  175. package/store/createRelayContext.js.flow +4 -4
  176. package/store/defaultGetDataID.js.flow +1 -1
  177. package/store/defaultRequiredFieldLogger.js.flow +1 -1
  178. package/store/hasOverlappingIDs.js.flow +1 -1
  179. package/store/isRelayModernEnvironment.js.flow +1 -1
  180. package/store/normalizeRelayPayload.js.flow +7 -8
  181. package/store/readInlineData.js.flow +8 -9
  182. package/subscription/requestSubscription.js.flow +55 -51
  183. package/util/JSResourceTypes.flow.js.flow +1 -1
  184. package/util/NormalizationNode.js.flow +11 -4
  185. package/util/ReaderNode.js.flow +25 -2
  186. package/util/RelayConcreteNode.js.flow +5 -1
  187. package/util/RelayDefaultHandleKey.js.flow +1 -1
  188. package/util/RelayError.js.flow +1 -1
  189. package/util/RelayFeatureFlags.js.flow +23 -15
  190. package/util/RelayProfiler.js.flow +1 -1
  191. package/util/RelayReplaySubject.js.flow +10 -10
  192. package/util/RelayRuntimeTypes.js.flow +70 -3
  193. package/util/StringInterner.js.flow +69 -0
  194. package/util/createPayloadFor3DField.js.flow +4 -4
  195. package/util/deepFreeze.js.flow +1 -1
  196. package/util/generateID.js.flow +1 -1
  197. package/util/getAllRootVariables.js.flow +36 -0
  198. package/util/getFragmentIdentifier.js.flow +28 -16
  199. package/util/getOperation.js.flow +3 -3
  200. package/util/getPaginationMetadata.js.flow +69 -0
  201. package/util/getPaginationVariables.js.flow +108 -0
  202. package/util/getPendingOperationsForFragment.js.flow +62 -0
  203. package/util/getRefetchMetadata.js.flow +76 -0
  204. package/util/getRelayHandleKey.js.flow +2 -3
  205. package/util/getRequestIdentifier.js.flow +4 -4
  206. package/util/getValueAtPath.js.flow +46 -0
  207. package/util/isEmptyObject.js.flow +2 -1
  208. package/util/isPromise.js.flow +1 -1
  209. package/util/isScalarAndEqual.js.flow +1 -1
  210. package/util/recycleNodesInto.js.flow +1 -1
  211. package/util/registerEnvironmentWithDevTools.js.flow +33 -0
  212. package/util/reportMissingRequiredFields.js.flow +1 -1
  213. package/util/resolveImmediate.js.flow +2 -2
  214. package/util/stableCopy.js.flow +1 -1
  215. package/util/withDuration.js.flow +32 -0
  216. package/lib/store/RelayRecordSourceMapImpl.js +0 -107
  217. package/lib/store/RelayStoreSubscriptionsUsingMapByID.js +0 -318
  218. package/store/RelayRecordSourceMapImpl.js.flow +0 -91
  219. package/store/RelayStoreSubscriptionsUsingMapByID.js.flow +0 -283
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
3
  *
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
@@ -12,12 +12,17 @@
12
12
 
13
13
  'use strict';
14
14
 
15
- const invariant = require('invariant');
16
-
17
- import type {NormalizationOperation} from '../util/NormalizationNode';
15
+ import type {
16
+ NormalizationArgument,
17
+ NormalizationLocalArgumentDefinition,
18
+ NormalizationOperation,
19
+ } from '../util/NormalizationNode';
18
20
  import type {ReaderFragment} from '../util/ReaderNode';
19
21
  import type {Variables} from '../util/RelayRuntimeTypes';
20
22
 
23
+ const {getArgumentValues} = require('./RelayStoreUtils');
24
+ const invariant = require('invariant');
25
+
21
26
  /**
22
27
  * Determines the variables that are in scope for a fragment given the variables
23
28
  * in scope at the root query as well as any arguments applied at the fragment
@@ -35,7 +40,6 @@ function getFragmentVariables(
35
40
  if (argumentVariables.hasOwnProperty(definition.name)) {
36
41
  return;
37
42
  }
38
- // $FlowFixMe[cannot-spread-interface]
39
43
  variables = variables || {...argumentVariables};
40
44
  switch (definition.kind) {
41
45
  case 'LocalArgument':
@@ -52,12 +56,9 @@ function getFragmentVariables(
52
56
  * RelayStoreUtils.getStableVariableValue() that variable keys are all
53
57
  * present.
54
58
  */
55
- // $FlowFixMe[incompatible-use]
56
59
  variables[definition.name] = undefined;
57
60
  break;
58
61
  }
59
- // $FlowFixMe[incompatible-use]
60
- // $FlowFixMe[cannot-write]
61
62
  variables[definition.name] = rootVariables[definition.name];
62
63
  break;
63
64
  default:
@@ -86,7 +87,6 @@ function getOperationVariables(
86
87
  const operationVariables = {};
87
88
  operation.argumentDefinitions.forEach(def => {
88
89
  let value = def.defaultValue;
89
- // $FlowFixMe[cannot-write]
90
90
  if (variables[def.name] != null) {
91
91
  value = variables[def.name];
92
92
  }
@@ -95,7 +95,26 @@ function getOperationVariables(
95
95
  return operationVariables;
96
96
  }
97
97
 
98
+ function getLocalVariables(
99
+ currentVariables: Variables,
100
+ argumentDefinitions: ?$ReadOnlyArray<NormalizationLocalArgumentDefinition>,
101
+ args: ?$ReadOnlyArray<NormalizationArgument>,
102
+ ): Variables {
103
+ if (argumentDefinitions == null) {
104
+ return currentVariables;
105
+ }
106
+ const nextVariables = {...currentVariables};
107
+ const nextArgs = args ? getArgumentValues(args, currentVariables) : {};
108
+ argumentDefinitions.forEach(def => {
109
+ // $FlowFixMe[cannot-write]
110
+ const value = nextArgs[def.name] ?? def.defaultValue;
111
+ nextVariables[def.name] = value;
112
+ });
113
+ return nextVariables;
114
+ }
115
+
98
116
  module.exports = {
117
+ getLocalVariables,
99
118
  getFragmentVariables,
100
119
  getOperationVariables,
101
120
  };
@@ -0,0 +1,121 @@
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
+ * @emails oncall+relay
8
+ * @flow
9
+ * @format
10
+ */
11
+
12
+ import type {
13
+ DataChunk,
14
+ GraphModeResponse,
15
+ RecordChunk,
16
+ } from './RelayExperimentalGraphResponseTransform';
17
+ import type {MutableRecordSource, Record} from './RelayStoreTypes';
18
+
19
+ const RelayModernRecord = require('./RelayModernRecord');
20
+ const invariant = require('invariant');
21
+
22
+ /**
23
+ * Given a stream of GraphMode chunks, populate a MutableRecordSource.
24
+ */
25
+ export function handleGraphModeResponse(
26
+ recordSource: MutableRecordSource,
27
+ response: GraphModeResponse,
28
+ ): MutableRecordSource {
29
+ const handler = new GraphModeHandler(recordSource);
30
+ return handler.populateRecordSource(response);
31
+ }
32
+
33
+ class GraphModeHandler {
34
+ _recordSource: MutableRecordSource;
35
+ _streamIdToCacheKey: Map<number, string>;
36
+ constructor(recordSource: MutableRecordSource) {
37
+ this._recordSource = recordSource;
38
+ this._streamIdToCacheKey = new Map();
39
+ }
40
+ populateRecordSource(response: GraphModeResponse): MutableRecordSource {
41
+ for (const chunk of response) {
42
+ switch (chunk.$kind) {
43
+ case 'Record':
44
+ this._handleRecordChunk(chunk);
45
+ break;
46
+ case 'Extend': {
47
+ const cacheKey = this._lookupCacheKey(chunk.$streamID);
48
+ const record = this._recordSource.get(cacheKey);
49
+ invariant(
50
+ record != null,
51
+ `Expected to have a record for cache key ${cacheKey}`,
52
+ );
53
+ this._populateRecord(record, chunk);
54
+ break;
55
+ }
56
+ case 'Complete':
57
+ this._streamIdToCacheKey.clear();
58
+ break;
59
+ default:
60
+ (chunk.$kind: empty);
61
+ }
62
+ }
63
+ return this._recordSource;
64
+ }
65
+
66
+ _handleRecordChunk(chunk: RecordChunk) {
67
+ const cacheKey = chunk.__id;
68
+ let record = this._recordSource.get(cacheKey);
69
+ if (record == null) {
70
+ record = RelayModernRecord.create(cacheKey, chunk.__typename);
71
+ this._recordSource.set(cacheKey, record);
72
+ }
73
+
74
+ this._streamIdToCacheKey.set(chunk.$streamID, cacheKey);
75
+ this._populateRecord(record, chunk);
76
+ }
77
+
78
+ _populateRecord(parentRecord: Record, chunk: DataChunk) {
79
+ for (const [key, value] of Object.entries(chunk)) {
80
+ switch (key) {
81
+ case '$streamID':
82
+ case '$kind':
83
+ case '__typename':
84
+ break;
85
+ default:
86
+ if (
87
+ typeof value !== 'object' ||
88
+ value == null ||
89
+ Array.isArray(value)
90
+ ) {
91
+ RelayModernRecord.setValue(parentRecord, key, value);
92
+ } else {
93
+ if (value.hasOwnProperty('__id')) {
94
+ // Singular
95
+ const streamID = ((value.__id: any): number);
96
+ const id = this._lookupCacheKey(streamID);
97
+ RelayModernRecord.setLinkedRecordID(parentRecord, key, id);
98
+ } else if (value.hasOwnProperty('__ids')) {
99
+ // Plural
100
+ const streamIDs = ((value.__ids: any): Array<number | null>);
101
+ const ids = streamIDs.map(sID => {
102
+ return sID == null ? null : this._lookupCacheKey(sID);
103
+ });
104
+ RelayModernRecord.setLinkedRecordIDs(parentRecord, key, ids);
105
+ } else {
106
+ invariant(false, 'Expected object to have either __id or __ids.');
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ _lookupCacheKey(streamID: number): string {
114
+ const cacheKey = this._streamIdToCacheKey.get(streamID);
115
+ invariant(
116
+ cacheKey != null,
117
+ `Expected to have a cacheKey for $streamID ${streamID}`,
118
+ );
119
+ return cacheKey;
120
+ }
121
+ }
@@ -0,0 +1,470 @@
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
+ * @emails oncall+relay
8
+ * @flow
9
+ * @format
10
+ */
11
+
12
+ import type {ActorIdentifier} from '../multi-actor-environment/ActorIdentifier';
13
+ import type {PayloadData} from '../network/RelayNetworkTypes';
14
+ import type {
15
+ NormalizationField,
16
+ NormalizationLinkedField,
17
+ NormalizationNode,
18
+ } from '../util/NormalizationNode';
19
+ import type {DataID, Variables} from '../util/RelayRuntimeTypes';
20
+ import type {NormalizationOptions} from './RelayResponseNormalizer';
21
+ import type {GetDataID} from './RelayResponseNormalizer';
22
+ import type {
23
+ IncrementalDataPlaceholder,
24
+ NormalizationSelector,
25
+ } from './RelayStoreTypes';
26
+
27
+ const {
28
+ CLIENT_EXTENSION,
29
+ CONDITION,
30
+ DEFER,
31
+ FRAGMENT_SPREAD,
32
+ INLINE_FRAGMENT,
33
+ LINKED_FIELD,
34
+ SCALAR_FIELD,
35
+ } = require('../util/RelayConcreteNode');
36
+ const {getLocalVariables} = require('./RelayConcreteVariables');
37
+ const {createNormalizationSelector} = require('./RelayModernSelector');
38
+ const {ROOT_TYPE, TYPENAME_KEY, getStorageKey} = require('./RelayStoreUtils');
39
+ const invariant = require('invariant');
40
+ const {generateClientID} = require('relay-runtime');
41
+
42
+ /**
43
+ * This module is an experiment to explore a proposal normalized response format for GraphQL.
44
+ * See the Quip document: Canonical Normalized Response Format (“GraphMode”) Proposal
45
+ */
46
+
47
+ /**
48
+ * # TODO
49
+ *
50
+ * - [ ] Compute storage keys using method outlined in the proposal
51
+ * - [ ] Plural fields
52
+ * - [ ] Write a utility to populate the store using a GraphMode response.
53
+ */
54
+
55
+ export type ScalarField = string | number | null;
56
+ export type LinkedField =
57
+ | {
58
+ __id: number,
59
+ }
60
+ | {
61
+ __ids: Array<number | null>,
62
+ };
63
+
64
+ export type ChunkField = ScalarField | Array<ScalarField> | LinkedField;
65
+
66
+ export type ChunkFields = {
67
+ [string]: ChunkField,
68
+ };
69
+
70
+ export type RecordChunk = {
71
+ $kind: 'Record',
72
+ $streamID: number,
73
+ __id: string,
74
+ __typename: string,
75
+ [string]: ChunkField,
76
+ };
77
+
78
+ export type ExtendChunk = {
79
+ $kind: 'Extend',
80
+ $streamID: number,
81
+ [string]: ChunkField,
82
+ };
83
+
84
+ export type CompleteChunk = {
85
+ $kind: 'Complete',
86
+ };
87
+
88
+ export type DataChunk = RecordChunk | ExtendChunk;
89
+
90
+ export type GraphModeChunk = DataChunk | CompleteChunk;
91
+
92
+ export type GraphModeResponse = Iterable<GraphModeChunk>;
93
+
94
+ /**
95
+ * Converts a JSON response (and Normalization AST) into a stream of GraphMode chunks
96
+ *
97
+ * The stream is modeled as a Generator in order to highlight the streaming
98
+ * nature of the response. Once a chunk is generated, it can be immediately flushed
99
+ * to the client.
100
+ *
101
+ * The response is traversed depth-first, meaning children are emitted before
102
+ * the parent. This allows parent objects to reference children using their
103
+ * `$streamID`.
104
+ *
105
+ * After each object is traversed, a chunk is emitted. The first time an object
106
+ * -- identified by its strong ID -- is encountered we emit a `Record`, and its
107
+ * `$streamID` is recorded. If that same object is encountered again later in
108
+ * the response, an `Extend` chunk is emitted, which includes any previously
109
+ * unsent fields. If no unsent fields are present in the second appearance of
110
+ * the new object, no chunk is emitted.
111
+ *
112
+ * ## State
113
+ *
114
+ * As we traverse we must maintain some state:
115
+ *
116
+ * - The next streamID
117
+ * - A mapping of cache keys to streamIDs
118
+ * - The set of fields which we've sent for each streamID. This allows us to
119
+ * avoid sending fields twice.
120
+ */
121
+ export function normalizeResponse(
122
+ response: PayloadData,
123
+ selector: NormalizationSelector,
124
+ options: NormalizationOptions,
125
+ ): GraphModeResponse {
126
+ const {node, variables, dataID} = selector;
127
+ const normalizer = new GraphModeNormalizer(variables, options);
128
+ return normalizer.normalizeResponse(node, dataID, response);
129
+ }
130
+
131
+ class GraphModeNormalizer {
132
+ _cacheKeyToStreamID: Map<string, number>;
133
+ _sentFields: Map<string, Set<string>>;
134
+ _getDataId: GetDataID;
135
+ _nextStreamID: number;
136
+ _getDataID: GetDataID;
137
+ _variables: Variables;
138
+ _path: Array<string>;
139
+ _incrementalPlaceholders: Array<IncrementalDataPlaceholder>;
140
+ _actorIdentifier: ?ActorIdentifier;
141
+ constructor(variables: Variables, options: NormalizationOptions) {
142
+ this._actorIdentifier = options.actorIdentifier;
143
+ this._path = options.path ? [...options.path] : [];
144
+ this._getDataID = options.getDataID;
145
+ this._cacheKeyToStreamID = new Map();
146
+ this._sentFields = new Map();
147
+ this._nextStreamID = 0;
148
+ this._variables = variables;
149
+ }
150
+
151
+ _getStreamID() {
152
+ return this._nextStreamID++;
153
+ }
154
+
155
+ _getSentFields(cacheKey: string): Set<string> {
156
+ const maybeSent = this._sentFields.get(cacheKey);
157
+ if (maybeSent != null) {
158
+ return maybeSent;
159
+ }
160
+ const sent = new Set();
161
+ this._sentFields.set(cacheKey, sent);
162
+ return sent;
163
+ }
164
+
165
+ _getObjectType(data: PayloadData): string {
166
+ const typeName = (data: any)[TYPENAME_KEY];
167
+ invariant(
168
+ typeName != null,
169
+ 'Expected a typename for record `%s`.',
170
+ JSON.stringify(data, null, 2),
171
+ );
172
+ return typeName;
173
+ }
174
+
175
+ // TODO: The GraphMode proposal outlines different approachs to derive keys. We
176
+ // can expriment with different approaches here.
177
+ _getStorageKey(selection: NormalizationField) {
178
+ return getStorageKey(selection, this._variables);
179
+ }
180
+
181
+ _getVariableValue(name: string): mixed {
182
+ invariant(
183
+ this._variables.hasOwnProperty(name),
184
+ 'Unexpected undefined variable `%s`.',
185
+ name,
186
+ );
187
+ return this._variables[name];
188
+ }
189
+
190
+ *normalizeResponse(
191
+ node: NormalizationNode,
192
+ dataID: DataID,
193
+ data: PayloadData,
194
+ ): Generator<GraphModeChunk, void, void> {
195
+ const rootFields: ChunkFields = {};
196
+ yield* this._traverseSelections(node, data, rootFields, dataID, new Set());
197
+
198
+ const $streamID = this._getStreamID();
199
+ yield {
200
+ ...rootFields,
201
+ $kind: 'Record',
202
+ $streamID,
203
+ __id: dataID,
204
+ __typename: ROOT_TYPE,
205
+ };
206
+ yield {
207
+ $kind: 'Complete',
208
+ };
209
+ }
210
+
211
+ *_flushFields(
212
+ cacheKey: string,
213
+ typename: string,
214
+ fields: ChunkFields,
215
+ ): Generator<GraphModeChunk, number, void> {
216
+ const maybeStreamID = this._cacheKeyToStreamID.get(cacheKey);
217
+ const $streamID = maybeStreamID ?? this._getStreamID();
218
+ if (maybeStreamID == null) {
219
+ this._cacheKeyToStreamID.set(cacheKey, $streamID);
220
+ // TODO: We could mutate `fields` rather than constructing a new
221
+ // chunk object, but it's hard to convince Flow that we've
222
+ // constructed a valid Chunk, and perf is not important for this
223
+ // experimental transform
224
+ yield {
225
+ ...fields,
226
+ $kind: 'Record',
227
+ __typename: typename,
228
+ __id: cacheKey,
229
+ $streamID,
230
+ };
231
+ } else if (Object.keys(fields).length > 0) {
232
+ yield {...fields, $kind: 'Extend', $streamID};
233
+ }
234
+ return $streamID;
235
+ }
236
+
237
+ *_traverseSelections(
238
+ node: NormalizationNode,
239
+ data: PayloadData,
240
+ parentFields: ChunkFields,
241
+ parentID: string,
242
+ sentFields: Set<string>,
243
+ ): Generator<GraphModeChunk, void, void> {
244
+ const selections = node.selections;
245
+
246
+ for (const selection of selections) {
247
+ switch (selection.kind) {
248
+ case LINKED_FIELD: {
249
+ const responseKey = selection.alias ?? selection.name;
250
+ const fieldData = ((data[responseKey]: any): PayloadData);
251
+
252
+ const storageKey = this._getStorageKey(selection);
253
+
254
+ this._path.push(responseKey);
255
+
256
+ const fieldValue = yield* this._traverseLinkedField(
257
+ selection.plural,
258
+ fieldData,
259
+ storageKey,
260
+ selection,
261
+ parentID,
262
+ );
263
+
264
+ this._path.pop();
265
+
266
+ // TODO: We could also opt to confirm that this matches the previously
267
+ // seen value.
268
+ if (sentFields.has(storageKey)) {
269
+ break;
270
+ }
271
+
272
+ parentFields[storageKey] = fieldValue;
273
+ sentFields.add(storageKey);
274
+ break;
275
+ }
276
+ case SCALAR_FIELD: {
277
+ const responseKey = selection.alias ?? selection.name;
278
+
279
+ const storageKey = this._getStorageKey(selection);
280
+
281
+ // TODO: We could also opt to confirm that this matches the previously
282
+ // seen value.
283
+ if (sentFields.has(storageKey)) {
284
+ break;
285
+ }
286
+ const fieldData = ((data[responseKey]: any): ChunkField);
287
+
288
+ parentFields[storageKey] = fieldData;
289
+ sentFields.add(storageKey);
290
+ break;
291
+ }
292
+ case INLINE_FRAGMENT: {
293
+ const objType = this._getObjectType(data);
294
+ const {abstractKey} = selection;
295
+ if (abstractKey == null) {
296
+ if (objType !== selection.type) {
297
+ break;
298
+ }
299
+ } else if (!data.hasOwnProperty(abstractKey)) {
300
+ break;
301
+ }
302
+ yield* this._traverseSelections(
303
+ selection,
304
+ data,
305
+ parentFields,
306
+ parentID,
307
+ sentFields,
308
+ );
309
+ break;
310
+ }
311
+ case FRAGMENT_SPREAD: {
312
+ const prevVariables = this._variables;
313
+ this._variables = getLocalVariables(
314
+ this._variables,
315
+ selection.fragment.argumentDefinitions,
316
+ selection.args,
317
+ );
318
+ yield* this._traverseSelections(
319
+ selection.fragment,
320
+ data,
321
+ parentFields,
322
+ parentID,
323
+ sentFields,
324
+ );
325
+ this._variables = prevVariables;
326
+ break;
327
+ }
328
+ case CONDITION:
329
+ const conditionValue = Boolean(
330
+ this._getVariableValue(selection.condition),
331
+ );
332
+ if (conditionValue === selection.passingValue) {
333
+ yield* this._traverseSelections(
334
+ selection,
335
+ data,
336
+ parentFields,
337
+ parentID,
338
+ sentFields,
339
+ );
340
+ }
341
+ break;
342
+ case DEFER:
343
+ const isDeferred =
344
+ selection.if === null || this._getVariableValue(selection.if);
345
+ if (isDeferred === false) {
346
+ // If defer is disabled there will be no additional response chunk:
347
+ // normalize the data already present.
348
+ yield* this._traverseSelections(
349
+ selection,
350
+ data,
351
+ parentFields,
352
+ parentID,
353
+ sentFields,
354
+ );
355
+ } else {
356
+ // Otherwise data *for this selection* should not be present: enqueue
357
+ // metadata to process the subsequent response chunk.
358
+ this._incrementalPlaceholders.push({
359
+ kind: 'defer',
360
+ data,
361
+ label: selection.label,
362
+ path: [...this._path],
363
+ selector: createNormalizationSelector(
364
+ selection,
365
+ parentID,
366
+ this._variables,
367
+ ),
368
+ typeName: this._getObjectType(data),
369
+ actorIdentifier: this._actorIdentifier,
370
+ });
371
+ }
372
+ break;
373
+ case CLIENT_EXTENSION:
374
+ // Since we are only expecting to handle server responses, we can skip
375
+ // over client extensions.
376
+ break;
377
+ default:
378
+ throw new Error(`Unexpected selection type: ${selection.kind}`);
379
+ }
380
+ }
381
+ }
382
+
383
+ *_traverseLinkedField(
384
+ plural: boolean,
385
+ fieldData: PayloadData,
386
+ storageKey: string,
387
+ selection: NormalizationLinkedField,
388
+ parentID: string,
389
+ index?: number,
390
+ ): Generator<GraphModeChunk, ChunkField, void> {
391
+ if (fieldData == null) {
392
+ return null;
393
+ }
394
+
395
+ if (plural) {
396
+ invariant(
397
+ Array.isArray(fieldData),
398
+ `Expected fieldData to be an array. Got ${JSON.stringify(fieldData)}`,
399
+ );
400
+
401
+ const fieldValue = [];
402
+ for (const [i, itemData] of fieldData.entries()) {
403
+ this._path.push(String(i));
404
+ const itemValue = yield* this._traverseLinkedField(
405
+ false,
406
+ itemData,
407
+ storageKey,
408
+ selection,
409
+ parentID,
410
+ i,
411
+ );
412
+ this._path.pop();
413
+ fieldValue.push(itemValue);
414
+ }
415
+
416
+ const ids = fieldValue.map(value => {
417
+ if (value == null) {
418
+ return null;
419
+ }
420
+ invariant(
421
+ typeof value.__id === 'number',
422
+ 'Expected objects in a plural linked field to have an __id.',
423
+ );
424
+ return value.__id;
425
+ });
426
+
427
+ return {__ids: ids};
428
+ }
429
+
430
+ invariant(
431
+ typeof fieldData === 'object',
432
+ 'Expected data for field `%s` to be an object.',
433
+ storageKey,
434
+ );
435
+
436
+ const objType = selection.concreteType ?? this._getObjectType(fieldData);
437
+
438
+ const nextID =
439
+ this._getDataID(fieldData, objType) ||
440
+ // Note: In RelayResponseNormalizer we try to access a cached
441
+ // version of the key before generating a new one. I'm not clear if
442
+ // that's a performance optimization (which would not be important
443
+ // here) or important for stable ids.
444
+
445
+ // TODO: The proposal does not yet specify how we handle objects
446
+ // without strong ids.
447
+ generateClientID(parentID, storageKey, index);
448
+
449
+ invariant(
450
+ typeof nextID === 'string',
451
+ 'Expected id on field `%s` to be a string.',
452
+ storageKey,
453
+ );
454
+
455
+ const fields: ChunkFields = {};
456
+
457
+ // Yield any decendent record chunks, and mutatively populate direct fields.
458
+ yield* this._traverseSelections(
459
+ selection,
460
+ fieldData,
461
+ fields,
462
+ nextID,
463
+ this._getSentFields(nextID),
464
+ );
465
+
466
+ const $streamID = yield* this._flushFields(nextID, objType, fields);
467
+
468
+ return {__id: $streamID};
469
+ }
470
+ }