@isograph/react 0.2.0 → 0.3.1

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 (151) hide show
  1. package/.turbo/turbo-compile-typescript.log +4 -0
  2. package/dist/core/FragmentReference.d.ts +25 -6
  3. package/dist/core/FragmentReference.d.ts.map +1 -0
  4. package/dist/core/FragmentReference.js +3 -13
  5. package/dist/core/IsographEnvironment.d.ts +34 -26
  6. package/dist/core/IsographEnvironment.d.ts.map +1 -0
  7. package/dist/core/IsographEnvironment.js +19 -22
  8. package/dist/core/PromiseWrapper.d.ts +4 -4
  9. package/dist/core/PromiseWrapper.d.ts.map +1 -0
  10. package/dist/core/PromiseWrapper.js +9 -9
  11. package/dist/core/areEqualWithDeepComparison.d.ts +5 -3
  12. package/dist/core/areEqualWithDeepComparison.d.ts.map +1 -0
  13. package/dist/core/areEqualWithDeepComparison.js +89 -39
  14. package/dist/core/cache.d.ts +20 -13
  15. package/dist/core/cache.d.ts.map +1 -0
  16. package/dist/core/cache.js +205 -128
  17. package/dist/core/check.d.ts +22 -0
  18. package/dist/core/check.d.ts.map +1 -0
  19. package/dist/core/check.js +127 -0
  20. package/dist/core/componentCache.d.ts +2 -2
  21. package/dist/core/componentCache.d.ts.map +1 -0
  22. package/dist/core/componentCache.js +28 -32
  23. package/dist/core/entrypoint.d.ts +31 -15
  24. package/dist/core/entrypoint.d.ts.map +1 -0
  25. package/dist/core/entrypoint.js +1 -2
  26. package/dist/core/garbageCollection.d.ts +6 -5
  27. package/dist/core/garbageCollection.d.ts.map +1 -0
  28. package/dist/core/garbageCollection.js +49 -16
  29. package/dist/core/logging.d.ts +68 -0
  30. package/dist/core/logging.d.ts.map +1 -0
  31. package/dist/core/logging.js +22 -0
  32. package/dist/core/makeNetworkRequest.d.ts +6 -3
  33. package/dist/core/makeNetworkRequest.d.ts.map +1 -0
  34. package/dist/core/makeNetworkRequest.js +160 -19
  35. package/dist/core/read.d.ts +25 -5
  36. package/dist/core/read.d.ts.map +1 -0
  37. package/dist/core/read.js +416 -259
  38. package/dist/core/reader.d.ts +31 -15
  39. package/dist/core/reader.d.ts.map +1 -0
  40. package/dist/core/startUpdate.d.ts +5 -0
  41. package/dist/core/startUpdate.d.ts.map +1 -0
  42. package/dist/core/startUpdate.js +15 -0
  43. package/dist/core/util.d.ts +5 -0
  44. package/dist/core/util.d.ts.map +1 -0
  45. package/dist/index.d.ts +19 -14
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +11 -2
  48. package/dist/loadable-hooks/useClientSideDefer.d.ts +9 -3
  49. package/dist/loadable-hooks/useClientSideDefer.d.ts.map +1 -0
  50. package/dist/loadable-hooks/useClientSideDefer.js +6 -8
  51. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts +27 -0
  52. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts.map +1 -0
  53. package/dist/loadable-hooks/useConnectionSpecPagination.js +162 -0
  54. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts +2 -2
  55. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts.map +1 -0
  56. package/dist/loadable-hooks/useImperativeExposedMutationField.js +1 -2
  57. package/dist/loadable-hooks/useImperativeLoadableField.d.ts +13 -7
  58. package/dist/loadable-hooks/useImperativeLoadableField.d.ts.map +1 -0
  59. package/dist/loadable-hooks/useImperativeLoadableField.js +4 -5
  60. package/dist/loadable-hooks/useSkipLimitPagination.d.ts +13 -26
  61. package/dist/loadable-hooks/useSkipLimitPagination.d.ts.map +1 -0
  62. package/dist/loadable-hooks/useSkipLimitPagination.js +93 -47
  63. package/dist/react/FragmentReader.d.ts +6 -4
  64. package/dist/react/FragmentReader.d.ts.map +1 -0
  65. package/dist/react/FragmentReader.js +4 -2
  66. package/dist/react/IsographEnvironmentProvider.d.ts +1 -0
  67. package/dist/react/IsographEnvironmentProvider.d.ts.map +1 -0
  68. package/dist/react/IsographEnvironmentProvider.js +3 -3
  69. package/dist/react/RenderAfterCommit__DO_NOT_USE.d.ts +10 -0
  70. package/dist/react/RenderAfterCommit__DO_NOT_USE.d.ts.map +1 -0
  71. package/dist/react/RenderAfterCommit__DO_NOT_USE.js +15 -0
  72. package/dist/react/useImperativeReference.d.ts +8 -6
  73. package/dist/react/useImperativeReference.d.ts.map +1 -0
  74. package/dist/react/useImperativeReference.js +6 -8
  75. package/dist/react/useLazyReference.d.ts +5 -3
  76. package/dist/react/useLazyReference.d.ts.map +1 -0
  77. package/dist/react/useLazyReference.js +34 -6
  78. package/dist/react/useReadAndSubscribe.d.ts +6 -3
  79. package/dist/react/useReadAndSubscribe.d.ts.map +1 -0
  80. package/dist/react/useReadAndSubscribe.js +13 -10
  81. package/dist/react/useRerenderOnChange.d.ts +7 -2
  82. package/dist/react/useRerenderOnChange.d.ts.map +1 -0
  83. package/dist/react/useRerenderOnChange.js +3 -4
  84. package/dist/react/useResult.d.ts +4 -3
  85. package/dist/react/useResult.d.ts.map +1 -0
  86. package/dist/react/useResult.js +14 -9
  87. package/isograph.config.json +8 -0
  88. package/package.json +14 -9
  89. package/{src/tests/schema.graphql → schema.graphql} +1 -0
  90. package/src/core/FragmentReference.ts +44 -17
  91. package/src/core/IsographEnvironment.ts +67 -50
  92. package/src/core/PromiseWrapper.ts +3 -3
  93. package/src/core/areEqualWithDeepComparison.ts +95 -41
  94. package/src/core/cache.ts +316 -169
  95. package/src/core/check.ts +212 -0
  96. package/src/core/componentCache.ts +40 -46
  97. package/src/core/entrypoint.ts +41 -16
  98. package/src/core/garbageCollection.ts +77 -26
  99. package/src/core/logging.ts +118 -0
  100. package/src/core/makeNetworkRequest.ts +249 -20
  101. package/src/core/read.ts +658 -368
  102. package/src/core/reader.ts +61 -21
  103. package/src/core/startUpdate.ts +28 -0
  104. package/src/core/util.ts +8 -0
  105. package/src/index.ts +94 -8
  106. package/src/loadable-hooks/useClientSideDefer.ts +48 -17
  107. package/src/loadable-hooks/useConnectionSpecPagination.ts +344 -0
  108. package/src/loadable-hooks/useImperativeExposedMutationField.ts +1 -1
  109. package/src/loadable-hooks/useImperativeLoadableField.ts +36 -12
  110. package/src/loadable-hooks/useSkipLimitPagination.ts +253 -94
  111. package/src/react/FragmentReader.tsx +15 -6
  112. package/src/react/IsographEnvironmentProvider.tsx +1 -1
  113. package/src/react/RenderAfterCommit__DO_NOT_USE.tsx +17 -0
  114. package/src/react/useImperativeReference.ts +50 -18
  115. package/src/react/useLazyReference.ts +79 -11
  116. package/src/react/useReadAndSubscribe.ts +33 -10
  117. package/src/react/useRerenderOnChange.ts +7 -2
  118. package/src/react/useResult.ts +30 -9
  119. package/src/tests/__isograph/Query/meName/entrypoint.ts +10 -29
  120. package/src/tests/__isograph/Query/meName/normalization_ast.ts +25 -0
  121. package/src/tests/__isograph/Query/meName/param_type.ts +5 -2
  122. package/src/tests/__isograph/Query/meName/query_text.ts +6 -0
  123. package/src/tests/__isograph/Query/meName/resolver_reader.ts +5 -0
  124. package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +10 -65
  125. package/src/tests/__isograph/Query/meNameSuccessor/normalization_ast.ts +56 -0
  126. package/src/tests/__isograph/Query/meNameSuccessor/param_type.ts +9 -6
  127. package/src/tests/__isograph/Query/meNameSuccessor/query_text.ts +13 -0
  128. package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +10 -0
  129. package/src/tests/__isograph/Query/nodeField/entrypoint.ts +10 -28
  130. package/src/tests/__isograph/Query/nodeField/normalization_ast.ts +30 -0
  131. package/src/tests/__isograph/Query/nodeField/param_type.ts +7 -3
  132. package/src/tests/__isograph/Query/nodeField/parameters_type.ts +3 -0
  133. package/src/tests/__isograph/Query/nodeField/query_text.ts +6 -0
  134. package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +5 -0
  135. package/src/tests/__isograph/Query/subquery/entrypoint.ts +28 -0
  136. package/src/tests/__isograph/Query/subquery/normalization_ast.ts +38 -0
  137. package/src/tests/__isograph/Query/subquery/output_type.ts +3 -0
  138. package/src/tests/__isograph/Query/subquery/param_type.ts +12 -0
  139. package/src/tests/__isograph/Query/subquery/parameters_type.ts +3 -0
  140. package/src/tests/__isograph/Query/subquery/query_text.ts +8 -0
  141. package/src/tests/__isograph/Query/subquery/resolver_reader.ts +52 -0
  142. package/src/tests/__isograph/iso.ts +24 -12
  143. package/src/tests/garbageCollection.test.ts +53 -45
  144. package/src/tests/meNameSuccessor.ts +8 -3
  145. package/src/tests/nodeQuery.ts +7 -4
  146. package/src/tests/normalizeData.test.ts +120 -0
  147. package/src/tests/tsconfig.json +3 -3
  148. package/tsconfig.json +2 -2
  149. package/tsconfig.pkg.json +7 -3
  150. package/vitest.config.ts +20 -0
  151. package/src/tests/isograph.config.json +0 -7
