@isograph/react 0.2.0 → 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 (134) hide show
  1. package/dist/core/FragmentReference.d.ts +14 -4
  2. package/dist/core/FragmentReference.d.ts.map +1 -0
  3. package/dist/core/FragmentReference.js +2 -3
  4. package/dist/core/IsographEnvironment.d.ts +28 -10
  5. package/dist/core/IsographEnvironment.d.ts.map +1 -0
  6. package/dist/core/IsographEnvironment.js +15 -22
  7. package/dist/core/PromiseWrapper.d.ts +1 -0
  8. package/dist/core/PromiseWrapper.d.ts.map +1 -0
  9. package/dist/core/PromiseWrapper.js +4 -5
  10. package/dist/core/areEqualWithDeepComparison.d.ts +5 -3
  11. package/dist/core/areEqualWithDeepComparison.d.ts.map +1 -0
  12. package/dist/core/areEqualWithDeepComparison.js +73 -39
  13. package/dist/core/cache.d.ts +26 -10
  14. package/dist/core/cache.d.ts.map +1 -0
  15. package/dist/core/cache.js +160 -98
  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 +1 -1
  20. package/dist/core/componentCache.d.ts.map +1 -0
  21. package/dist/core/componentCache.js +14 -14
  22. package/dist/core/entrypoint.d.ts +27 -8
  23. package/dist/core/entrypoint.d.ts.map +1 -0
  24. package/dist/core/entrypoint.js +1 -2
  25. package/dist/core/garbageCollection.d.ts +3 -1
  26. package/dist/core/garbageCollection.d.ts.map +1 -0
  27. package/dist/core/garbageCollection.js +48 -15
  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 +4 -1
  32. package/dist/core/makeNetworkRequest.d.ts.map +1 -0
  33. package/dist/core/makeNetworkRequest.js +71 -15
  34. package/dist/core/read.d.ts +20 -5
  35. package/dist/core/read.d.ts.map +1 -0
  36. package/dist/core/read.js +104 -41
  37. package/dist/core/reader.d.ts +34 -10
  38. package/dist/core/reader.d.ts.map +1 -0
  39. package/dist/core/util.d.ts +2 -0
  40. package/dist/core/util.d.ts.map +1 -0
  41. package/dist/index.d.ts +10 -5
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +10 -2
  44. package/dist/loadable-hooks/useClientSideDefer.d.ts +15 -3
  45. package/dist/loadable-hooks/useClientSideDefer.d.ts.map +1 -0
  46. package/dist/loadable-hooks/useClientSideDefer.js +4 -6
  47. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts +34 -0
  48. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts.map +1 -0
  49. package/dist/loadable-hooks/useConnectionSpecPagination.js +160 -0
  50. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts +1 -0
  51. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts.map +1 -0
  52. package/dist/loadable-hooks/useImperativeExposedMutationField.js +1 -2
  53. package/dist/loadable-hooks/useImperativeLoadableField.d.ts +13 -5
  54. package/dist/loadable-hooks/useImperativeLoadableField.d.ts.map +1 -0
  55. package/dist/loadable-hooks/useImperativeLoadableField.js +3 -4
  56. package/dist/loadable-hooks/useSkipLimitPagination.d.ts +18 -24
  57. package/dist/loadable-hooks/useSkipLimitPagination.d.ts.map +1 -0
  58. package/dist/loadable-hooks/useSkipLimitPagination.js +88 -44
  59. package/dist/react/FragmentReader.d.ts +7 -4
  60. package/dist/react/FragmentReader.d.ts.map +1 -0
  61. package/dist/react/FragmentReader.js +4 -2
  62. package/dist/react/IsographEnvironmentProvider.d.ts +1 -0
  63. package/dist/react/IsographEnvironmentProvider.d.ts.map +1 -0
  64. package/dist/react/IsographEnvironmentProvider.js +3 -3
  65. package/dist/react/RenderAfterCommit__DO_NOT_USE.d.ts +10 -0
  66. package/dist/react/RenderAfterCommit__DO_NOT_USE.d.ts.map +1 -0
  67. package/dist/react/RenderAfterCommit__DO_NOT_USE.js +15 -0
  68. package/dist/react/useImperativeReference.d.ts +8 -3
  69. package/dist/react/useImperativeReference.d.ts.map +1 -0
  70. package/dist/react/useImperativeReference.js +4 -5
  71. package/dist/react/useLazyReference.d.ts +7 -2
  72. package/dist/react/useLazyReference.d.ts.map +1 -0
  73. package/dist/react/useLazyReference.js +11 -4
  74. package/dist/react/useReadAndSubscribe.d.ts +12 -3
  75. package/dist/react/useReadAndSubscribe.d.ts.map +1 -0
  76. package/dist/react/useReadAndSubscribe.js +6 -7
  77. package/dist/react/useRerenderOnChange.d.ts +6 -1
  78. package/dist/react/useRerenderOnChange.d.ts.map +1 -0
  79. package/dist/react/useRerenderOnChange.js +3 -4
  80. package/dist/react/useResult.d.ts +5 -1
  81. package/dist/react/useResult.d.ts.map +1 -0
  82. package/dist/react/useResult.js +8 -5
  83. package/{src/tests/isograph.config.json → isograph.config.json} +1 -1
  84. package/package.json +12 -8
  85. package/{src/tests/schema.graphql → schema.graphql} +1 -0
  86. package/src/core/FragmentReference.ts +17 -5
  87. package/src/core/IsographEnvironment.ts +38 -29
  88. package/src/core/areEqualWithDeepComparison.ts +76 -42
  89. package/src/core/cache.ts +237 -123
  90. package/src/core/check.ts +207 -0
  91. package/src/core/componentCache.ts +18 -17
  92. package/src/core/entrypoint.ts +15 -8
  93. package/src/core/garbageCollection.ts +71 -20
  94. package/src/core/logging.ts +116 -0
  95. package/src/core/makeNetworkRequest.ts +89 -13
  96. package/src/core/read.ts +162 -55
  97. package/src/core/reader.ts +40 -13
  98. package/src/core/util.ts +4 -0
  99. package/src/index.ts +14 -1
  100. package/src/loadable-hooks/useClientSideDefer.ts +45 -15
  101. package/src/loadable-hooks/useConnectionSpecPagination.ts +331 -0
  102. package/src/loadable-hooks/useImperativeLoadableField.ts +36 -10
  103. package/src/loadable-hooks/useSkipLimitPagination.ts +231 -90
  104. package/src/react/FragmentReader.tsx +13 -4
  105. package/src/react/RenderAfterCommit__DO_NOT_USE.tsx +17 -0
  106. package/src/react/useImperativeReference.ts +18 -7
  107. package/src/react/useLazyReference.ts +24 -4
  108. package/src/react/useReadAndSubscribe.ts +20 -5
  109. package/src/react/useRerenderOnChange.ts +6 -1
  110. package/src/react/useResult.ts +10 -2
  111. package/src/tests/__isograph/Query/meName/entrypoint.ts +7 -2
  112. package/src/tests/__isograph/Query/meName/param_type.ts +5 -2
  113. package/src/tests/__isograph/Query/meName/resolver_reader.ts +1 -0
  114. package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +9 -2
  115. package/src/tests/__isograph/Query/meNameSuccessor/param_type.ts +9 -6
  116. package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +3 -0
  117. package/src/tests/__isograph/Query/nodeField/entrypoint.ts +13 -2
  118. package/src/tests/__isograph/Query/nodeField/param_type.ts +7 -3
  119. package/src/tests/__isograph/Query/nodeField/parameters_type.ts +3 -0
  120. package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +1 -0
  121. package/src/tests/__isograph/Query/subquery/entrypoint.ts +67 -0
  122. package/src/tests/__isograph/Query/subquery/output_type.ts +3 -0
  123. package/src/tests/__isograph/Query/subquery/param_type.ts +12 -0
  124. package/src/tests/__isograph/Query/subquery/parameters_type.ts +3 -0
  125. package/src/tests/__isograph/Query/subquery/resolver_reader.ts +47 -0
  126. package/src/tests/__isograph/iso.ts +22 -11
  127. package/src/tests/garbageCollection.test.ts +45 -39
  128. package/src/tests/meNameSuccessor.ts +8 -3
  129. package/src/tests/nodeQuery.ts +6 -4
  130. package/src/tests/normalizeData.test.ts +120 -0
  131. package/src/tests/tsconfig.json +3 -3
  132. package/tsconfig.json +2 -2
  133. package/tsconfig.pkg.json +6 -1
  134. package/vitest.config.ts +20 -0
