@isograph/react 0.1.1 → 0.3.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 (160) hide show
  1. package/dist/core/FragmentReference.d.ts +25 -0
  2. package/dist/core/FragmentReference.d.ts.map +1 -0
  3. package/dist/core/FragmentReference.js +16 -0
  4. package/dist/core/IsographEnvironment.d.ts +89 -0
  5. package/dist/core/IsographEnvironment.d.ts.map +1 -0
  6. package/dist/core/IsographEnvironment.js +65 -0
  7. package/dist/core/PromiseWrapper.d.ts +28 -0
  8. package/dist/core/PromiseWrapper.d.ts.map +1 -0
  9. package/dist/core/PromiseWrapper.js +57 -0
  10. package/dist/core/areEqualWithDeepComparison.d.ts +5 -0
  11. package/dist/core/areEqualWithDeepComparison.d.ts.map +1 -0
  12. package/dist/core/areEqualWithDeepComparison.js +95 -0
  13. package/dist/core/cache.d.ts +44 -0
  14. package/dist/core/cache.d.ts.map +1 -0
  15. package/dist/core/cache.js +514 -0
  16. package/dist/core/check.d.ts +18 -0
  17. package/dist/core/check.d.ts.map +1 -0
  18. package/dist/core/check.js +127 -0
  19. package/dist/core/componentCache.d.ts +5 -0
  20. package/dist/core/componentCache.d.ts.map +1 -0
  21. package/dist/core/componentCache.js +38 -0
  22. package/dist/core/entrypoint.d.ts +69 -0
  23. package/dist/core/entrypoint.d.ts.map +1 -0
  24. package/dist/core/entrypoint.js +7 -0
  25. package/dist/core/garbageCollection.d.ts +13 -0
  26. package/dist/core/garbageCollection.d.ts.map +1 -0
  27. package/dist/core/garbageCollection.js +107 -0
  28. package/dist/core/logging.d.ts +69 -0
  29. package/dist/core/logging.d.ts.map +1 -0
  30. package/dist/core/logging.js +19 -0
  31. package/dist/core/makeNetworkRequest.d.ts +9 -0
  32. package/dist/core/makeNetworkRequest.d.ts.map +1 -0
  33. package/dist/core/makeNetworkRequest.js +118 -0
  34. package/dist/core/read.d.ts +27 -0
  35. package/dist/core/read.d.ts.map +1 -0
  36. package/dist/core/read.js +478 -0
  37. package/dist/core/reader.d.ts +87 -0
  38. package/dist/core/reader.d.ts.map +1 -0
  39. package/dist/core/reader.js +2 -0
  40. package/dist/core/util.d.ts +19 -0
  41. package/dist/core/util.d.ts.map +1 -0
  42. package/dist/core/util.js +2 -0
  43. package/dist/index.d.ts +26 -120
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +57 -306
  46. package/dist/loadable-hooks/useClientSideDefer.d.ts +16 -0
  47. package/dist/loadable-hooks/useClientSideDefer.d.ts.map +1 -0
  48. package/dist/loadable-hooks/useClientSideDefer.js +13 -0
  49. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts +34 -0
  50. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts.map +1 -0
  51. package/dist/loadable-hooks/useConnectionSpecPagination.js +160 -0
  52. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts +6 -0
  53. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts.map +1 -0
  54. package/dist/loadable-hooks/useImperativeExposedMutationField.js +14 -0
  55. package/dist/loadable-hooks/useImperativeLoadableField.d.ts +17 -0
  56. package/dist/loadable-hooks/useImperativeLoadableField.d.ts.map +1 -0
  57. package/dist/loadable-hooks/useImperativeLoadableField.js +14 -0
  58. package/dist/loadable-hooks/useSkipLimitPagination.d.ts +27 -0
  59. package/dist/loadable-hooks/useSkipLimitPagination.d.ts.map +1 -0
  60. package/dist/loadable-hooks/useSkipLimitPagination.js +162 -0
  61. package/dist/react/FragmentReader.d.ts +16 -0
  62. package/dist/react/FragmentReader.d.ts.map +1 -0
  63. package/dist/{EntrypointReader.js → react/FragmentReader.js} +7 -5
  64. package/dist/react/IsographEnvironmentProvider.d.ts +11 -0
  65. package/dist/react/IsographEnvironmentProvider.d.ts.map +1 -0
  66. package/dist/{IsographEnvironment.js → react/IsographEnvironmentProvider.js} +4 -22
  67. package/dist/react/RenderAfterCommit__DO_NOT_USE.d.ts +10 -0
  68. package/dist/react/RenderAfterCommit__DO_NOT_USE.d.ts.map +1 -0
  69. package/dist/react/RenderAfterCommit__DO_NOT_USE.js +15 -0
  70. package/dist/react/useImperativeReference.d.ts +12 -0
  71. package/dist/react/useImperativeReference.d.ts.map +1 -0
  72. package/dist/react/useImperativeReference.js +35 -0
  73. package/dist/react/useLazyReference.d.ts +10 -0
  74. package/dist/react/useLazyReference.d.ts.map +1 -0
  75. package/dist/react/useLazyReference.js +21 -0
  76. package/dist/react/useReadAndSubscribe.d.ts +20 -0
  77. package/dist/react/useReadAndSubscribe.d.ts.map +1 -0
  78. package/dist/react/useReadAndSubscribe.js +40 -0
  79. package/dist/react/useRerenderOnChange.d.ts +8 -0
  80. package/dist/react/useRerenderOnChange.d.ts.map +1 -0
  81. package/dist/react/useRerenderOnChange.js +22 -0
  82. package/dist/react/useResult.d.ts +9 -0
  83. package/dist/react/useResult.d.ts.map +1 -0
  84. package/dist/react/useResult.js +39 -0
  85. package/docs/how-useLazyReference-works.md +117 -0
  86. package/isograph.config.json +7 -0
  87. package/package.json +18 -9
  88. package/schema.graphql +17 -0
  89. package/src/core/FragmentReference.ts +49 -0
  90. package/src/core/IsographEnvironment.ts +192 -0
  91. package/src/core/PromiseWrapper.ts +86 -0
  92. package/src/core/areEqualWithDeepComparison.ts +112 -0
  93. package/src/core/cache.ts +835 -0
  94. package/src/core/check.ts +207 -0
  95. package/src/core/componentCache.ts +62 -0
  96. package/src/core/entrypoint.ts +106 -0
  97. package/src/core/garbageCollection.ts +173 -0
  98. package/src/core/logging.ts +116 -0
  99. package/src/core/makeNetworkRequest.ts +175 -0
  100. package/src/core/read.ts +722 -0
  101. package/src/core/reader.ts +160 -0
  102. package/src/core/util.ts +27 -0
  103. package/src/index.ts +99 -0
  104. package/src/loadable-hooks/useClientSideDefer.ts +58 -0
  105. package/src/loadable-hooks/useConnectionSpecPagination.ts +331 -0
  106. package/src/loadable-hooks/useImperativeExposedMutationField.ts +17 -0
  107. package/src/loadable-hooks/useImperativeLoadableField.ts +52 -0
  108. package/src/loadable-hooks/useSkipLimitPagination.ts +352 -0
  109. package/src/react/FragmentReader.tsx +43 -0
  110. package/src/react/IsographEnvironmentProvider.tsx +33 -0
  111. package/src/react/RenderAfterCommit__DO_NOT_USE.tsx +17 -0
  112. package/src/react/useImperativeReference.ts +68 -0
  113. package/src/react/useLazyReference.ts +42 -0
  114. package/src/react/useReadAndSubscribe.ts +81 -0
  115. package/src/react/useRerenderOnChange.ts +38 -0
  116. package/src/react/useResult.ts +73 -0
  117. package/src/tests/__isograph/Query/meName/entrypoint.ts +52 -0
  118. package/src/tests/__isograph/Query/meName/output_type.ts +3 -0
  119. package/src/tests/__isograph/Query/meName/param_type.ts +9 -0
  120. package/src/tests/__isograph/Query/meName/resolver_reader.ts +33 -0
  121. package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +90 -0
  122. package/src/tests/__isograph/Query/meNameSuccessor/output_type.ts +3 -0
  123. package/src/tests/__isograph/Query/meNameSuccessor/param_type.ts +14 -0
  124. package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +57 -0
  125. package/src/tests/__isograph/Query/nodeField/entrypoint.ts +57 -0
  126. package/src/tests/__isograph/Query/nodeField/output_type.ts +3 -0
  127. package/src/tests/__isograph/Query/nodeField/param_type.ts +10 -0
  128. package/src/tests/__isograph/Query/nodeField/parameters_type.ts +3 -0
  129. package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +38 -0
  130. package/src/tests/__isograph/Query/subquery/entrypoint.ts +67 -0
  131. package/src/tests/__isograph/Query/subquery/output_type.ts +3 -0
  132. package/src/tests/__isograph/Query/subquery/param_type.ts +12 -0
  133. package/src/tests/__isograph/Query/subquery/parameters_type.ts +3 -0
  134. package/src/tests/__isograph/Query/subquery/resolver_reader.ts +47 -0
  135. package/src/tests/__isograph/iso.ts +99 -0
  136. package/src/tests/garbageCollection.test.ts +142 -0
  137. package/src/tests/meNameSuccessor.ts +25 -0
  138. package/src/tests/nodeQuery.ts +19 -0
  139. package/src/tests/normalizeData.test.ts +120 -0
  140. package/src/tests/tsconfig.json +21 -0
  141. package/tsconfig.json +6 -0
  142. package/tsconfig.pkg.json +7 -1
  143. package/vitest.config.ts +20 -0
  144. package/dist/EntrypointReader.d.ts +0 -6
  145. package/dist/IsographEnvironment.d.ts +0 -56
  146. package/dist/PromiseWrapper.d.ts +0 -13
  147. package/dist/PromiseWrapper.js +0 -22
  148. package/dist/cache.d.ts +0 -26
  149. package/dist/cache.js +0 -274
  150. package/dist/componentCache.d.ts +0 -6
  151. package/dist/componentCache.js +0 -31
  152. package/dist/useImperativeReference.d.ts +0 -8
  153. package/dist/useImperativeReference.js +0 -28
  154. package/src/EntrypointReader.tsx +0 -20
  155. package/src/IsographEnvironment.tsx +0 -120
  156. package/src/PromiseWrapper.ts +0 -29
  157. package/src/cache.tsx +0 -484
  158. package/src/componentCache.ts +0 -41
  159. package/src/index.tsx +0 -617
  160. package/src/useImperativeReference.ts +0 -58