package/src/core/cache.ts CHANGED
@@ -3,50 +3,53 @@ import {
3
3
  ItemCleanupPair,
4
4
  ParentCache,
5
5
  } from '@isograph/react-disposable-state';
6
- import {
7
- DataId,
8
- ROOT_ID,
9
- StoreRecord,
10
- Link,
11
- type IsographEnvironment,
12
- DataTypeValue,
13
- getLink,
14
- FragmentSubscription,
15
- } from './IsographEnvironment';
16
6
  import {
17
7
  IsographEntrypoint,
18
- NormalizationAst,
19
8
  NormalizationInlineFragment,
20
9
  NormalizationLinkedField,
21
10
  NormalizationScalarField,
22
11
  RefetchQueryNormalizationArtifactWrapper,
12
+ type NormalizationAst,
13
+ type NormalizationAstLoader,
14
+ type NormalizationAstNodes,
23
15
  } from '../core/entrypoint';
24
- import { ReaderLinkedField, ReaderScalarField } from './reader';
25
- import { Argument, ArgumentValue } from './util';
26
- import { WithEncounteredRecords, readButDoNotEvaluate } from './read';
27
- import { FragmentReference, Variables } from './FragmentReference';
28
- import { areEqualObjectsWithDeepComparison } from './areEqualWithDeepComparison';
29
- import { makeNetworkRequest } from './makeNetworkRequest';
16
+ import { mergeObjectsUsingReaderAst } from './areEqualWithDeepComparison';
17
+ import { FetchOptions } from './check';
18
+ import {
19
+ ExtractParameters,
20
+ FragmentReference,
21
+ Variables,
22
+ type UnknownTReadFromStore,
23
+ type VariableValue,
24
+ } from './FragmentReference';
25
+ import {
26
+ DataId,
27
+ DataTypeValue,
28
+ FragmentSubscription,
29
+ getLink,
30
+ Link,
31
+ ROOT_ID,
32
+ StoreRecord,
33
+ type IsographEnvironment,
34
+ type TypeName,
35
+ } from './IsographEnvironment';
36
+ import { logMessage } from './logging';
37
+ import { maybeMakeNetworkRequest } from './makeNetworkRequest';
30
38
  import { wrapResolvedValue } from './PromiseWrapper';