package/src/core/cache.ts CHANGED
@@ -5,13 +5,14 @@ import {
5
5
  } from '@isograph/react-disposable-state';
6
6
  import {
7
7
  DataId,
8
+ Link,
8
9
  ROOT_ID,
9
10
  StoreRecord,
10
- Link,
11
11
  type IsographEnvironment,
12
12
  DataTypeValue,
13
13
  getLink,
14
14
  FragmentSubscription,
15
+ type TypeName,
15
16
  } from './IsographEnvironment';
16
17
  import {
17
18
  IsographEntrypoint,
@@ -21,32 +22,37 @@ import {
21
22
  NormalizationScalarField,
22
23
  RefetchQueryNormalizationArtifactWrapper,
23
24
  } from '../core/entrypoint';
24
- import { ReaderLinkedField, ReaderScalarField } from './reader';
25
+ import { ReaderLinkedField, ReaderScalarField, type ReaderAst } from './reader';
25
26
  import { Argument, ArgumentValue } from './util';
26
27
  import { WithEncounteredRecords, readButDoNotEvaluate } from './read';
27
- import { FragmentReference, Variables } from './FragmentReference';
28
- import { areEqualObjectsWithDeepComparison } from './areEqualWithDeepComparison';
29
- import { makeNetworkRequest } from './makeNetworkRequest';
28
+ import {
29
+ FragmentReference,
30
+ Variables,
31
+ ExtractParameters,
32
+ } from './FragmentReference';
33
+ import { mergeObjectsUsingReaderAst } from './areEqualWithDeepComparison';
34
+ import { maybeMakeNetworkRequest } from './makeNetworkRequest';
30
35
  import { wrapResolvedValue } from './PromiseWrapper';
36
+ import { logMessage } from './logging';
37
+ import { FetchOptions } from './check';
31
38
 
32
- const TYPENAME_FIELD_NAME = '__typename';
39
+ export const TYPENAME_FIELD_NAME = '__typename';
33
40
 
34
41
  export function getOrCreateItemInSuspenseCache<
35
- TReadFromStore extends Object,
42
+ TReadFromStore extends { parameters: object; data: object },
36
43
  TClientFieldValue,
37
44
  >(
38
45
  environment: IsographEnvironment,
39
46
  index: string,
40
47
  factory: Factory<FragmentReference<TReadFromStore, TClientFieldValue>>,
41
48
  ): ParentCache<FragmentReference<TReadFromStore, TClientFieldValue>> {
42
- // @ts-expect-error
43
- if (typeof window !== 'undefined' && window.__LOG) {
44
- console.log('getting cache for', {
45
- index,
46
- cache: Object.keys(environment.fragmentCache),
47
- found: !!environment.fragmentCache[index],
48
- });
49
- }
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
+ });
50
56
  if (environment.fragmentCache[index] == null) {
51
57
  environment.fragmentCache[index] = new ParentCache(factory);
52
58
  }
