@isograph/react 0.4.3 → 0.5.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 (137) hide show
  1. package/.turbo/turbo-compile-libs.log +2 -2
  2. package/dist/core/FragmentReference.d.ts +4 -2
  3. package/dist/core/FragmentReference.d.ts.map +1 -1
  4. package/dist/core/FragmentReference.js +2 -2
  5. package/dist/core/IsographEnvironment.d.ts +19 -11
  6. package/dist/core/IsographEnvironment.d.ts.map +1 -1
  7. package/dist/core/IsographEnvironment.js +27 -2
  8. package/dist/core/PromiseWrapper.d.ts +13 -7
  9. package/dist/core/PromiseWrapper.d.ts.map +1 -1
  10. package/dist/core/brand.d.ts +17 -0
  11. package/dist/core/brand.d.ts.map +1 -1
  12. package/dist/core/cache.d.ts +10 -7
  13. package/dist/core/cache.d.ts.map +1 -1
  14. package/dist/core/cache.js +102 -74
  15. package/dist/core/check.d.ts +8 -4
  16. package/dist/core/check.d.ts.map +1 -1
  17. package/dist/core/check.js +10 -7
  18. package/dist/core/componentCache.d.ts +1 -1
  19. package/dist/core/componentCache.d.ts.map +1 -1
  20. package/dist/core/componentCache.js +6 -4
  21. package/dist/core/entrypoint.d.ts +17 -7
  22. package/dist/core/entrypoint.d.ts.map +1 -1
  23. package/dist/core/garbageCollection.d.ts +8 -2
  24. package/dist/core/garbageCollection.d.ts.map +1 -1
  25. package/dist/core/garbageCollection.js +36 -14
  26. package/dist/core/logging.d.ts +16 -3
  27. package/dist/core/logging.d.ts.map +1 -1
  28. package/dist/core/makeNetworkRequest.d.ts +4 -2
  29. package/dist/core/makeNetworkRequest.d.ts.map +1 -1
  30. package/dist/core/makeNetworkRequest.js +115 -38
  31. package/dist/core/optimisticProxy.d.ts +59 -0
  32. package/dist/core/optimisticProxy.d.ts.map +1 -0
  33. package/dist/core/optimisticProxy.js +399 -0
  34. package/dist/core/read.d.ts +3 -2
  35. package/dist/core/read.d.ts.map +1 -1
  36. package/dist/core/read.js +158 -123
  37. package/dist/core/reader.d.ts +7 -4
  38. package/dist/core/reader.d.ts.map +1 -1
  39. package/dist/core/startUpdate.d.ts +3 -2
  40. package/dist/core/startUpdate.d.ts.map +1 -1
  41. package/dist/core/startUpdate.js +33 -34
  42. package/dist/index.d.ts +2 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +2 -1
  45. package/dist/loadable-hooks/useClientSideDefer.d.ts +9 -4
  46. package/dist/loadable-hooks/useClientSideDefer.d.ts.map +1 -1
  47. package/dist/loadable-hooks/useClientSideDefer.js +34 -1
  48. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts +5 -3
  49. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts.map +1 -1
  50. package/dist/loadable-hooks/useConnectionSpecPagination.js +27 -13
  51. package/dist/loadable-hooks/useImperativeLoadableField.d.ts +1 -1
  52. package/dist/loadable-hooks/useImperativeLoadableField.d.ts.map +1 -1
  53. package/dist/loadable-hooks/useSkipLimitPagination.d.ts +1 -1
  54. package/dist/loadable-hooks/useSkipLimitPagination.d.ts.map +1 -1
  55. package/dist/loadable-hooks/useSkipLimitPagination.js +1 -1
  56. package/dist/react/FragmentReader.d.ts +2 -1
  57. package/dist/react/FragmentReader.d.ts.map +1 -1
  58. package/dist/react/FragmentRenderer.d.ts +2 -1
  59. package/dist/react/FragmentRenderer.d.ts.map +1 -1
  60. package/dist/react/LoadableFieldReader.d.ts +9 -3
  61. package/dist/react/LoadableFieldReader.d.ts.map +1 -1
  62. package/dist/react/LoadableFieldReader.js +40 -1
  63. package/dist/react/LoadableFieldRenderer.d.ts +9 -3
  64. package/dist/react/LoadableFieldRenderer.d.ts.map +1 -1
  65. package/dist/react/LoadableFieldRenderer.js +36 -1
  66. package/dist/react/useImperativeReference.d.ts +4 -3
  67. package/dist/react/useImperativeReference.d.ts.map +1 -1
  68. package/dist/react/useImperativeReference.js +3 -5
  69. package/dist/react/useLazyReference.d.ts +2 -1
  70. package/dist/react/useLazyReference.d.ts.map +1 -1
  71. package/dist/react/useReadAndSubscribe.d.ts.map +1 -1
  72. package/dist/react/useReadAndSubscribe.js +1 -3
  73. package/dist/react/useResult.d.ts.map +1 -1
  74. package/dist/react/useResult.js +6 -5
  75. package/package.json +16 -17
  76. package/src/core/FragmentReference.ts +10 -4
  77. package/src/core/IsographEnvironment.ts +59 -13
  78. package/src/core/PromiseWrapper.ts +14 -7
  79. package/src/core/brand.ts +18 -0
  80. package/src/core/cache.ts +186 -91
  81. package/src/core/check.ts +21 -10
  82. package/src/core/componentCache.ts +8 -4
  83. package/src/core/entrypoint.ts +35 -6
  84. package/src/core/garbageCollection.ts +61 -19
  85. package/src/core/logging.ts +15 -3
  86. package/src/core/makeNetworkRequest.ts +307 -74
  87. package/src/core/optimisticProxy.ts +563 -0
  88. package/src/core/read.ts +233 -163
  89. package/src/core/reader.ts +10 -6
  90. package/src/core/startUpdate.ts +45 -30
  91. package/src/index.ts +2 -1
  92. package/src/loadable-hooks/useClientSideDefer.ts +76 -26
  93. package/src/loadable-hooks/useConnectionSpecPagination.ts +34 -17
  94. package/src/loadable-hooks/useImperativeLoadableField.ts +2 -2
  95. package/src/loadable-hooks/useSkipLimitPagination.ts +2 -3
  96. package/src/react/FragmentReader.tsx +3 -1
  97. package/src/react/FragmentRenderer.tsx +8 -1
  98. package/src/react/LoadableFieldReader.tsx +123 -12
  99. package/src/react/LoadableFieldRenderer.tsx +122 -12
  100. package/src/react/useImperativeReference.ts +20 -11
  101. package/src/react/useLazyReference.ts +17 -6
  102. package/src/react/useReadAndSubscribe.ts +1 -8
  103. package/src/react/useResult.ts +9 -11
  104. package/src/tests/__isograph/Node/asEconomist/resolver_reader.ts +1 -1
  105. package/src/tests/__isograph/Query/linkedUpdate/entrypoint.ts +3 -1
  106. package/src/tests/__isograph/Query/linkedUpdate/raw_response_type.ts +13 -0
  107. package/src/tests/__isograph/Query/linkedUpdate/resolver_reader.ts +1 -1
  108. package/src/tests/__isograph/Query/meName/entrypoint.ts +3 -1
  109. package/src/tests/__isograph/Query/meName/raw_response_type.ts +7 -0
  110. package/src/tests/__isograph/Query/meName/resolver_reader.ts +1 -1
  111. package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +3 -1
  112. package/src/tests/__isograph/Query/meNameSuccessor/raw_response_type.ts +14 -0
  113. package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +1 -1
  114. package/src/tests/__isograph/Query/nodeField/entrypoint.ts +3 -1
  115. package/src/tests/__isograph/Query/nodeField/raw_response_type.ts +7 -0
  116. package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +1 -1
  117. package/src/tests/__isograph/Query/normalizeUndefinedField/entrypoint.ts +33 -0
  118. package/src/tests/__isograph/Query/normalizeUndefinedField/normalization_ast.ts +25 -0
  119. package/src/tests/__isograph/Query/normalizeUndefinedField/output_type.ts +3 -0
  120. package/src/tests/__isograph/Query/normalizeUndefinedField/param_type.ts +9 -0
  121. package/src/tests/__isograph/Query/normalizeUndefinedField/query_text.ts +6 -0
  122. package/src/tests/__isograph/Query/normalizeUndefinedField/raw_response_type.ts +7 -0
  123. package/src/tests/__isograph/Query/normalizeUndefinedField/resolver_reader.ts +38 -0
  124. package/src/tests/__isograph/Query/startUpdate/entrypoint.ts +3 -1
  125. package/src/tests/__isograph/Query/startUpdate/raw_response_type.ts +8 -0
  126. package/src/tests/__isograph/Query/startUpdate/resolver_reader.ts +1 -1
  127. package/src/tests/__isograph/Query/subquery/entrypoint.ts +3 -1
  128. package/src/tests/__isograph/Query/subquery/raw_response_type.ts +9 -0
  129. package/src/tests/__isograph/Query/subquery/resolver_reader.ts +1 -1
  130. package/src/tests/__isograph/iso.ts +11 -1
  131. package/src/tests/garbageCollection.test.ts +8 -5
  132. package/src/tests/meNameSuccessor.ts +6 -3
  133. package/src/tests/nodeQuery.ts +4 -2
  134. package/src/tests/normalizeData.test.ts +89 -15
  135. package/src/tests/optimisticProxy.test.ts +860 -0
  136. package/src/tests/startUpdate.test.ts +7 -5
  137. package/src/tests/__isograph/Economist/__link/output_type.ts +0 -2