39
+ import { readButDoNotEvaluate, WithEncounteredRecords } from './read';
40
+ import { ReaderLinkedField, ReaderScalarField, type ReaderAst } from './reader';
41
+ import { Argument, ArgumentValue } from './util';
31
42
 
32
- const TYPENAME_FIELD_NAME = '__typename';
43
+ export const TYPENAME_FIELD_NAME = '__typename';
33
44
 
34
45
  export function getOrCreateItemInSuspenseCache<
35
- TReadFromStore extends Object,
46
+ TReadFromStore extends UnknownTReadFromStore,
36
47
  TClientFieldValue,
37
48
  >(
38
49
  environment: IsographEnvironment,
39
50
  index: string,
40
51
  factory: Factory<FragmentReference<TReadFromStore, TClientFieldValue>>,
41
52
  ): 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
- }
50
53
  if (environment.fragmentCache[index] == null) {
51
54
  environment.fragmentCache[index] = new ParentCache(factory);
52
55
  }
@@ -77,20 +80,30 @@ export function stableCopy<T>(value: T): T {
77
80
  }
78
81
 
79
82
  export function getOrCreateCacheForArtifact<
80
- TReadFromStore extends Object,
83
+ TReadFromStore extends UnknownTReadFromStore,
81
84
  TClientFieldValue,