@@ -77,20 +83,25 @@ export function stableCopy<T>(value: T): T {
77
83
  }
78
84
 
79
85
  export function getOrCreateCacheForArtifact<
80
- TReadFromStore extends Object,
86
+ TReadFromStore extends { parameters: object; data: object },
81
87
  TClientFieldValue,
82
88
  >(
83
89
  environment: IsographEnvironment,
84
90
  entrypoint: IsographEntrypoint<TReadFromStore, TClientFieldValue>,
85
- variables: Variables,
91
+ variables: ExtractParameters<TReadFromStore>,
92
+ fetchOptions?: FetchOptions,
86
93
  ): ParentCache<FragmentReference<TReadFromStore, TClientFieldValue>> {
87
- const cacheKey = entrypoint.queryText + JSON.stringify(stableCopy(variables));
94
+ const cacheKey =
95
+ entrypoint.networkRequestInfo.queryText +
96
+ JSON.stringify(stableCopy(variables));
88
97
  const factory = () => {
89
- const [networkRequest, disposeNetworkRequest] = makeNetworkRequest(
98
+ const [networkRequest, disposeNetworkRequest] = maybeMakeNetworkRequest(
90
99
  environment,
91
100
  entrypoint,
92
101
  variables,
102
+ fetchOptions,
93
103
  );
104
+
94
105
  const itemCleanupPair: ItemCleanupPair<
95
106
  FragmentReference<TReadFromStore, TClientFieldValue>
96
107
  > = [
@@ -102,7 +113,7 @@ export function getOrCreateCacheForArtifact<
102
113
  nestedRefetchQueries:
103
114
  entrypoint.readerWithRefetchQueries.nestedRefetchQueries,
104
115
  }),
105
- root: ROOT_ID,
116
+ root: { __link: ROOT_ID, __typename: entrypoint.concreteType },
106
117
  variables,
107
118
  networkRequest: networkRequest,
108
119
  },
@@ -118,13 +129,15 @@ type NetworkResponseValue =
118
129
  | NetworkResponseScalarValue
119
130
  | null
120
131
  | NetworkResponseObject
121
- | NetworkResponseObject[]
122
- | NetworkResponseScalarValue[];
123
- type NetworkResponseObject = {
132
+ | (NetworkResponseObject | null)[]
133
+ | (NetworkResponseScalarValue | null)[];
134
+
135
+ export type NetworkResponseObject = {
124
136
  // N.B. undefined is here to support optional id's, but
125
137
  // undefined should not *actually* be present in the network response.
126
138
  [index: string]: undefined | NetworkResponseValue;
127
139
  id?: DataId;
140
+ __typename?: TypeName;
128
141
  };
129
142
 
130
143
  export function normalizeData(
@@ -133,36 +146,37 @@ export function normalizeData(
133
146
  networkResponse: NetworkResponseObject,
134
147
  variables: Variables,
135
148
  nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
136
- ): Set<DataId> {
137
- const encounteredIds = new Set<DataId>();
138
-
139
- // @ts-expect-error
140
- if (typeof window !== 'undefined' && window.__LOG) {
141
- console.log(
142
- 'about to normalize',
143
- normalizationAst,
144
- networkResponse,
145
- variables,
146
- );
147
- }
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
+
148
163
  normalizeDataIntoRecord(
149
164
  environment,
150
165
  normalizationAst,
151
166
  networkResponse,
152
- environment.store.__ROOT,
153
- ROOT_ID,
154
- variables as any,
167
+ newStoreRecord,
168
+ root,
169
+ variables,
155
170
  nestedRefetchQueries,
156
171
  encounteredIds,
157
172
  );
158
- // @ts-expect-error
159
- if (typeof window !== 'undefined' && window.__LOG) {
160
- console.log('after normalization', {
161
- store: environment.store,
162
- encounteredIds,
163
- environment,
164
- });
165
- }
173
+
174
+ logMessage(environment, {
175
+ kind: 'AfterNormalization',
176
+ store: environment.store,
177
+ encounteredIds,
178
+ });
179
+
166
180
  callSubscriptions(environment, encounteredIds);
167
181
  return encounteredIds;
168
182
  }
@@ -179,33 +193,56 @@ export function subscribeToAnyChange(
179
193
  return () => environment.subscriptions.delete(subscription);
180
194
  }
181
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
+
182
210
  // TODO we should re-read and call callback if the value has changed
183
- export function subscribe<TReadFromStore extends Object>(
211
+ export function subscribe<
212
+ TReadFromStore extends { parameters: object; data: object },
213
+ >(
184
214
  environment: IsographEnvironment,
185
215
  encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
186
216
  fragmentReference: FragmentReference<TReadFromStore, any>,
187
217
  callback: (
188
218
  newEncounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
189
219
  ) => void,
220
+ readerAst: ReaderAst<TReadFromStore>,
190
221
  ): () => void {
191
222
  const fragmentSubscription: FragmentSubscription<TReadFromStore> = {
192
223
  kind: 'FragmentSubscription',
193
224
  callback,
194
225
  encounteredDataAndRecords,
195
226
  fragmentReference,
227
+ readerAst,
196
228
  };
197
- // @ts-expect-error
198
229
  environment.subscriptions.add(fragmentSubscription);
199
- // @ts-expect-error
200
230
  return () => environment.subscriptions.delete(fragmentSubscription);
201
231
  }
202
232
 
203
- export function onNextChange(environment: IsographEnvironment): Promise<void> {
233
+ export function onNextChangeToRecord(
234
+ environment: IsographEnvironment,
235
+ recordLink: Link,
236
+ ): Promise<void> {
204
237
  return new Promise((resolve) => {
205
- const unsubscribe = subscribeToAnyChange(environment, () => {
206
- unsubscribe();
207
- resolve();
208
- });
238
+ const unsubscribe = subscribeToAnyChangesToRecord(
239
+ environment,
240
+ recordLink,
241
+ () => {
242
+ unsubscribe();
243
+ resolve();
244
+ },
245
+ );
209
246
  });
210
247
  }
211
248
 
@@ -224,7 +261,7 @@ function withErrorHandling<T>(f: (t: T) => void): (t: T) => void {
224
261
 
225
262
  function callSubscriptions(
226
263
  environment: IsographEnvironment,
227
- recordsEncounteredWhenNormalizing: Set<DataId>,
264
+ recordsEncounteredWhenNormalizing: EncounteredIds,
228
265
  ) {
229
266
  environment.subscriptions.forEach(
230
267
  withErrorHandling((subscription) => {
@@ -260,36 +297,40 @@ function callSubscriptions(
260
297
  },
261
298
  );
262
299
 
263
- if (
264
- !areEqualObjectsWithDeepComparison(
265
- subscription.encounteredDataAndRecords.item,
266
- newEncounteredDataAndRecords.item,
267
- )
268
- ) {
269
- // @ts-expect-error
270
- if (typeof window !== 'undefined' && window.__LOG) {
271
- console.log('Deep equality - No', {
272
- fragmentReference: subscription.fragmentReference,
273
- old: subscription.encounteredDataAndRecords.item,
274
- new: newEncounteredDataAndRecords.item,
275
- });
276
- }
277
- // TODO deep compare values
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) {
278
316
  subscription.callback(newEncounteredDataAndRecords);
279
- } else {
280
- // @ts-expect-error
281
- if (typeof window !== 'undefined' && window.__LOG) {
282
- console.log('Deep equality - Yes', {
283
- fragmentReference: subscription.fragmentReference,
284
- old: subscription.encounteredDataAndRecords.item,
285
- });
286
- }
287
317
  }
288
318
  }
289
319
  return;
290
320
  }
291
321
  case 'AnyRecords': {
292
- return subscription.callback();
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;
293
334
  }
294
335
  default: {
295
336
  // Ensure we have covered all variants
@@ -302,7 +343,25 @@ function callSubscriptions(
302
343
  );
303
344
  }
304
345
 
305
- function hasOverlappingIds(set1: Set<DataId>, set2: Set<DataId>): boolean {
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 {
306
365
  for (const id of set1) {
307
366
  if (set2.has(id)) {
308
367
  return true;
@@ -311,6 +370,7 @@ function hasOverlappingIds(set1: Set<DataId>, set2: Set<DataId>): boolean {
311
370
  return false;
312
371
  }
313
372
 
373
+ export type EncounteredIds = Map<TypeName, Set<DataId>>;
314
374
  /**
315
375
  * Mutate targetParentRecord according to the normalizationAst and networkResponseParentRecord.
316
376
  */
@@ -319,10 +379,10 @@ function normalizeDataIntoRecord(
319
379
  normalizationAst: NormalizationAst,
320
380
  networkResponseParentRecord: NetworkResponseObject,
321
381
  targetParentRecord: StoreRecord,
322
- targetParentRecordId: DataId,
382
+ targetParentRecordLink: Link,
323
383
  variables: Variables,
324
384
  nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
325
- mutableEncounteredIds: Set<DataId>,
385
+ mutableEncounteredIds: EncounteredIds,
326
386
  ): RecordHasBeenUpdated {
327
387
  let recordHasBeenUpdated = false;
328
388
  for (const normalizationNode of normalizationAst) {
@@ -344,7 +404,7 @@ function normalizeDataIntoRecord(
344
404
  normalizationNode,
345
405
  networkResponseParentRecord,
346
406
  targetParentRecord,
347
- targetParentRecordId,
407
+ targetParentRecordLink,
348
408
  variables,
349
409
  nestedRefetchQueries,
350
410
  mutableEncounteredIds,
@@ -359,7 +419,7 @@ function normalizeDataIntoRecord(
359
419
  normalizationNode,
360
420
  networkResponseParentRecord,
361
421
  targetParentRecord,
362
- targetParentRecordId,
422
+ targetParentRecordLink,
363
423
  variables,
364
424
  nestedRefetchQueries,
365
425
  mutableEncounteredIds,
@@ -377,11 +437,25 @@ function normalizeDataIntoRecord(
377
437
  }
378
438
  }
379
439
  if (recordHasBeenUpdated) {
380
- mutableEncounteredIds.add(targetParentRecordId);
440
+ let encounteredRecordsIds = insertIfNotExists(
441
+ mutableEncounteredIds,
442
+ targetParentRecordLink.__typename,
443
+ );
444
+
445
+ encounteredRecordsIds.add(targetParentRecordLink.__link);
381
446
  }
382
447
  return recordHasBeenUpdated;
383
448
  }
384
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
+
385
459
  type RecordHasBeenUpdated = boolean;
386
460
  function normalizeScalarField(
387
461
  astNode: NormalizationScalarField,
@@ -413,10 +487,10 @@ function normalizeLinkedField(
413
487
  astNode: NormalizationLinkedField,
414
488
  networkResponseParentRecord: NetworkResponseObject,
415
489
  targetParentRecord: StoreRecord,
416
- targetParentRecordId: DataId,
490
+ targetParentRecordLink: Link,
417
491
  variables: Variables,
418
492
  nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
419
- mutableEncounteredIds: Set<DataId>,
493
+ mutableEncounteredIds: EncounteredIds,
420
494
  ): RecordHasBeenUpdated {
421
495
  const networkResponseKey = getNetworkResponseKey(astNode);
422
496
  const networkResponseData = networkResponseParentRecord[networkResponseKey];
@@ -428,7 +502,10 @@ function normalizeLinkedField(
428
502
  return existingValue !== null;
429
503
  }
430
504
 
431
- if (isScalarButNotEmptyArray(networkResponseData)) {
505
+ if (
506
+ isScalarOrEmptyArray(networkResponseData) &&
507
+ !isNullOrEmptyArray(networkResponseData)
508
+ ) {
432
509
  throw new Error(
433
510
  'Unexpected scalar network response when normalizing a linked field',
434
511
  );
@@ -436,21 +513,36 @@ function normalizeLinkedField(
436
513
 
437
514
  if (Array.isArray(networkResponseData)) {
438
515
  // TODO check astNode.plural or the like
439
- const dataIds: Link[] = [];
516
+ const dataIds: (Link | null)[] = [];
440
517
  for (let i = 0; i < networkResponseData.length; i++) {
441
518
  const networkResponseObject = networkResponseData[i];
519
+ if (networkResponseObject == null) {
520
+ dataIds.push(null);
521
+ continue;
522
+ }
442
523
  const newStoreRecordId = normalizeNetworkResponseObject(
443
524
  environment,
444
525
  astNode,
445
526
  networkResponseObject,
446
- targetParentRecordId,
527
+ targetParentRecordLink,
447
528
  variables,
448
529
  i,
449
530
  nestedRefetchQueries,
450
531
  mutableEncounteredIds,
451
532
  );
452
533
 
453
- dataIds.push({ __link: newStoreRecordId });
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
+ });
454
546
  }
455
547
  targetParentRecord[parentRecordKey] = dataIds;
456
548
  return !dataIdsAreTheSame(existingValue, dataIds);
@@ -459,18 +551,30 @@ function normalizeLinkedField(
459
551
  environment,
460
552
  astNode,
461
553
  networkResponseData,
462
- targetParentRecordId,
554
+ targetParentRecordLink,
463
555
  variables,
464
556
  null,
465
557
  nestedRefetchQueries,
466
558
  mutableEncounteredIds,
467
559
  );
468
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
+
469
571
  targetParentRecord[parentRecordKey] = {
470
572
  __link: newStoreRecordId,
573
+ __typename,
471
574
  };
575
+
472
576
  const link = getLink(existingValue);
473
- return link?.__link !== newStoreRecordId;
577
+ return link?.__link !== newStoreRecordId || link.__typename !== __typename;
474
578
  }
475
579
  }
476
580
 
@@ -482,10 +586,10 @@ function normalizeInlineFragment(
482
586
  astNode: NormalizationInlineFragment,
483
587
  networkResponseParentRecord: NetworkResponseObject,
484
588
  targetParentRecord: StoreRecord,
485
- targetParentRecordId: DataId,
589
+ targetParentRecordLink: Link,
486
590
  variables: Variables,
487
591
  nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
488
- mutableEncounteredIds: Set<DataId>,
592
+ mutableEncounteredIds: EncounteredIds,
489
593
  ): RecordHasBeenUpdated {
490
594
  const typeToRefineTo = astNode.type;
491
595
  if (networkResponseParentRecord[TYPENAME_FIELD_NAME] === typeToRefineTo) {
@@ -494,7 +598,7 @@ function normalizeInlineFragment(
494
598
  astNode.selections,
495
599
  networkResponseParentRecord,
496
600
  targetParentRecord,
497
- targetParentRecordId,
601
+ targetParentRecordLink,
498
602
  variables,
499
603
  nestedRefetchQueries,
500
604
  mutableEncounteredIds,
@@ -506,7 +610,7 @@ function normalizeInlineFragment(
506
610
 
507
611
  function dataIdsAreTheSame(
508
612
  existingValue: DataTypeValue,
509
- newDataIds: Link[],
613
+ newDataIds: (Link | null)[],
510
614
  ): boolean {
511
615
  if (Array.isArray(existingValue)) {
512
616
  if (newDataIds.length !== existingValue.length) {
@@ -514,10 +618,11 @@ function dataIdsAreTheSame(
514
618
  }
515
619
  for (let i = 0; i < newDataIds.length; i++) {
516
620
  const maybeLink = getLink(existingValue[i]);
517
- if (maybeLink !== null) {
518
- if (newDataIds[i].__link !== maybeLink.__link) {
519
- return false;
520
- }
621
+ if (
622
+ newDataIds[i]?.__link !== maybeLink?.__link ||
623
+ newDataIds[i]?.__typename !== maybeLink?.__typename
624
+ ) {
625
+ return false;
521
626
  }
522
627
  }
523
628
  return true;
@@ -530,29 +635,38 @@ function normalizeNetworkResponseObject(
530
635
  environment: IsographEnvironment,
531
636
  astNode: NormalizationLinkedField,
532
637
  networkResponseData: NetworkResponseObject,
533
- targetParentRecordId: string,
638
+ targetParentRecordLink: Link,
534
639
  variables: Variables,
535
640
  index: number | null,
536
641
  nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
537
- mutableEncounteredIds: Set<DataId>,
642
+ mutableEncounteredIds: EncounteredIds,
538
643
  ): DataId /* The id of the modified or newly created item */ {
539
644
  const newStoreRecordId = getDataIdOfNetworkResponse(
540
- targetParentRecordId,
645
+ targetParentRecordLink,
541
646
  networkResponseData,
542
647
  astNode,
543
648
  variables,
544
649
  index,
545
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
+ }
546
660
 
547
- const newStoreRecord = environment.store[newStoreRecordId] ?? {};
548
- environment.store[newStoreRecordId] = newStoreRecord;
661
+ const recordsById = (environment.store[__typename] ??= {});
662
+ const newStoreRecord = (recordsById[newStoreRecordId] ??= {});
549
663
 
550
664
  normalizeDataIntoRecord(
551
665
  environment,
552
666
  astNode.selections,
553
667
  networkResponseData,
554
668
  newStoreRecord,
555
- newStoreRecordId,
669
+ { __link: newStoreRecordId, __typename: __typename },
556
670
  variables,
557
671
  nestedRefetchQueries,
558
672
  mutableEncounteredIds,
@@ -563,35 +677,29 @@ function normalizeNetworkResponseObject(
563
677
 
564
678
  function isScalarOrEmptyArray(
565
679
  data: NonNullable<NetworkResponseValue>,
566
- ): data is NetworkResponseScalarValue | NetworkResponseScalarValue[] {
680
+ ): data is NetworkResponseScalarValue | (NetworkResponseScalarValue | null)[] {
567
681
  // N.B. empty arrays count as empty arrays of scalar fields.
568
682
  if (Array.isArray(data)) {
569
683
  // This is maybe fixed in a new version of Typescript??
570
684
  return (data as any).every((x: any) => isScalarOrEmptyArray(x));
571
685
  }
572
686
  const isScalarValue =
687
+ data === null ||
573
688
  typeof data === 'string' ||
574
689
  typeof data === 'number' ||
575
690
  typeof data === 'boolean';
576
691
  return isScalarValue;
577
692
  }
578
693
 
579
- function isScalarButNotEmptyArray(
580
- data: NonNullable<NetworkResponseValue>,
581
- ): data is NetworkResponseScalarValue | NetworkResponseScalarValue[] {
582
- // N.B. empty arrays count as empty arrays of linked fields.
694
+ function isNullOrEmptyArray(data: unknown): data is never[] | null[] | null {
583
695
  if (Array.isArray(data)) {
584
696
  if (data.length === 0) {
585
- return false;
697
+ return true;
586
698
  }
587
- // This is maybe fixed in a new version of Typescript??
588
- return (data as any).every((x: any) => isScalarOrEmptyArray(x));
699
+ return data.every((x) => isNullOrEmptyArray(x));
589
700
  }
590
- const isScalarValue =
591
- typeof data === 'string' ||
592
- typeof data === 'number' ||
593
- typeof data === 'boolean';
594
- return isScalarValue;
701
+
702
+ return data === null;
595
703
  }
596
704
 
597
705
  export function getParentRecordKey(
@@ -690,12 +798,18 @@ export const SECOND_SPLIT_KEY = '___';
690
798
 
691
799
  // Returns a key to look up an item in the store
692
800
  function getDataIdOfNetworkResponse(
693
- parentRecordId: DataId,
801
+ parentRecordLink: Link,
694
802
  dataToNormalize: NetworkResponseObject,
695
- astNode: NormalizationLinkedField | NormalizationScalarField,
803
+ astNode: NormalizationLinkedField,
696
804
  variables: Variables,
697
805
  index: number | null,
698
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
+
699
813
  // Check whether the dataToNormalize has an id field. If so, that is the key.
700
814
  // If not, we construct an id from the parentRecordId and the field parameters.
701
815
 
@@ -704,7 +818,7 @@ function getDataIdOfNetworkResponse(
704
818
  return dataId;
705
819
  }
706
820
 
707
- let storeKey = `${parentRecordId}.${astNode.fieldName}`;
821
+ let storeKey = `${parentRecordLink.__typename}:${parentRecordLink.__link}.${astNode.fieldName}`;
708
822
  if (index != null) {
709
823
  storeKey += `.${index}`;
710
824
  }