package/src/core/cache.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  DataTypeValue,
27
27
  FragmentSubscription,
28
28
  getLink,
29
+ getOrLoadReaderWithRefetchQueries,
29
30
  ROOT_ID,
30
31
  StoreLink,
31
32
  StoreRecord,
@@ -33,8 +34,15 @@ import {
33
34
  type TypeName,
34
35
  } from './IsographEnvironment';
35
36
  import { logMessage } from './logging';
36
- import { maybeMakeNetworkRequest } from './makeNetworkRequest';
37
- import { wrapPromise, wrapResolvedValue } from './PromiseWrapper';
37
+ import {
38
+ maybeMakeNetworkRequest,
39
+ retainQueryWithoutMakingNetworkRequest,
40
+ } from './makeNetworkRequest';
41
+ import {
42
+ addNetworkResponseStoreLayer,
43
+ getMutableStoreRecordProxy,
44
+ type StoreLayerWithData,
45
+ } from './optimisticProxy';
38
46
  import { readButDoNotEvaluate, WithEncounteredRecords } from './read';
39
47
  import { ReaderLinkedField, ReaderScalarField, type ReaderAst } from './reader';
40
48
  import { Argument, ArgumentValue } from './util';
@@ -65,7 +73,7 @@ export function stableCopy<T>(value: T): T {
65
73
  if (!value || typeof value !== 'object') {
66
74
  return value;
67
75
  }
68
- if (Array.isArray(value)) {
76
+ if (isArray(value)) {
69
77
  // @ts-ignore
70
78
  return value.map(stableCopy);
71
79
  }
@@ -82,15 +90,17 @@ export function getOrCreateCacheForArtifact<
82
90
  TReadFromStore extends UnknownTReadFromStore,
83
91
  TClientFieldValue,
84
92
  TNormalizationAst extends NormalizationAst | NormalizationAstLoader,
93
+ TRawResponseType extends NetworkResponseObject,
85
94
  >(
86
95
  environment: IsographEnvironment,
87
96
  entrypoint: IsographEntrypoint<
88
97
  TReadFromStore,
89
98
  TClientFieldValue,
90
- TNormalizationAst
99
+ TNormalizationAst,
100
+ TRawResponseType
91
101
  >,
92
102
  variables: ExtractParameters<TReadFromStore>,
93
- fetchOptions?: FetchOptions<TClientFieldValue>,
103
+ fetchOptions?: FetchOptions<TClientFieldValue, TRawResponseType>,
94
104
  ): ParentCache<FragmentReference<TReadFromStore, TClientFieldValue>> {
95
105
  let cacheKey = '';
96
106
  switch (entrypoint.networkRequestInfo.operation.kind) {
@@ -106,11 +116,11 @@ export function getOrCreateCacheForArtifact<
106
116
  break;
107
117
  }
108
118
  const factory = () => {
109
- const readerWithRefetchQueries =
110
- entrypoint.readerWithRefetchQueries.kind ===
111
- 'ReaderWithRefetchQueriesLoader'
112
- ? wrapPromise(entrypoint.readerWithRefetchQueries.loader())
113
- : wrapResolvedValue(entrypoint.readerWithRefetchQueries);
119
+ const { fieldName, readerArtifactKind, readerWithRefetchQueries } =
120
+ getOrLoadReaderWithRefetchQueries(
121
+ environment,
122
+ entrypoint.readerWithRefetchQueries,
123
+ );
114
124
  const [networkRequest, disposeNetworkRequest] = maybeMakeNetworkRequest(
115
125
  environment,
116
126
  entrypoint,
@@ -125,6 +135,8 @@ export function getOrCreateCacheForArtifact<
125
135
  {
126
136
  kind: 'FragmentReference',
127
137
  readerWithRefetchQueries,
138
+ fieldName,
139
+ readerArtifactKind,
128
140
  root: { __link: ROOT_ID, __typename: entrypoint.concreteType },
129
141
  variables,
130
142
  networkRequest: networkRequest,
@@ -141,26 +153,26 @@ export type NetworkResponseValue =
141
153
  | NetworkResponseScalarValue
142
154
  | null
143
155
  | NetworkResponseObject
144
- | (NetworkResponseObject | null)[]
145
- | (NetworkResponseScalarValue | null)[];
156
+ | readonly (NetworkResponseObject | null)[]
157
+ | readonly (NetworkResponseScalarValue | null)[];
146
158
 
147
159
  export type NetworkResponseObject = {
148
160
  // N.B. undefined is here to support optional id's, but
149
161
  // undefined should not *actually* be present in the network response.
150
- [index: string]: undefined | NetworkResponseValue;
151
- id?: DataId;
152
- __typename?: TypeName;
162
+ readonly [index: string]: undefined | NetworkResponseValue;
163
+ readonly id?: DataId;
164
+ readonly __typename?: TypeName;
153
165
  };
154
166
 
155
167
  export function normalizeData(
156
168
  environment: IsographEnvironment,
169
+ storeLayer: StoreLayerWithData,
157
170
  normalizationAst: NormalizationAstNodes,
158
171
  networkResponse: NetworkResponseObject,
159
172
  variables: Variables,
160
173
  root: StoreLink,
174
+ encounteredIds: EncounteredIds,
161
175
  ): EncounteredIds {
162
- const encounteredIds: EncounteredIds = new Map();
163
-
164
176
  logMessage(environment, () => ({
165
177
  kind: 'AboutToNormalize',
166
178
  normalizationAst,
@@ -168,11 +180,11 @@ export function normalizeData(
168
180
  variables,
169
181
  }));
170
182
 
171
- const recordsById = (environment.store[root.__typename] ??= {});
172
- const newStoreRecord = (recordsById[root.__link] ??= {});
183
+ const newStoreRecord = getMutableStoreRecordProxy(storeLayer, root);
173
184
 
174
185
  normalizeDataIntoRecord(
175
186
  environment,
187
+ storeLayer,
176
188
  normalizationAst,
177
189
  networkResponse,
178
190
  newStoreRecord,
@@ -181,13 +193,6 @@ export function normalizeData(
181
193
  encounteredIds,
182
194
  );
183
195
 
184
- logMessage(environment, () => ({
185
- kind: 'AfterNormalization',
186
- store: environment.store,
187
- encounteredIds,
188
- }));
189
-
190
- callSubscriptions(environment, encounteredIds);
191
196
  return encounteredIds;
192
197
  }
193
198
 
@@ -217,7 +222,6 @@ export function subscribeToAnyChangesToRecord(
217
222
  return () => environment.subscriptions.delete(subscription);
218
223
  }
219
224
 
220
- // TODO we should re-read and call callback if the value has changed
221
225
  export function subscribe<TReadFromStore extends UnknownTReadFromStore>(
222
226
  environment: IsographEnvironment,
223
227
  encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
@@ -234,6 +238,13 @@ export function subscribe<TReadFromStore extends UnknownTReadFromStore>(
234
238
  fragmentReference,
235
239
  readerAst,
236
240
  };
241
+
242
+ // subscribe is called in an effect. (We should actually subscribe during the
243
+ // initial render.) Because it's called in an effect, we might have missed some
244
+ // changes since the initial render! So, at this point, we re-read and call the
245
+ // subscription (i.e. re-render) if the fragment data has changed.
246
+ callSubscriptionIfDataChanged(environment, fragmentSubscription);
247
+
237
248
  environment.subscriptions.add(fragmentSubscription);
238
249
  return () => environment.subscriptions.delete(fragmentSubscription);
239
250
  }
@@ -292,53 +303,7 @@ export function callSubscriptions(
292
303
  subscription.encounteredDataAndRecords.encounteredRecords,
293
304
  )
294
305
  ) {
295
- const newEncounteredDataAndRecords = readButDoNotEvaluate(
296
- environment,
297
- subscription.fragmentReference,
298
- // Is this wrong?
299
- // Reasons to think no:
300
- // - we are only updating the read-out value, and the network
301
- // options only affect whether we throw.
302
- // - the component will re-render, and re-throw on its own, anyway.
303
- //
304
- // Reasons to think not:
305
- // - it seems more efficient to suspend here and not update state,
306
- // if we expect that the component will just throw anyway
307
- // - consistency
308
- // - it's also weird, this is called from makeNetworkRequest, where
309
- // we don't currently pass network request options
310
- {
311
- suspendIfInFlight: false,
312
- throwOnNetworkError: false,
313
- },
314
- );
315
-
316
- const mergedItem = mergeObjectsUsingReaderAst(
317
- subscription.readerAst,
318
- subscription.encounteredDataAndRecords.item,
319
- newEncounteredDataAndRecords.item,
320
- );
321
-
322
- logMessage(environment, () => ({
323
- kind: 'DeepEqualityCheck',
324
- fragmentReference: subscription.fragmentReference,
325
- old: subscription.encounteredDataAndRecords.item,
326
- new: newEncounteredDataAndRecords.item,
327
- deeplyEqual:
328
- mergedItem === subscription.encounteredDataAndRecords.item,
329
- }));
330
-
331
- if (mergedItem !== subscription.encounteredDataAndRecords.item) {
332
- logAnyError(
333
- environment,
334
- { situation: 'calling FragmentSubscription callback' },
335
- () => {
336
- subscription.callback(newEncounteredDataAndRecords);
337
- },
338
- );
339
- subscription.encounteredDataAndRecords =
340
- newEncounteredDataAndRecords;
341
- }
306
+ callSubscriptionIfDataChanged(environment, subscription);
342
307
  }
343
308
  return;
344
309
  }
@@ -375,6 +340,59 @@ export function callSubscriptions(
375
340
  );
376
341
  }
377
342
 
343
+ function callSubscriptionIfDataChanged<
344
+ TReadFromStore extends UnknownTReadFromStore,
345
+ >(
346
+ environment: IsographEnvironment,
347
+ subscription: FragmentSubscription<TReadFromStore>,
348
+ ) {
349
+ const newEncounteredDataAndRecords = readButDoNotEvaluate(
350
+ environment,
351
+ subscription.fragmentReference,
352
+ // Is this wrong?
353
+ // Reasons to think no:
354
+ // - we are only updating the read-out value, and the network
355
+ // options only affect whether we throw.
356
+ // - the component will re-render, and re-throw on its own, anyway.
357
+ //
358
+ // Reasons to think not:
359
+ // - it seems more efficient to suspend here and not update state,
360
+ // if we expect that the component will just throw anyway
361
+ // - consistency
362
+ // - it's also weird, this is called from makeNetworkRequest, where
363
+ // we don't currently pass network request options
364
+ {
365
+ suspendIfInFlight: false,
366
+ throwOnNetworkError: false,
367
+ },
368
+ );
369
+
370
+ const mergedItem = mergeObjectsUsingReaderAst(
371
+ subscription.readerAst,
372
+ subscription.encounteredDataAndRecords.item,
373
+ newEncounteredDataAndRecords.item,
374
+ );
375
+
376
+ logMessage(environment, () => ({
377
+ kind: 'DeepEqualityCheck',
378
+ fragmentReference: subscription.fragmentReference,
379
+ old: subscription.encounteredDataAndRecords.item,
380
+ new: newEncounteredDataAndRecords.item,
381
+ deeplyEqual: mergedItem === subscription.encounteredDataAndRecords.item,
382
+ }));
383
+
384
+ if (mergedItem !== subscription.encounteredDataAndRecords.item) {
385
+ logAnyError(
386
+ environment,
387
+ { situation: 'calling FragmentSubscription callback' },
388
+ () => {
389
+ subscription.callback(newEncounteredDataAndRecords);
390
+ },
391
+ );
392
+ subscription.encounteredDataAndRecords = newEncounteredDataAndRecords;
393
+ }
394
+ }
395
+
378
396
  function hasOverlappingIds(
379
397
  ids1: EncounteredIds,
380
398
  ids2: EncounteredIds,
@@ -408,6 +426,7 @@ export type EncounteredIds = Map<TypeName, Set<DataId>>;
408
426
  */
409
427
  function normalizeDataIntoRecord(
410
428
  environment: IsographEnvironment,
429
+ storeLayer: StoreLayerWithData,
411
430
  normalizationAst: NormalizationAstNodes,
412
431
  networkResponseParentRecord: NetworkResponseObject,
413
432
  targetParentRecord: StoreRecord,
@@ -432,6 +451,7 @@ function normalizeDataIntoRecord(
432
451
  case 'Linked': {
433
452
  const linkedFieldResultedInChange = normalizeLinkedField(
434
453
  environment,
454
+ storeLayer,
435
455
  normalizationNode,
436
456
  networkResponseParentRecord,
437
457
  targetParentRecord,
@@ -446,6 +466,7 @@ function normalizeDataIntoRecord(
446
466
  case 'InlineFragment': {
447
467
  const inlineFragmentResultedInChange = normalizeInlineFragment(
448
468
  environment,
469
+ storeLayer,
449
470
  normalizationNode,
450
471
  networkResponseParentRecord,
451
472
  targetParentRecord,
@@ -495,12 +516,14 @@ function normalizeScalarField(
495
516
  const networkResponseKey = getNetworkResponseKey(astNode);
496
517
  const networkResponseData = networkResponseParentRecord[networkResponseKey];
497
518
  const parentRecordKey = getParentRecordKey(astNode, variables);
519
+ const existingValue = targetStoreRecord[parentRecordKey];
498
520
 
499
- if (
500
- networkResponseData == null ||
501
- isScalarOrEmptyArray(networkResponseData)
502
- ) {
503
- const existingValue = targetStoreRecord[parentRecordKey];
521
+ if (networkResponseData == null) {
522
+ targetStoreRecord[parentRecordKey] = null;
523
+ return existingValue !== null;
524
+ }
525
+
526
+ if (isScalarOrEmptyArray(networkResponseData)) {
504
527
  targetStoreRecord[parentRecordKey] = networkResponseData;
505
528
  return existingValue !== networkResponseData;
506
529
  } else {
@@ -508,11 +531,16 @@ function normalizeScalarField(
508
531
  }
509
532
  }
510
533
 
534
+ export function isArray(value: unknown): value is readonly unknown[] {
535
+ return Array.isArray(value);
536
+ }
537
+
511
538
  /**
512
539
  * Mutate targetParentRecord with a given linked field ast node.
513
540
  */
514
541
  function normalizeLinkedField(
515
542
  environment: IsographEnvironment,
543
+ storeLayer: StoreLayerWithData,
516
544
  astNode: NormalizationLinkedField,
517
545
  networkResponseParentRecord: NetworkResponseObject,
518
546
  targetParentRecord: StoreRecord,
@@ -539,7 +567,7 @@ function normalizeLinkedField(
539
567
  );
540
568
  }
541
569
 
542
- if (Array.isArray(networkResponseData)) {
570
+ if (isArray(networkResponseData)) {
543
571
  // TODO check astNode.plural or the like
544
572
  const dataIds: (StoreLink | null)[] = [];
545
573
  for (let i = 0; i < networkResponseData.length; i++) {
@@ -550,6 +578,7 @@ function normalizeLinkedField(
550
578
  }
551
579
  const newStoreRecordId = normalizeNetworkResponseObject(
552
580
  environment,
581
+ storeLayer,
553
582
  astNode,
554
583
  networkResponseObject,
555
584
  targetParentRecordLink,
@@ -576,6 +605,7 @@ function normalizeLinkedField(
576
605
  } else {
577
606
  const newStoreRecordId = normalizeNetworkResponseObject(
578
607
  environment,
608
+ storeLayer,
579
609
  astNode,
580
610
  networkResponseData,
581
611
  targetParentRecordLink,
@@ -609,6 +639,7 @@ function normalizeLinkedField(
609
639
  */
610
640
  function normalizeInlineFragment(
611
641
  environment: IsographEnvironment,
642
+ storeLayer: StoreLayerWithData,
612
643
  astNode: NormalizationInlineFragment,
613
644
  networkResponseParentRecord: NetworkResponseObject,
614
645
  targetParentRecord: StoreRecord,
@@ -620,6 +651,7 @@ function normalizeInlineFragment(
620
651
  if (networkResponseParentRecord[TYPENAME_FIELD_NAME] === typeToRefineTo) {
621
652
  const hasBeenModified = normalizeDataIntoRecord(
622
653
  environment,
654
+ storeLayer,
623
655
  astNode.selections,
624
656
  networkResponseParentRecord,
625
657
  targetParentRecord,
@@ -636,7 +668,7 @@ function dataIdsAreTheSame(
636
668
  existingValue: DataTypeValue,
637
669
  newDataIds: (StoreLink | null)[],
638
670
  ): boolean {
639
- if (Array.isArray(existingValue)) {
671
+ if (isArray(existingValue)) {
640
672
  if (newDataIds.length !== existingValue.length) {
641
673
  return false;
642
674
  }
@@ -657,6 +689,7 @@ function dataIdsAreTheSame(
657
689
 
658
690
  function normalizeNetworkResponseObject(
659
691
  environment: IsographEnvironment,
692
+ storeLayer: StoreLayerWithData,
660
693
  astNode: NormalizationLinkedField,
661
694
  networkResponseData: NetworkResponseObject,
662
695
  targetParentRecordLink: StoreLink,
@@ -681,15 +714,16 @@ function normalizeNetworkResponseObject(
681
714
  );
682
715
  }
683
716
 
684
- const recordsById = (environment.store[__typename] ??= {});
685
- const newStoreRecord = (recordsById[newStoreRecordId] ??= {});
717
+ const link = { __link: newStoreRecordId, __typename };
718
+ const newStoreRecord = getMutableStoreRecordProxy(storeLayer, link);
686
719
 
687
720
  normalizeDataIntoRecord(
688
721
  environment,
722
+ storeLayer,
689
723
  astNode.selections,
690
724
  networkResponseData,
691
725
  newStoreRecord,
692
- { __link: newStoreRecordId, __typename: __typename },
726
+ link,
693
727
  variables,
694
728
  mutableEncounteredIds,
695
729
  );
@@ -698,12 +732,13 @@ function normalizeNetworkResponseObject(
698
732
  }
699
733
 
700
734
  function isScalarOrEmptyArray(
701
- data: NonNullable<NetworkResponseValue>,
702
- ): data is NetworkResponseScalarValue | (NetworkResponseScalarValue | null)[] {
735
+ data: NetworkResponseValue,
736
+ ): data is
737
+ | NetworkResponseScalarValue
738
+ | readonly (NetworkResponseScalarValue | null)[] {
703
739
  // N.B. empty arrays count as empty arrays of scalar fields.
704
- if (Array.isArray(data)) {
705
- // This is maybe fixed in a new version of Typescript??
706
- return (data as any).every((x: any) => isScalarOrEmptyArray(x));
740
+ if (isArray(data)) {
741
+ return data.every((x) => isScalarOrEmptyArray(x));
707
742
  }
708
743
  const isScalarValue =
709
744
  data === null ||
@@ -713,8 +748,10 @@ function isScalarOrEmptyArray(
713
748
  return isScalarValue;
714
749
  }
715
750
 
716
- function isNullOrEmptyArray(data: unknown): data is never[] | null[] | null {
717
- if (Array.isArray(data)) {
751
+ function isNullOrEmptyArray(
752
+ data: unknown,
753
+ ): data is readonly never[] | null[] | null {
754
+ if (isArray(data)) {
718
755
  if (data.length === 0) {
719
756
  return true;
720
757
  }
@@ -889,3 +926,61 @@ function getDataIdOfNetworkResponse(
889
926
  }
890
927
  return storeKey;
891
928
  }
929
+
930
+ export function writeData<
931
+ TReadFromStore extends UnknownTReadFromStore,
932
+ TRawResponseType extends NetworkResponseObject,
933
+ TClientFieldValue,
934
+ >(
935
+ environment: IsographEnvironment,
936
+ entrypoint: IsographEntrypoint<
937
+ TReadFromStore,
938
+ TClientFieldValue,
939
+ NormalizationAst,
940
+ TRawResponseType
941
+ >,
942
+ data: TRawResponseType,
943
+ variables: ExtractParameters<TReadFromStore>,
944
+ ): ItemCleanupPair<FragmentReference<TReadFromStore, TClientFieldValue>> {
945
+ const encounteredIds: EncounteredIds = new Map();
946
+ environment.store = addNetworkResponseStoreLayer(environment.store);
947
+ normalizeData(
948
+ environment,
949
+ environment.store,
950
+ entrypoint.networkRequestInfo.normalizationAst.selections,
951
+ data,
952
+ variables,
953
+ { __link: ROOT_ID, __typename: entrypoint.concreteType },
954
+ encounteredIds,
955
+ );
956
+ logMessage(environment, () => ({
957
+ kind: 'AfterNormalization',
958
+ store: environment.store,
959
+ encounteredIds,
960
+ }));
961
+
962
+ callSubscriptions(environment, encounteredIds);
963
+
964
+ const { fieldName, readerArtifactKind, readerWithRefetchQueries } =
965
+ getOrLoadReaderWithRefetchQueries(
966
+ environment,
967
+ entrypoint.readerWithRefetchQueries,
968
+ );
969
+ const [networkRequest, disposeNetworkRequest] =
970
+ retainQueryWithoutMakingNetworkRequest(environment, entrypoint, variables);
971
+
972
+ return [
973
+ {
974
+ kind: 'FragmentReference',
975
+ readerWithRefetchQueries,
976
+ fieldName,
977
+ readerArtifactKind,
978
+ root: { __link: ROOT_ID, __typename: entrypoint.concreteType },
979
+ variables,
980
+ networkRequest,
981
+ },
982
+ () => {
983
+ disposeNetworkRequest();
984
+ },
985
+ ];
986
+ }
package/src/core/check.ts CHANGED
@@ -8,21 +8,28 @@ import {
8
8
  StoreRecord,
9
9
  } from './IsographEnvironment';
10
10
  import { logMessage } from './logging';
11
+ import { getStoreRecordProxy } from './optimisticProxy';
11
12
 
12
13
  export type ShouldFetch = RequiredShouldFetch | 'IfNecessary';
13
14
  export type RequiredShouldFetch = 'Yes' | 'No';
14
15
 
15
16
  export const DEFAULT_SHOULD_FETCH_VALUE: ShouldFetch = 'IfNecessary';
16
17
 
17
- export type FetchOptions<TReadOutData> = {
18
- shouldFetch?: ShouldFetch;
18
+ type FetchOptionsShared<TReadOutData> = {
19
19
  onComplete?: (data: TReadOutData) => void;
20
20
  onError?: () => void;
21
21
  };
22
22
 
23
- export type RequiredFetchOptions<TReadOutData> = {
23
+ export interface FetchOptions<TReadOutData, TRawResponseType>
24
+ extends FetchOptionsShared<TReadOutData> {
25
+ shouldFetch?: ShouldFetch;
26
+ optimisticNetworkResponse?: TRawResponseType;
27
+ }
28
+
29
+ export interface RequiredFetchOptions<TReadOutData>
30
+ extends FetchOptionsShared<TReadOutData> {
24
31
  shouldFetch: RequiredShouldFetch;
25
- } & FetchOptions<TReadOutData>;
32
+ }
26
33
 
27
34
  export type CheckResult =
28
35
  | {
@@ -39,8 +46,14 @@ export function check(
39
46
  variables: Variables,
40
47
  root: StoreLink,
41
48
  ): CheckResult {
42
- const recordsById = (environment.store[root.__typename] ??= {});
43
- const newStoreRecord = (recordsById[root.__link] ??= {});
49
+ const newStoreRecord = getStoreRecordProxy(environment.store, root);
50
+
51
+ if (newStoreRecord == null) {
52
+ return {
53
+ kind: 'MissingData',
54
+ record: root,
55
+ };
56
+ }
44
57
 
45
58
  const checkResult = checkFromRecord(
46
59
  environment,
@@ -107,8 +120,7 @@ function checkFromRecord(
107
120
  );
108
121
  }
109
122
 
110
- const linkedRecord =
111
- environment.store[link.__typename]?.[link.__link];
123
+ const linkedRecord = getStoreRecordProxy(environment.store, link);
112
124
 
113
125
  if (linkedRecord === undefined) {
114
126
  return {
@@ -141,8 +153,7 @@ function checkFromRecord(
141
153
  );
142
154
  }
143
155
 
144
- const linkedRecord =
145
- environment.store[link.__typename]?.[link.__link];
156
+ const linkedRecord = getStoreRecordProxy(environment.store, link);
146
157
 
147
158
  if (linkedRecord === undefined) {
148
159
  return {
@@ -1,4 +1,5 @@
1
1
  import { useReadAndSubscribe } from '../react/useReadAndSubscribe';
2
+ import { maybeUnwrapNetworkRequest } from '../react/useResult';
2
3
  import {
3
4
  FragmentReference,
4
5
  stableIdForFragmentReference,
@@ -11,7 +12,6 @@ import { createStartUpdate } from './startUpdate';
11
12
 
12
13
  export function getOrCreateCachedComponent(
13
14
  environment: IsographEnvironment,
14
- componentName: string,
15
15
  fragmentReference: FragmentReference<any, any>,
16
16
  networkRequestOptions: NetworkRequestReaderOptions,
17
17
  ): React.FC<any> {
@@ -23,9 +23,13 @@ export function getOrCreateCachedComponent(
23
23
  );
24
24
 
25
25
  return (environment.componentCache[
26
- stableIdForFragmentReference(fragmentReference, componentName)
26
+ stableIdForFragmentReference(fragmentReference)
27
27
  ] ??= (() => {
28
28
  function Component(additionalRuntimeProps: { [key: string]: any }) {
29
+ maybeUnwrapNetworkRequest(
30
+ fragmentReference.networkRequest,
31
+ networkRequestOptions,
32
+ );
29
33
  const readerWithRefetchQueries = readPromise(
30
34
  fragmentReference.readerWithRefetchQueries,
31
35
  );
@@ -38,7 +42,7 @@ export function getOrCreateCachedComponent(
38
42
 
39
43
  logMessage(environment, () => ({
40
44
  kind: 'ComponentRerendered',
41
- componentName,
45
+ componentName: fragmentReference.fieldName,
42
46
  rootLink: fragmentReference.root,
43
47
  }));
44
48
 
@@ -54,7 +58,7 @@ export function getOrCreateCachedComponent(
54
58
  );
55
59
  }
56
60
  const idString = `(type: ${fragmentReference.root.__typename}, id: ${fragmentReference.root.__link})`;
57
- Component.displayName = `${componentName} ${idString} @component`;
61
+ Component.displayName = `${fragmentReference.fieldName} ${idString} @component`;
58
62
  return Component;
59
63
  })());
60
64
  }