85
+ TNormalizationAst extends NormalizationAst | NormalizationAstLoader,
82
86
  >(
83
87
  environment: IsographEnvironment,
84
- entrypoint: IsographEntrypoint<TReadFromStore, TClientFieldValue>,
85
- variables: Variables,
88
+ entrypoint: IsographEntrypoint<
89
+ TReadFromStore,
90
+ TClientFieldValue,
91
+ TNormalizationAst
92
+ >,
93
+ variables: ExtractParameters<TReadFromStore>,
94
+ fetchOptions?: FetchOptions<TClientFieldValue>,
86
95
  ): ParentCache<FragmentReference<TReadFromStore, TClientFieldValue>> {
87
- const cacheKey = entrypoint.queryText + JSON.stringify(stableCopy(variables));
96
+ const cacheKey =
97
+ entrypoint.networkRequestInfo.queryText +
98
+ JSON.stringify(stableCopy(variables));
88
99
  const factory = () => {
89
- const [networkRequest, disposeNetworkRequest] = makeNetworkRequest(
100
+ const [networkRequest, disposeNetworkRequest] = maybeMakeNetworkRequest(
90
101
  environment,
91
102
  entrypoint,
92
103
  variables,
104
+ fetchOptions,
93
105
  );
106
+
94
107
  const itemCleanupPair: ItemCleanupPair<
95
108
  FragmentReference<TReadFromStore, TClientFieldValue>
96
109
  > = [
@@ -102,7 +115,7 @@ export function getOrCreateCacheForArtifact<
102
115
  nestedRefetchQueries:
103
116
  entrypoint.readerWithRefetchQueries.nestedRefetchQueries,
104
117
  }),
105
- root: ROOT_ID,
118
+ root: { __link: ROOT_ID, __typename: entrypoint.concreteType },
106
119
  variables,
107
120
  networkRequest: networkRequest,
108
121
  },
@@ -113,56 +126,59 @@ export function getOrCreateCacheForArtifact<
113
126
  return getOrCreateItemInSuspenseCache(environment, cacheKey, factory);
114
127
  }
115
128
 
116
- type NetworkResponseScalarValue = string | number | boolean;
117
- type NetworkResponseValue =
129
+ export type NetworkResponseScalarValue = string | number | boolean;
130
+ export type NetworkResponseValue =
118
131
  | NetworkResponseScalarValue
119
132
  | null
120
133
  | NetworkResponseObject