@@ -0,0 +1,835 @@
1
+ import {
2
+ Factory,
3
+ ItemCleanupPair,
4
+ ParentCache,
5
+ } from '@isograph/react-disposable-state';
6
+ import {
7
+ DataId,
8
+ Link,
9
+ ROOT_ID,
10
+ StoreRecord,
11
+ type IsographEnvironment,
12
+ DataTypeValue,
13
+ getLink,
14
+ FragmentSubscription,
15
+ type TypeName,
16
+ } from './IsographEnvironment';
17
+ import {
18
+ IsographEntrypoint,
19
+ NormalizationAst,
20
+ NormalizationInlineFragment,
21
+ NormalizationLinkedField,
22
+ NormalizationScalarField,
23
+ RefetchQueryNormalizationArtifactWrapper,
24
+ } from '../core/entrypoint';
25
+ import { ReaderLinkedField, ReaderScalarField, type ReaderAst } from './reader';
26
+ import { Argument, ArgumentValue } from './util';
27
+ import { WithEncounteredRecords, readButDoNotEvaluate } from './read';
28
+ import {
29
+ FragmentReference,
30
+ Variables,
31
+ ExtractParameters,
32
+ } from './FragmentReference';
33
+ import { mergeObjectsUsingReaderAst } from './areEqualWithDeepComparison';
34
+ import { maybeMakeNetworkRequest } from './makeNetworkRequest';
35
+ import { wrapResolvedValue } from './PromiseWrapper';
36
+ import { logMessage } from './logging';
37
+ import { FetchOptions } from './check';
38
+
39
+ export const TYPENAME_FIELD_NAME = '__typename';
40
+
41
+ export function getOrCreateItemInSuspenseCache<
42
+ TReadFromStore extends { parameters: object; data: object },
43
+ TClientFieldValue,
44
+ >(
45
+ environment: IsographEnvironment,
46
+ index: string,
47
+ factory: Factory<FragmentReference<TReadFromStore, TClientFieldValue>>,
48
+ ): ParentCache<FragmentReference<TReadFromStore, TClientFieldValue>> {
49
+ // TODO this is probably a useless message, we should remove it
50
+ logMessage(environment, {
51
+ kind: 'GettingSuspenseCacheItem',
52
+ index,
53
+ availableCacheItems: Object.keys(environment.fragmentCache),
54
+ found: !!environment.fragmentCache[index],
55
+ });
56
+ if (environment.fragmentCache[index] == null) {
57
+ environment.fragmentCache[index] = new ParentCache(factory);
58
+ }
59
+
60
+ return environment.fragmentCache[index];
61
+ }
62
+
63
+ /**
64
+ * Creates a copy of the provided value, ensuring any nested objects have their
65
+ * keys sorted such that equivalent values would have identical JSON.stringify
66
+ * results.
67
+ */
68
+ export function stableCopy<T>(value: T): T {
69
+ if (!value || typeof value !== 'object') {
70
+ return value;
71
+ }
72
+ if (Array.isArray(value)) {
73
+ // @ts-ignore
74
+ return value.map(stableCopy);
75
+ }
76
+ const keys = Object.keys(value).sort();
77
+ const stable: { [index: string]: any } = {};
78
+ for (let i = 0; i < keys.length; i++) {
79
+ // @ts-ignore
80
+ stable[keys[i]] = stableCopy(value[keys[i]]);
81
+ }
82
+ return stable as any;
83
+ }
84
+
85
+ export function getOrCreateCacheForArtifact<
86
+ TReadFromStore extends { parameters: object; data: object },
87
+ TClientFieldValue,
88
+ >(
89
+ environment: IsographEnvironment,
90
+ entrypoint: IsographEntrypoint<TReadFromStore, TClientFieldValue>,
91
+ variables: ExtractParameters<TReadFromStore>,
92
+ fetchOptions?: FetchOptions,
93
+ ): ParentCache<FragmentReference<TReadFromStore, TClientFieldValue>> {
94
+ const cacheKey =
95
+ entrypoint.networkRequestInfo.queryText +
96
+ JSON.stringify(stableCopy(variables));
97
+ const factory = () => {
98
+ const [networkRequest, disposeNetworkRequest] = maybeMakeNetworkRequest(
99
+ environment,
100
+ entrypoint,
101
+ variables,
102
+ fetchOptions,
103
+ );
104
+
105
+ const itemCleanupPair: ItemCleanupPair<
106
+ FragmentReference<TReadFromStore, TClientFieldValue>
107
+ > = [
108
+ {
109
+ kind: 'FragmentReference',
110
+ readerWithRefetchQueries: wrapResolvedValue({
111
+ kind: 'ReaderWithRefetchQueries',
112
+ readerArtifact: entrypoint.readerWithRefetchQueries.readerArtifact,
113
+ nestedRefetchQueries:
114
+ entrypoint.readerWithRefetchQueries.nestedRefetchQueries,
115
+ }),
116
+ root: { __link: ROOT_ID, __typename: entrypoint.concreteType },
117
+ variables,
118
+ networkRequest: networkRequest,
119
+ },
120
+ disposeNetworkRequest,
121
+ ];
122
+ return itemCleanupPair;
123
+ };
124
+ return getOrCreateItemInSuspenseCache(environment, cacheKey, factory);
125
+ }
126
+
127
+ type NetworkResponseScalarValue = string | number | boolean;
128
+ type NetworkResponseValue =
129
+ | NetworkResponseScalarValue
130
+ | null
131
+ | NetworkResponseObject
132
+ | (NetworkResponseObject | null)[]
133
+ | (NetworkResponseScalarValue | null)[];
134
+
135
+ export type NetworkResponseObject = {
136
+ // N.B. undefined is here to support optional id's, but
137
+ // undefined should not *actually* be present in the network response.
138
+ [index: string]: undefined | NetworkResponseValue;
139
+ id?: DataId;
140
+ __typename?: TypeName;
141
+ };
142
+
143
+ export function normalizeData(
144
+ environment: IsographEnvironment,
145
+ normalizationAst: NormalizationAst,
146
+ networkResponse: NetworkResponseObject,
147
+ variables: Variables,
148
+ nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
149
+ root: Link,
150
+ ): EncounteredIds {
151
+ const encounteredIds: EncounteredIds = new Map();
152
+
153
+ logMessage(environment, {
154
+ kind: 'AboutToNormalize',
155
+ normalizationAst,
156
+ networkResponse,
157
+ variables,
158
+ });
159
+
160
+ const recordsById = (environment.store[root.__typename] ??= {});
161
+ const newStoreRecord = (recordsById[root.__link] ??= {});
162
+
163
+ normalizeDataIntoRecord(
164
+ environment,
165
+ normalizationAst,
166
+ networkResponse,
167
+ newStoreRecord,
168
+ root,
169
+ variables,
170
+ nestedRefetchQueries,
171
+ encounteredIds,
172
+ );
173
+
174
+ logMessage(environment, {
175
+ kind: 'AfterNormalization',
176
+ store: environment.store,
177
+ encounteredIds,
178
+ });
179
+
180
+ callSubscriptions(environment, encounteredIds);
181
+ return encounteredIds;
182
+ }
183
+
184
+ export function subscribeToAnyChange(
185
+ environment: IsographEnvironment,
186
+ callback: () => void,
187
+ ): () => void {
188
+ const subscription = {
189
+ kind: 'AnyRecords',
190
+ callback,
191
+ } as const;
192
+ environment.subscriptions.add(subscription);
193
+ return () => environment.subscriptions.delete(subscription);
194
+ }
195
+
196
+ export function subscribeToAnyChangesToRecord(
197
+ environment: IsographEnvironment,
198
+ recordLink: Link,
199
+ callback: () => void,
200
+ ): () => void {
201
+ const subscription = {
202
+ kind: 'AnyChangesToRecord',
203
+ recordLink,
204
+ callback,
205
+ } as const;
206
+ environment.subscriptions.add(subscription);
207
+ return () => environment.subscriptions.delete(subscription);
208
+ }
209
+
210
+ // TODO we should re-read and call callback if the value has changed
211
+ export function subscribe<
212
+ TReadFromStore extends { parameters: object; data: object },
213
+ >(
214
+ environment: IsographEnvironment,
215
+ encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
216
+ fragmentReference: FragmentReference<TReadFromStore, any>,
217
+ callback: (
218
+ newEncounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
219
+ ) => void,
220
+ readerAst: ReaderAst<TReadFromStore>,
221
+ ): () => void {
222
+ const fragmentSubscription: FragmentSubscription<TReadFromStore> = {
223
+ kind: 'FragmentSubscription',
224
+ callback,
225
+ encounteredDataAndRecords,
226
+ fragmentReference,
227
+ readerAst,
228
+ };
229
+ environment.subscriptions.add(fragmentSubscription);
230
+ return () => environment.subscriptions.delete(fragmentSubscription);
231
+ }
232
+
233
+ export function onNextChangeToRecord(
234
+ environment: IsographEnvironment,
235
+ recordLink: Link,
236
+ ): Promise<void> {
237
+ return new Promise((resolve) => {
238
+ const unsubscribe = subscribeToAnyChangesToRecord(
239
+ environment,
240
+ recordLink,
241
+ () => {
242
+ unsubscribe();
243
+ resolve();
244
+ },
245
+ );
246
+ });
247
+ }
248
+
249
+ // Calls to readButDoNotEvaluate can suspend (i.e. throw a promise).
250
+ // Maybe in the future, they will be able to throw errors.
251
+ //
252
+ // That's probably okay to ignore. We don't, however, want to prevent
253
+ // updating other subscriptions if one subscription had missing data.
254
+ function withErrorHandling<T>(f: (t: T) => void): (t: T) => void {
255
+ return (t) => {
256
+ try {
257
+ return f(t);
258
+ } catch {}
259
+ };
260
+ }
261
+
262
+ function callSubscriptions(
263
+ environment: IsographEnvironment,
264
+ recordsEncounteredWhenNormalizing: EncounteredIds,
265
+ ) {
266
+ environment.subscriptions.forEach(
267
+ withErrorHandling((subscription) => {
268
+ switch (subscription.kind) {
269
+ case 'FragmentSubscription': {
270
+ // TODO if there are multiple components subscribed to the same
271
+ // fragment, we will call readButNotEvaluate multiple times. We
272
+ // should fix that.
273
+ if (
274
+ hasOverlappingIds(
275
+ recordsEncounteredWhenNormalizing,
276
+ subscription.encounteredDataAndRecords.encounteredRecords,
277
+ )
278
+ ) {
279
+ const newEncounteredDataAndRecords = readButDoNotEvaluate(
280
+ environment,
281
+ subscription.fragmentReference,
282
+ // Is this wrong?
283
+ // Reasons to think no:
284
+ // - we are only updating the read-out value, and the network
285
+ // options only affect whether we throw.
286
+ // - the component will re-render, and re-throw on its own, anyway.
287
+ //
288
+ // Reasons to think not:
289
+ // - it seems more efficient to suspend here and not update state,
290
+ // if we expect that the component will just throw anyway
291
+ // - consistency
292
+ // - it's also weird, this is called from makeNetworkRequest, where
293
+ // we don't currently pass network request options
294
+ {
295
+ suspendIfInFlight: false,
296
+ throwOnNetworkError: false,
297
+ },
298
+ );
299
+
300
+ const mergedItem = mergeObjectsUsingReaderAst(
301
+ subscription.readerAst,
302
+ subscription.encounteredDataAndRecords.item,
303
+ newEncounteredDataAndRecords.item,
304
+ );
305
+
306
+ logMessage(environment, {
307
+ kind: 'DeepEqualityCheck',
308
+ fragmentReference: subscription.fragmentReference,
309
+ old: subscription.encounteredDataAndRecords.item,
310
+ new: newEncounteredDataAndRecords.item,
311
+ deeplyEqual:
312
+ mergedItem === subscription.encounteredDataAndRecords.item,
313
+ });
314
+
315
+ if (mergedItem !== subscription.encounteredDataAndRecords.item) {
316
+ subscription.callback(newEncounteredDataAndRecords);
317
+ }
318
+ }
319
+ return;
320
+ }
321
+ case 'AnyRecords': {
322
+ subscription.callback();
323
+ return;
324
+ }
325
+ case 'AnyChangesToRecord': {
326
+ if (
327
+ recordsEncounteredWhenNormalizing
328
+ .get(subscription.recordLink.__typename)
329
+ ?.has(subscription.recordLink.__link)
330
+ ) {
331
+ subscription.callback();
332
+ }
333
+ return;
334
+ }
335
+ default: {
336
+ // Ensure we have covered all variants
337
+ const _: never = subscription;
338
+ _;
339
+ throw new Error('Unexpected case');
340
+ }
341
+ }
342
+ }),
343
+ );
344
+ }
345
+
346
+ function hasOverlappingIds(
347
+ ids1: EncounteredIds,
348
+ ids2: EncounteredIds,
349
+ ): boolean {
350
+ for (const [typeName, set1] of ids1.entries()) {
351
+ const set2 = ids2.get(typeName);
352
+ if (set2 === undefined) {
353
+ continue;
354
+ }
355
+
356
+ if (isNotDisjointFrom(set1, set2)) {
357
+ return true;
358
+ }
359
+ }
360
+ return false;
361
+ }
362
+
363
+ // TODO use a polyfill library
364
+ function isNotDisjointFrom<T>(set1: Set<T>, set2: Set<T>): boolean {
365
+ for (const id of set1) {
366
+ if (set2.has(id)) {
367
+ return true;
368
+ }
369
+ }
370
+ return false;
371
+ }
372
+
373
+ export type EncounteredIds = Map<TypeName, Set<DataId>>;
374
+ /**
375
+ * Mutate targetParentRecord according to the normalizationAst and networkResponseParentRecord.
376
+ */
377
+ function normalizeDataIntoRecord(
378
+ environment: IsographEnvironment,
379
+ normalizationAst: NormalizationAst,
380
+ networkResponseParentRecord: NetworkResponseObject,
381
+ targetParentRecord: StoreRecord,
382
+ targetParentRecordLink: Link,
383
+ variables: Variables,
384
+ nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
385
+ mutableEncounteredIds: EncounteredIds,
386
+ ): RecordHasBeenUpdated {
387
+ let recordHasBeenUpdated = false;
388
+ for (const normalizationNode of normalizationAst) {
389
+ switch (normalizationNode.kind) {
390
+ case 'Scalar': {
391
+ const scalarFieldResultedInChange = normalizeScalarField(
392
+ normalizationNode,
393
+ networkResponseParentRecord,
394
+ targetParentRecord,
395
+ variables,
396
+ );
397
+ recordHasBeenUpdated =
398
+ recordHasBeenUpdated || scalarFieldResultedInChange;
399
+ break;
400
+ }
401
+ case 'Linked': {
402
+ const linkedFieldResultedInChange = normalizeLinkedField(
403
+ environment,
404
+ normalizationNode,
405
+ networkResponseParentRecord,
406
+ targetParentRecord,
407
+ targetParentRecordLink,
408
+ variables,
409
+ nestedRefetchQueries,
410
+ mutableEncounteredIds,
411
+ );
412
+ recordHasBeenUpdated =
413
+ recordHasBeenUpdated || linkedFieldResultedInChange;
414
+ break;
415
+ }
416
+ case 'InlineFragment': {
417
+ const inlineFragmentResultedInChange = normalizeInlineFragment(
418
+ environment,
419
+ normalizationNode,
420
+ networkResponseParentRecord,
421
+ targetParentRecord,
422
+ targetParentRecordLink,
423
+ variables,
424
+ nestedRefetchQueries,
425
+ mutableEncounteredIds,
426
+ );
427
+ recordHasBeenUpdated =
428
+ recordHasBeenUpdated || inlineFragmentResultedInChange;
429
+ break;
430
+ }
431
+ default: {
432
+ // Ensure we have covered all variants
433
+ let _: never = normalizationNode;
434
+ _;
435
+ throw new Error('Unexpected normalization node kind');
436
+ }
437
+ }
438
+ }
439
+ if (recordHasBeenUpdated) {
440
+ let encounteredRecordsIds = insertIfNotExists(
441
+ mutableEncounteredIds,
442
+ targetParentRecordLink.__typename,
443
+ );
444
+
445
+ encounteredRecordsIds.add(targetParentRecordLink.__link);
446
+ }
447
+ return recordHasBeenUpdated;
448
+ }
449
+
450
+ export function insertIfNotExists<K, V>(map: Map<K, Set<V>>, key: K) {
451
+ let result = map.get(key);
452
+ if (result === undefined) {
453
+ result = new Set();
454
+ map.set(key, result);
455
+ }
456
+ return result;
457
+ }
458
+
459
+ type RecordHasBeenUpdated = boolean;
460
+ function normalizeScalarField(
461
+ astNode: NormalizationScalarField,
462
+ networkResponseParentRecord: NetworkResponseObject,
463
+ targetStoreRecord: StoreRecord,
464
+ variables: Variables,
465
+ ): RecordHasBeenUpdated {
466
+ const networkResponseKey = getNetworkResponseKey(astNode);
467
+ const networkResponseData = networkResponseParentRecord[networkResponseKey];
468
+ const parentRecordKey = getParentRecordKey(astNode, variables);
469
+
470
+ if (
471
+ networkResponseData == null ||
472
+ isScalarOrEmptyArray(networkResponseData)
473
+ ) {
474
+ const existingValue = targetStoreRecord[parentRecordKey];
475
+ targetStoreRecord[parentRecordKey] = networkResponseData;
476
+ return existingValue !== networkResponseData;
477
+ } else {
478
+ throw new Error('Unexpected object array when normalizing scalar');
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Mutate targetParentRecord with a given linked field ast node.
484
+ */
485
+ function normalizeLinkedField(
486
+ environment: IsographEnvironment,
487
+ astNode: NormalizationLinkedField,
488
+ networkResponseParentRecord: NetworkResponseObject,
489
+ targetParentRecord: StoreRecord,
490
+ targetParentRecordLink: Link,
491
+ variables: Variables,
492
+ nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
493
+ mutableEncounteredIds: EncounteredIds,
494
+ ): RecordHasBeenUpdated {
495
+ const networkResponseKey = getNetworkResponseKey(astNode);
496
+ const networkResponseData = networkResponseParentRecord[networkResponseKey];
497
+ const parentRecordKey = getParentRecordKey(astNode, variables);
498
+ const existingValue = targetParentRecord[parentRecordKey];
499
+
500
+ if (networkResponseData == null) {
501
+ targetParentRecord[parentRecordKey] = null;
502
+ return existingValue !== null;
503
+ }
504
+
505
+ if (
506
+ isScalarOrEmptyArray(networkResponseData) &&
507
+ !isNullOrEmptyArray(networkResponseData)
508
+ ) {
509
+ throw new Error(
510
+ 'Unexpected scalar network response when normalizing a linked field',
511
+ );
512
+ }
513
+
514
+ if (Array.isArray(networkResponseData)) {
515
+ // TODO check astNode.plural or the like
516
+ const dataIds: (Link | null)[] = [];
517
+ for (let i = 0; i < networkResponseData.length; i++) {
518
+ const networkResponseObject = networkResponseData[i];
519
+ if (networkResponseObject == null) {
520
+ dataIds.push(null);
521
+ continue;
522
+ }
523
+ const newStoreRecordId = normalizeNetworkResponseObject(
524
+ environment,
525
+ astNode,
526
+ networkResponseObject,
527
+ targetParentRecordLink,
528
+ variables,
529
+ i,
530
+ nestedRefetchQueries,
531
+ mutableEncounteredIds,
532
+ );
533
+
534
+ const __typename =
535
+ astNode.concreteType ?? networkResponseObject[TYPENAME_FIELD_NAME];
536
+ if (__typename == null) {
537
+ throw new Error(
538
+ 'Unexpected missing __typename in network response when normalizing a linked field. ' +
539
+ 'This is indicative of a bug in Isograph.',
540
+ );
541
+ }
542
+ dataIds.push({
543
+ __link: newStoreRecordId,
544
+ __typename,
545
+ });
546
+ }
547
+ targetParentRecord[parentRecordKey] = dataIds;
548
+ return !dataIdsAreTheSame(existingValue, dataIds);
549
+ } else {
550
+ const newStoreRecordId = normalizeNetworkResponseObject(
551
+ environment,
552
+ astNode,
553
+ networkResponseData,
554
+ targetParentRecordLink,
555
+ variables,
556
+ null,
557
+ nestedRefetchQueries,
558
+ mutableEncounteredIds,
559
+ );
560
+
561
+ let __typename =
562
+ astNode.concreteType ?? networkResponseData[TYPENAME_FIELD_NAME];
563
+
564
+ if (__typename == null) {
565
+ throw new Error(
566
+ 'Unexpected missing __typename in network response when normalizing a linked field. ' +
567
+ 'This is indicative of a bug in Isograph.',
568
+ );
569
+ }
570
+
571
+ targetParentRecord[parentRecordKey] = {
572
+ __link: newStoreRecordId,
573
+ __typename,
574
+ };
575
+
576
+ const link = getLink(existingValue);
577
+ return link?.__link !== newStoreRecordId || link.__typename !== __typename;
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Mutate targetParentRecord with a given linked field ast node.
583
+ */
584
+ function normalizeInlineFragment(
585
+ environment: IsographEnvironment,
586
+ astNode: NormalizationInlineFragment,
587
+ networkResponseParentRecord: NetworkResponseObject,
588
+ targetParentRecord: StoreRecord,
589
+ targetParentRecordLink: Link,
590
+ variables: Variables,
591
+ nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
592
+ mutableEncounteredIds: EncounteredIds,
593
+ ): RecordHasBeenUpdated {
594
+ const typeToRefineTo = astNode.type;
595
+ if (networkResponseParentRecord[TYPENAME_FIELD_NAME] === typeToRefineTo) {
596
+ const hasBeenModified = normalizeDataIntoRecord(
597
+ environment,
598
+ astNode.selections,
599
+ networkResponseParentRecord,
600
+ targetParentRecord,
601
+ targetParentRecordLink,
602
+ variables,
603
+ nestedRefetchQueries,
604
+ mutableEncounteredIds,
605
+ );
606
+ return hasBeenModified;
607
+ }
608
+ return false;
609
+ }
610
+
611
+ function dataIdsAreTheSame(
612
+ existingValue: DataTypeValue,
613
+ newDataIds: (Link | null)[],
614
+ ): boolean {
615
+ if (Array.isArray(existingValue)) {
616
+ if (newDataIds.length !== existingValue.length) {
617
+ return false;
618
+ }
619
+ for (let i = 0; i < newDataIds.length; i++) {
620
+ const maybeLink = getLink(existingValue[i]);
621
+ if (
622
+ newDataIds[i]?.__link !== maybeLink?.__link ||
623
+ newDataIds[i]?.__typename !== maybeLink?.__typename
624
+ ) {
625
+ return false;
626
+ }
627
+ }
628
+ return true;
629
+ } else {
630
+ return false;
631
+ }
632
+ }
633
+
634
+ function normalizeNetworkResponseObject(
635
+ environment: IsographEnvironment,
636
+ astNode: NormalizationLinkedField,
637
+ networkResponseData: NetworkResponseObject,
638
+ targetParentRecordLink: Link,
639
+ variables: Variables,
640
+ index: number | null,
641
+ nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
642
+ mutableEncounteredIds: EncounteredIds,
643
+ ): DataId /* The id of the modified or newly created item */ {
644
+ const newStoreRecordId = getDataIdOfNetworkResponse(
645
+ targetParentRecordLink,
646
+ networkResponseData,
647
+ astNode,
648
+ variables,
649
+ index,
650
+ );
651
+ const __typename =
652
+ astNode.concreteType ?? networkResponseData[TYPENAME_FIELD_NAME];
653
+
654
+ if (__typename == null) {
655
+ throw new Error(
656
+ 'Unexpected missing __typename in network response object. ' +
657
+ 'This is indicative of a bug in Isograph.',
658
+ );
659
+ }
660
+
661
+ const recordsById = (environment.store[__typename] ??= {});
662
+ const newStoreRecord = (recordsById[newStoreRecordId] ??= {});
663
+
664
+ normalizeDataIntoRecord(
665
+ environment,
666
+ astNode.selections,
667
+ networkResponseData,
668
+ newStoreRecord,
669
+ { __link: newStoreRecordId, __typename: __typename },
670
+ variables,
671
+ nestedRefetchQueries,
672
+ mutableEncounteredIds,
673
+ );
674
+
675
+ return newStoreRecordId;
676
+ }
677
+
678
+ function isScalarOrEmptyArray(
679
+ data: NonNullable<NetworkResponseValue>,
680
+ ): data is NetworkResponseScalarValue | (NetworkResponseScalarValue | null)[] {
681
+ // N.B. empty arrays count as empty arrays of scalar fields.
682
+ if (Array.isArray(data)) {
683
+ // This is maybe fixed in a new version of Typescript??
684
+ return (data as any).every((x: any) => isScalarOrEmptyArray(x));
685
+ }
686
+ const isScalarValue =
687
+ data === null ||
688
+ typeof data === 'string' ||
689
+ typeof data === 'number' ||
690
+ typeof data === 'boolean';
691
+ return isScalarValue;
692
+ }
693
+
694
+ function isNullOrEmptyArray(data: unknown): data is never[] | null[] | null {
695
+ if (Array.isArray(data)) {
696
+ if (data.length === 0) {
697
+ return true;
698
+ }
699
+ return data.every((x) => isNullOrEmptyArray(x));
700
+ }
701
+
702
+ return data === null;
703
+ }
704
+
705
+ export function getParentRecordKey(
706
+ astNode:
707
+ | NormalizationLinkedField
708
+ | NormalizationScalarField
709
+ | ReaderLinkedField
710
+ | ReaderScalarField,
711
+ variables: Variables,
712
+ ): string {
713
+ let parentRecordKey = astNode.fieldName;
714
+ const fieldParameters = astNode.arguments;
715
+ if (fieldParameters != null) {
716
+ for (const fieldParameter of fieldParameters) {
717
+ parentRecordKey += getStoreKeyChunkForArgument(fieldParameter, variables);
718
+ }
719
+ }
720
+
721
+ return parentRecordKey;
722
+ }
723
+
724
+ function getStoreKeyChunkForArgumentValue(
725
+ argumentValue: ArgumentValue,
726
+ variables: Variables,
727
+ ) {
728
+ switch (argumentValue.kind) {
729
+ case 'Literal': {
730
+ return argumentValue.value;
731
+ }
732
+ case 'Variable': {
733
+ return variables[argumentValue.name] ?? 'null';
734
+ }
735
+ case 'String': {
736
+ return argumentValue.value;
737
+ }
738
+ case 'Enum': {
739
+ return argumentValue.value;
740
+ }
741
+ default: {
742
+ // TODO configure eslint to allow unused vars starting with _
743
+ // Ensure we have covered all variants
744
+ const _: never = argumentValue;
745
+ _;
746
+ throw new Error('Unexpected case');
747
+ }
748
+ }
749
+ }
750
+
751
+ function getStoreKeyChunkForArgument(argument: Argument, variables: Variables) {
752
+ const chunk = getStoreKeyChunkForArgumentValue(argument[1], variables);
753
+ return `${FIRST_SPLIT_KEY}${argument[0]}${SECOND_SPLIT_KEY}${chunk}`;
754
+ }
755
+
756
+ function getNetworkResponseKey(
757
+ astNode: NormalizationLinkedField | NormalizationScalarField,
758
+ ): string {
759
+ let networkResponseKey = astNode.fieldName;
760
+ const fieldParameters = astNode.arguments;
761
+ if (fieldParameters != null) {
762
+ for (const fieldParameter of fieldParameters) {
763
+ const [argumentName, argumentValue] = fieldParameter;
764
+ let argumentValueChunk;
765
+ switch (argumentValue.kind) {
766
+ case 'Literal': {
767
+ argumentValueChunk = 'l_' + argumentValue.value;
768
+ break;
769
+ }
770
+ case 'Variable': {
771
+ argumentValueChunk = 'v_' + argumentValue.name;
772
+ break;
773
+ }
774
+ case 'String': {
775
+ argumentValueChunk = 's_' + argumentValue.value;
776
+ break;
777
+ }
778
+ case 'Enum': {
779
+ argumentValueChunk = 'e_' + argumentValue.value;
780
+ break;
781
+ }
782
+ default: {
783
+ // Ensure we have covered all variants
784
+ let _: never = argumentValue;
785
+ _;
786
+ throw new Error('Unexpected case');
787
+ }
788
+ }
789
+ networkResponseKey += `${FIRST_SPLIT_KEY}${argumentName}${SECOND_SPLIT_KEY}${argumentValueChunk}`;
790
+ }
791
+ }
792
+ return networkResponseKey;
793
+ }
794
+
795
+ // an alias might be pullRequests____first___first____after___cursor
796
+ export const FIRST_SPLIT_KEY = '____';
797
+ export const SECOND_SPLIT_KEY = '___';
798
+
799
+ // Returns a key to look up an item in the store
800
+ function getDataIdOfNetworkResponse(
801
+ parentRecordLink: Link,
802
+ dataToNormalize: NetworkResponseObject,
803
+ astNode: NormalizationLinkedField,
804
+ variables: Variables,
805
+ index: number | null,
806
+ ): DataId {
807
+ // If we are dealing with nested Query, use __ROOT as id
808
+ // TODO do not hard code this value here
809
+ if (astNode.concreteType === 'Query') {
810
+ return ROOT_ID;
811
+ }
812
+
813
+ // Check whether the dataToNormalize has an id field. If so, that is the key.
814
+ // If not, we construct an id from the parentRecordId and the field parameters.
815
+
816
+ const dataId = dataToNormalize.id;
817
+ if (dataId != null) {
818
+ return dataId;
819
+ }
820
+
821
+ let storeKey = `${parentRecordLink.__typename}:${parentRecordLink.__link}.${astNode.fieldName}`;
822
+ if (index != null) {
823
+ storeKey += `.${index}`;
824
+ }
825
+
826
+ const fieldParameters = astNode.arguments;
827
+ if (fieldParameters == null) {
828
+ return storeKey;
829
+ }
830
+
831
+ for (const fieldParameter of fieldParameters) {
832
+ storeKey += getStoreKeyChunkForArgument(fieldParameter, variables);
833
+ }
834
+ return storeKey;
835
+ }