121
- | NetworkResponseObject[]
122
- | NetworkResponseScalarValue[];
123
- type NetworkResponseObject = {
134
+ | (NetworkResponseObject | null)[]
135
+ | (NetworkResponseScalarValue | null)[];
136
+
137
+ export type NetworkResponseObject = {
124
138
  // N.B. undefined is here to support optional id's, but
125
139
  // undefined should not *actually* be present in the network response.
126
140
  [index: string]: undefined | NetworkResponseValue;
127
141
  id?: DataId;
142
+ __typename?: TypeName;
128
143
  };
129
144
 
130
145
  export function normalizeData(
131
146
  environment: IsographEnvironment,
132
- normalizationAst: NormalizationAst,
147
+ normalizationAst: NormalizationAstNodes,
133
148
  networkResponse: NetworkResponseObject,
134
149
  variables: Variables,
135
150
  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
- }
151
+ root: Link,
152
+ ): EncounteredIds {
153
+ const encounteredIds: EncounteredIds = new Map();
154
+
155
+ logMessage(environment, () => ({
156
+ kind: 'AboutToNormalize',
157
+ normalizationAst,
158
+ networkResponse,
159
+ variables,
160
+ }));
161
+
162
+ const recordsById = (environment.store[root.__typename] ??= {});
163
+ const newStoreRecord = (recordsById[root.__link] ??= {});
164
+
148
165
  normalizeDataIntoRecord(
149
166
  environment,
150
167
  normalizationAst,
151
168
  networkResponse,
152
- environment.store.__ROOT,
153
- ROOT_ID,
154
- variables as any,
169
+ newStoreRecord,
170
+ root,
171
+ variables,
155
172
  nestedRefetchQueries,
156
173
  encounteredIds,
157
174
  );
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
- }
175
+
176
+ logMessage(environment, () => ({
177
+ kind: 'AfterNormalization',
178
+ store: environment.store,
179
+ encounteredIds,
180
+ }));
181
+
166
182
  callSubscriptions(environment, encounteredIds);
167
183
  return encounteredIds;
168
184
  }
@@ -179,33 +195,54 @@ export function subscribeToAnyChange(
179
195
  return () => environment.subscriptions.delete(subscription);
180
196
  }
181
197
 
198
+ export function subscribeToAnyChangesToRecord(
199
+ environment: IsographEnvironment,
200
+ recordLink: Link,
201
+ callback: () => void,
202
+ ): () => void {
203
+ const subscription = {
204
+ kind: 'AnyChangesToRecord',
205
+ recordLink,
206
+ callback,
207
+ } as const;
208
+ environment.subscriptions.add(subscription);
209
+ return () => environment.subscriptions.delete(subscription);
210
+ }
211
+
182
212
  // TODO we should re-read and call callback if the value has changed
183
- export function subscribe<TReadFromStore extends Object>(
213
+ export function subscribe<TReadFromStore extends UnknownTReadFromStore>(
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,18 +370,19 @@ 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
  */
317
377
  function normalizeDataIntoRecord(
318
378
  environment: IsographEnvironment,
319
- normalizationAst: NormalizationAst,
379
+ normalizationAst: NormalizationAstNodes,
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(
@@ -616,8 +724,19 @@ export function getParentRecordKey(
616
724
  function getStoreKeyChunkForArgumentValue(
617
725
  argumentValue: ArgumentValue,
618
726
  variables: Variables,
619
- ) {
727
+ ): VariableValue {
620
728
  switch (argumentValue.kind) {
729
+ case 'Object': {
730
+ return Object.fromEntries(
731
+ argumentValue.value.map(([argumentName, argumentValue]) => {
732
+ return [
733
+ argumentName,
734
+ // substitute variables
735
+ getStoreKeyChunkForArgumentValue(argumentValue, variables),
736
+ ];
737
+ }),
738
+ );
739
+ }
621
740
  case 'Literal': {
622
741
  return argumentValue.value;
623
742
  }
@@ -641,7 +760,12 @@ function getStoreKeyChunkForArgumentValue(
641
760
  }
642
761
 
643
762
  function getStoreKeyChunkForArgument(argument: Argument, variables: Variables) {
644
- const chunk = getStoreKeyChunkForArgumentValue(argument[1], variables);
763
+ let chunk = getStoreKeyChunkForArgumentValue(argument[1], variables);
764
+
765
+ if (typeof chunk === 'object') {
766
+ chunk = JSON.stringify(stableCopy(chunk));
767
+ }
768
+
645
769
  return `${FIRST_SPLIT_KEY}${argument[0]}${SECOND_SPLIT_KEY}${chunk}`;
646
770
  }
647
771
 
@@ -650,52 +774,75 @@ function getNetworkResponseKey(
650
774
  ): string {
651
775
  let networkResponseKey = astNode.fieldName;
652
776
  const fieldParameters = astNode.arguments;
777
+
653
778
  if (fieldParameters != null) {
654
- for (const fieldParameter of fieldParameters) {
655
- const [argumentName, argumentValue] = fieldParameter;
656
- let argumentValueChunk;
657
- switch (argumentValue.kind) {
658
- case 'Literal': {
659
- argumentValueChunk = 'l_' + argumentValue.value;
660
- break;
661
- }
662
- case 'Variable': {
663
- argumentValueChunk = 'v_' + argumentValue.name;
664
- break;
665
- }
666
- case 'String': {
667
- argumentValueChunk = 's_' + argumentValue.value;
668
- break;
669
- }
670
- case 'Enum': {
671
- argumentValueChunk = 'e_' + argumentValue.value;
672
- break;
673
- }
674
- default: {
675
- // Ensure we have covered all variants
676
- let _: never = argumentValue;
677
- _;
678
- throw new Error('Unexpected case');
679
- }
680
- }
779
+ for (const [argumentName, argumentValue] of fieldParameters) {
780
+ let argumentValueChunk = getArgumentValueChunk(argumentValue);
681
781
  networkResponseKey += `${FIRST_SPLIT_KEY}${argumentName}${SECOND_SPLIT_KEY}${argumentValueChunk}`;
682
782
  }
683
783
  }
784
+
684
785
  return networkResponseKey;
685
786
  }
686
787
 
788
+ function getArgumentValueChunk(argumentValue: ArgumentValue): string {
789
+ switch (argumentValue.kind) {
790
+ case 'Object': {
791
+ return (
792
+ 'o_' +
793
+ argumentValue.value
794
+ .map(([argumentName, argumentValue]) => {
795
+ return (
796
+ argumentName +
797
+ THIRD_SPLIT_KEY +
798
+ getArgumentValueChunk(argumentValue)
799
+ );
800
+ })
801
+ .join('_') +
802
+ '_c'
803
+ );
804
+ }
805
+ case 'Literal': {
806
+ return 'l_' + argumentValue.value;
807
+ }
808
+ case 'Variable': {
809
+ return 'v_' + argumentValue.name;
810
+ }
811
+ case 'String': {
812
+ // replace all non-word characters (alphanumeric & underscore) with underscores
813
+ return 's_' + argumentValue.value.replaceAll(/\W/g, '_');
814
+ }
815
+ case 'Enum': {
816
+ return 'e_' + argumentValue.value;
817
+ }
818
+ default: {
819
+ // Ensure we have covered all variants
820
+ let _: never = argumentValue;
821
+ _;
822
+ throw new Error('Unexpected case');
823
+ }
824
+ }
825
+ }
826
+
687
827
  // an alias might be pullRequests____first___first____after___cursor
688
828
  export const FIRST_SPLIT_KEY = '____';
689
829
  export const SECOND_SPLIT_KEY = '___';
830
+ export const THIRD_SPLIT_KEY = '__';
690
831
 
691
832
  // Returns a key to look up an item in the store
692
833
  function getDataIdOfNetworkResponse(
693
- parentRecordId: DataId,
834
+ parentRecordLink: Link,
694
835
  dataToNormalize: NetworkResponseObject,
695
- astNode: NormalizationLinkedField | NormalizationScalarField,
836
+ astNode: NormalizationLinkedField,
696
837
  variables: Variables,
697
838
  index: number | null,
698
839
  ): DataId {
840
+ // If we are dealing with nested Query, use __ROOT as id
841
+ // TODO do not hard code this value here
842
+ if (astNode.concreteType === 'Query') {
843
+ return ROOT_ID;
844
+ }
845
+
699
846
  // Check whether the dataToNormalize has an id field. If so, that is the key.
700
847
  // If not, we construct an id from the parentRecordId and the field parameters.
701
848
 
@@ -704,7 +851,7 @@ function getDataIdOfNetworkResponse(
704
851
  return dataId;
705
852
  }
706
853
 
707
- let storeKey = `${parentRecordId}.${astNode.fieldName}`;
854
+ let storeKey = `${parentRecordLink.__typename}:${parentRecordLink.__link}.${astNode.fieldName}`;
708
855
  if (index != null) {
709
856
  storeKey += `.${index}`;
710
857
  }