@isograph/react 0.3.0 → 0.4.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 (164) hide show
  1. package/.turbo/turbo-compile-libs.log +5 -0
  2. package/dist/core/FragmentReference.d.ts +17 -8
  3. package/dist/core/FragmentReference.d.ts.map +1 -1
  4. package/dist/core/FragmentReference.js +3 -12
  5. package/dist/core/IsographEnvironment.d.ts +30 -35
  6. package/dist/core/IsographEnvironment.d.ts.map +1 -1
  7. package/dist/core/IsographEnvironment.js +4 -0
  8. package/dist/core/PromiseWrapper.d.ts +6 -7
  9. package/dist/core/PromiseWrapper.d.ts.map +1 -1
  10. package/dist/core/PromiseWrapper.js +6 -12
  11. package/dist/core/areEqualWithDeepComparison.d.ts +1 -3
  12. package/dist/core/areEqualWithDeepComparison.d.ts.map +1 -1
  13. package/dist/core/areEqualWithDeepComparison.js +16 -2
  14. package/dist/core/brand.d.ts +2 -0
  15. package/dist/core/brand.d.ts.map +1 -0
  16. package/dist/core/brand.js +2 -0
  17. package/dist/core/cache.d.ts +16 -24
  18. package/dist/core/cache.d.ts.map +1 -1
  19. package/dist/core/cache.js +105 -72
  20. package/dist/core/check.d.ts +11 -7
  21. package/dist/core/check.d.ts.map +1 -1
  22. package/dist/core/check.js +2 -2
  23. package/dist/core/componentCache.d.ts +1 -1
  24. package/dist/core/componentCache.d.ts.map +1 -1
  25. package/dist/core/componentCache.js +27 -31
  26. package/dist/core/entrypoint.d.ts +43 -28
  27. package/dist/core/entrypoint.d.ts.map +1 -1
  28. package/dist/core/garbageCollection.d.ts +5 -6
  29. package/dist/core/garbageCollection.d.ts.map +1 -1
  30. package/dist/core/garbageCollection.js +1 -1
  31. package/dist/core/logging.d.ts +23 -15
  32. package/dist/core/logging.d.ts.map +1 -1
  33. package/dist/core/logging.js +8 -5
  34. package/dist/core/makeNetworkRequest.d.ts +5 -5
  35. package/dist/core/makeNetworkRequest.d.ts.map +1 -1
  36. package/dist/core/makeNetworkRequest.js +113 -28
  37. package/dist/core/read.d.ts +16 -11
  38. package/dist/core/read.d.ts.map +1 -1
  39. package/dist/core/read.js +468 -305
  40. package/dist/core/reader.d.ts +33 -37
  41. package/dist/core/reader.d.ts.map +1 -1
  42. package/dist/core/startUpdate.d.ts +8 -0
  43. package/dist/core/startUpdate.d.ts.map +1 -0
  44. package/dist/core/startUpdate.js +163 -0
  45. package/dist/core/util.d.ts +3 -0
  46. package/dist/core/util.d.ts.map +1 -1
  47. package/dist/index.d.ts +18 -15
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +9 -1
  50. package/dist/loadable-hooks/useClientSideDefer.d.ts +4 -10
  51. package/dist/loadable-hooks/useClientSideDefer.d.ts.map +1 -1
  52. package/dist/loadable-hooks/useClientSideDefer.js +2 -2
  53. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts +8 -15
  54. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts.map +1 -1
  55. package/dist/loadable-hooks/useConnectionSpecPagination.js +6 -4
  56. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts +1 -2
  57. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts.map +1 -1
  58. package/dist/loadable-hooks/useImperativeLoadableField.d.ts +4 -6
  59. package/dist/loadable-hooks/useImperativeLoadableField.d.ts.map +1 -1
  60. package/dist/loadable-hooks/useImperativeLoadableField.js +1 -1
  61. package/dist/loadable-hooks/useSkipLimitPagination.d.ts +6 -13
  62. package/dist/loadable-hooks/useSkipLimitPagination.d.ts.map +1 -1
  63. package/dist/loadable-hooks/useSkipLimitPagination.js +11 -9
  64. package/dist/react/FragmentReader.d.ts +7 -14
  65. package/dist/react/FragmentReader.d.ts.map +1 -1
  66. package/dist/react/FragmentReader.js +3 -30
  67. package/dist/react/FragmentRenderer.d.ts +15 -0
  68. package/dist/react/FragmentRenderer.d.ts.map +1 -0
  69. package/dist/react/FragmentRenderer.js +35 -0
  70. package/dist/react/IsographEnvironmentProvider.d.ts.map +1 -1
  71. package/dist/react/LoadableFieldReader.d.ts +12 -0
  72. package/dist/react/LoadableFieldReader.d.ts.map +1 -0
  73. package/dist/react/LoadableFieldReader.js +10 -0
  74. package/dist/react/LoadableFieldRenderer.d.ts +13 -0
  75. package/dist/react/LoadableFieldRenderer.d.ts.map +1 -0
  76. package/dist/react/LoadableFieldRenderer.js +37 -0
  77. package/dist/react/useImperativeReference.d.ts +7 -10
  78. package/dist/react/useImperativeReference.d.ts.map +1 -1
  79. package/dist/react/useImperativeReference.js +8 -9
  80. package/dist/react/useLazyReference.d.ts +4 -7
  81. package/dist/react/useLazyReference.d.ts.map +1 -1
  82. package/dist/react/useLazyReference.js +26 -5
  83. package/dist/react/useReadAndSubscribe.d.ts +3 -9
  84. package/dist/react/useReadAndSubscribe.d.ts.map +1 -1
  85. package/dist/react/useReadAndSubscribe.js +7 -3
  86. package/dist/react/useRerenderOnChange.d.ts +1 -1
  87. package/dist/react/useRerenderOnChange.d.ts.map +1 -1
  88. package/dist/react/useResult.d.ts +3 -6
  89. package/dist/react/useResult.d.ts.map +1 -1
  90. package/dist/react/useResult.js +10 -8
  91. package/isograph.config.json +1 -0
  92. package/package.json +6 -6
  93. package/src/core/FragmentReference.ts +40 -16
  94. package/src/core/IsographEnvironment.ts +57 -39
  95. package/src/core/PromiseWrapper.ts +15 -18
  96. package/src/core/areEqualWithDeepComparison.ts +22 -2
  97. package/src/core/brand.ts +18 -0
  98. package/src/core/cache.ts +153 -113
  99. package/src/core/check.ts +17 -12
  100. package/src/core/componentCache.ts +47 -50
  101. package/src/core/entrypoint.ts +66 -21
  102. package/src/core/garbageCollection.ts +9 -9
  103. package/src/core/logging.ts +39 -25
  104. package/src/core/makeNetworkRequest.ts +212 -34
  105. package/src/core/read.ts +728 -440
  106. package/src/core/reader.ts +46 -29
  107. package/src/core/startUpdate.ts +334 -0
  108. package/src/core/util.ts +4 -0
  109. package/src/index.ts +89 -8
  110. package/src/loadable-hooks/useClientSideDefer.ts +11 -10
  111. package/src/loadable-hooks/useConnectionSpecPagination.ts +27 -13
  112. package/src/loadable-hooks/useImperativeExposedMutationField.ts +1 -1
  113. package/src/loadable-hooks/useImperativeLoadableField.ts +10 -12
  114. package/src/loadable-hooks/useSkipLimitPagination.ts +38 -19
  115. package/src/react/FragmentReader.tsx +23 -39
  116. package/src/react/FragmentRenderer.tsx +46 -0
  117. package/src/react/IsographEnvironmentProvider.tsx +1 -1
  118. package/src/react/LoadableFieldReader.tsx +40 -0
  119. package/src/react/LoadableFieldRenderer.tsx +41 -0
  120. package/src/react/useImperativeReference.ts +49 -27
  121. package/src/react/useLazyReference.ts +62 -14
  122. package/src/react/useReadAndSubscribe.ts +17 -9
  123. package/src/react/useRerenderOnChange.ts +2 -2
  124. package/src/react/useResult.ts +22 -8
  125. package/src/tests/__isograph/Economist/link/output_type.ts +2 -0
  126. package/src/tests/__isograph/Node/asEconomist/resolver_reader.ts +28 -0
  127. package/src/tests/__isograph/Node/link/output_type.ts +3 -0
  128. package/src/tests/__isograph/Query/linkedUpdate/entrypoint.ts +31 -0
  129. package/src/tests/__isograph/Query/linkedUpdate/normalization_ast.ts +95 -0
  130. package/src/tests/__isograph/Query/linkedUpdate/output_type.ts +3 -0
  131. package/src/tests/__isograph/Query/linkedUpdate/param_type.ts +51 -0
  132. package/src/tests/__isograph/Query/linkedUpdate/query_text.ts +20 -0
  133. package/src/tests/__isograph/Query/linkedUpdate/resolver_reader.ts +93 -0
  134. package/src/tests/__isograph/Query/meName/entrypoint.ts +8 -29
  135. package/src/tests/__isograph/Query/meName/normalization_ast.ts +25 -0
  136. package/src/tests/__isograph/Query/meName/query_text.ts +6 -0
  137. package/src/tests/__isograph/Query/meName/resolver_reader.ts +5 -0
  138. package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +8 -67
  139. package/src/tests/__isograph/Query/meNameSuccessor/normalization_ast.ts +56 -0
  140. package/src/tests/__isograph/Query/meNameSuccessor/query_text.ts +13 -0
  141. package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +10 -0
  142. package/src/tests/__isograph/Query/nodeField/entrypoint.ts +8 -34
  143. package/src/tests/__isograph/Query/nodeField/normalization_ast.ts +30 -0
  144. package/src/tests/__isograph/Query/nodeField/query_text.ts +6 -0
  145. package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +5 -0
  146. package/src/tests/__isograph/Query/startUpdate/entrypoint.ts +31 -0
  147. package/src/tests/__isograph/Query/startUpdate/normalization_ast.ts +51 -0
  148. package/src/tests/__isograph/Query/startUpdate/output_type.ts +3 -0
  149. package/src/tests/__isograph/Query/startUpdate/param_type.ts +26 -0
  150. package/src/tests/__isograph/Query/startUpdate/parameters_type.ts +3 -0
  151. package/src/tests/__isograph/Query/startUpdate/query_text.ts +11 -0
  152. package/src/tests/__isograph/Query/startUpdate/resolver_reader.ts +55 -0
  153. package/src/tests/__isograph/Query/subquery/entrypoint.ts +8 -44
  154. package/src/tests/__isograph/Query/subquery/normalization_ast.ts +38 -0
  155. package/src/tests/__isograph/Query/subquery/query_text.ts +8 -0
  156. package/src/tests/__isograph/Query/subquery/resolver_reader.ts +7 -0
  157. package/src/tests/__isograph/iso.ts +24 -3
  158. package/src/tests/__isograph/tsconfig.json +8 -0
  159. package/src/tests/garbageCollection.test.ts +10 -8
  160. package/src/tests/meNameSuccessor.ts +1 -1
  161. package/src/tests/nodeQuery.ts +2 -1
  162. package/src/tests/normalizeData.test.ts +1 -2
  163. package/src/tests/startUpdate.test.ts +205 -0
  164. package/tsconfig.pkg.json +1 -2
package/src/core/read.ts CHANGED
@@ -1,39 +1,51 @@
1
+ import { CleanupFn, type ItemCleanupPair } from '@isograph/disposable-types';
1
2
  import {
2
3
  getParentRecordKey,
3
- insertIfNotExists,
4
+ insertEmptySetIfMissing,
4
5
  onNextChangeToRecord,
5
6
  type EncounteredIds,
6
7
  } from './cache';
8
+ import { FetchOptions } from './check';
7
9
  import { getOrCreateCachedComponent } from './componentCache';
8
10
  import {
9
11
  IsographEntrypoint,
10
12
  RefetchQueryNormalizationArtifactWrapper,
13
+ type ReaderWithRefetchQueries,
11
14
  } from './entrypoint';
12
15
  import {
16
+ ExtractData,
13
17
  FragmentReference,
14
18
  Variables,
15
- ExtractData,
16
- ExtractParameters,
19
+ type UnknownTReadFromStore,
17
20
  } from './FragmentReference';
18
21
  import {
19
22
  assertLink,
20
23
  getOrLoadIsographArtifact,
21
24
  IsographEnvironment,
22
- type Link,
25
+ type DataTypeValue,
26
+ type StoreLink,
27
+ type StoreRecord,
23
28
  } from './IsographEnvironment';
29
+ import { logMessage } from './logging';
24
30
  import { maybeMakeNetworkRequest } from './makeNetworkRequest';
25
31
  import {
26
32
  getPromiseState,
33
+ NOT_SET,
27
34
  PromiseWrapper,
28
35
  readPromise,
29
36
  wrapPromise,
30
37
  wrapResolvedValue,
31
38
  } from './PromiseWrapper';
32
- import { ReaderAst } from './reader';
39
+ import {
40
+ ReaderAst,
41
+ type LoadablySelectedField,
42
+ type ReaderImperativelyLoadedField,
43
+ type ReaderLinkedField,
44
+ type ReaderNonLoadableResolverField,
45
+ type ReaderScalarField,
46
+ } from './reader';
47
+ import { getOrCreateCachedStartUpdate } from './startUpdate';
33
48
  import { Arguments } from './util';
34
- import { logMessage } from './logging';
35
- import { CleanupFn } from '@isograph/disposable-types';
36
- import { FetchOptions } from './check';
37
49
 
38
50
  export type WithEncounteredRecords<T> = {
39
51
  readonly encounteredRecords: EncounteredIds;
@@ -41,7 +53,7 @@ export type WithEncounteredRecords<T> = {
41
53
  };
42
54
 
43
55
  export function readButDoNotEvaluate<
44
- TReadFromStore extends { parameters: object; data: object },
56
+ TReadFromStore extends UnknownTReadFromStore,
45
57
  >(
46
58
  environment: IsographEnvironment,
47
59
  fragmentReference: FragmentReference<TReadFromStore, unknown>,
@@ -49,6 +61,7 @@ export function readButDoNotEvaluate<
49
61
  ): WithEncounteredRecords<TReadFromStore> {
50
62
  const mutableEncounteredRecords: EncounteredIds = new Map();
51
63
 
64
+ // TODO consider moving this to the outside
52
65
  const readerWithRefetchQueries = readPromise(
53
66
  fragmentReference.readerWithRefetchQueries,
54
67
  );
@@ -64,14 +77,16 @@ export function readButDoNotEvaluate<
64
77
  mutableEncounteredRecords,
65
78
  );
66
79
 
67
- logMessage(environment, {
80
+ logMessage(environment, () => ({
68
81
  kind: 'DoneReading',
69
82
  response,
70
- });
83
+ fieldName: readerWithRefetchQueries.readerArtifact.fieldName,
84
+ root: fragmentReference.root,
85
+ }));
71
86
 
72
87
  if (response.kind === 'MissingData') {
73
88
  // There are two cases here that we care about:
74
- // 1. the network request is in flight, we haven't suspend on it, and we want
89
+ // 1. the network request is in flight, we haven't suspended on it, and we want
75
90
  // to throw if it errors out. So, networkRequestOptions.suspendIfInFlight === false
76
91
  // and networkRequestOptions.throwOnNetworkError === true.
77
92
  // 2. everything else
@@ -83,7 +98,17 @@ export function readButDoNotEvaluate<
83
98
  !networkRequestOptions.suspendIfInFlight &&
84
99
  networkRequestOptions.throwOnNetworkError
85
100
  ) {
86
- // TODO assert that the network request state is not Err
101
+ // What are we doing here? If the network response has errored out, we can do
102
+ // two things: throw a rejected promise, or throw an error. Both work identically
103
+ // in the browser. However, during initial SSR on NextJS, throwing a rejected
104
+ // promise results in an infinite loop (including re-issuing the query until the
105
+ // process OOM's or something.) Hence, we throw an error.
106
+
107
+ const result = fragmentReference.networkRequest.result;
108
+ if (result !== NOT_SET && result.kind === 'Err') {
109
+ throw new Error('NetworkError', { cause: result.error });
110
+ }
111
+
87
112
  throw new Promise((resolve, reject) => {
88
113
  onNextChangeToRecord(environment, response.recordLink).then(resolve);
89
114
  fragmentReference.networkRequest.promise.catch(reject);
@@ -98,30 +123,31 @@ export function readButDoNotEvaluate<
98
123
  }
99
124
  }
100
125
 
101
- export type ReadDataResult<TReadFromStore> =
102
- | {
103
- readonly kind: 'Success';
104
- readonly data: ExtractData<TReadFromStore>;
105
- readonly encounteredRecords: EncounteredIds;
106
- }
126
+ export type ReadDataResultSuccess<Data> = {
127
+ readonly kind: 'Success';
128
+ readonly data: Data;
129
+ };
130
+
131
+ export type ReadDataResult<Data> =
132
+ | ReadDataResultSuccess<Data>
107
133
  | {
108
134
  readonly kind: 'MissingData';
109
135
  readonly reason: string;
110
136
  readonly nestedReason?: ReadDataResult<unknown>;
111
- readonly recordLink: Link;
137
+ readonly recordLink: StoreLink;
112
138
  };
113
139
 
114
140
  function readData<TReadFromStore>(
115
141
  environment: IsographEnvironment,
116
142
  ast: ReaderAst<TReadFromStore>,
117
- root: Link,
118
- variables: ExtractParameters<TReadFromStore>,
143
+ root: StoreLink,
144
+ variables: Variables,
119
145
  nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
120
146
  networkRequest: PromiseWrapper<void, any>,
121
147
  networkRequestOptions: NetworkRequestReaderOptions,
122
148
  mutableEncounteredRecords: EncounteredIds,
123
- ): ReadDataResult<TReadFromStore> {
124
- const encounteredIds = insertIfNotExists(
149
+ ): ReadDataResult<ExtractData<TReadFromStore>> {
150
+ const encounteredIds = insertEmptySetIfMissing(
125
151
  mutableEncounteredRecords,
126
152
  root.__typename,
127
153
  );
@@ -139,7 +165,6 @@ function readData<TReadFromStore>(
139
165
  return {
140
166
  kind: 'Success',
141
167
  data: null as any,
142
- encounteredRecords: mutableEncounteredRecords,
143
168
  };
144
169
  }
145
170
 
@@ -148,156 +173,51 @@ function readData<TReadFromStore>(
148
173
  for (const field of ast) {
149
174
  switch (field.kind) {
150
175
  case 'Scalar': {
151
- const storeRecordName = getParentRecordKey(field, variables);
152
- const value = storeRecord[storeRecordName];
153
- // TODO consider making scalars into discriminated unions. This probably has
154
- // to happen for when we handle errors.
155
- if (value === undefined) {
156
- return {
157
- kind: 'MissingData',
158
- reason:
159
- 'No value for ' + storeRecordName + ' on root ' + root.__link,
160
- recordLink: root,
161
- };
176
+ const data = readScalarFieldData(field, storeRecord, root, variables);
177
+
178
+ if (data.kind === 'MissingData') {
179
+ return data;
162
180
  }
163
- target[field.alias ?? field.fieldName] = value;
181
+ target[field.alias ?? field.fieldName] = data.data;
182
+ break;
183
+ }
184
+ case 'Link': {
185
+ target[field.alias] = root;
164
186
  break;
165
187
  }
166
188
  case 'Linked': {
167
- const storeRecordName = getParentRecordKey(field, variables);
168
- const value = storeRecord[storeRecordName];
169
- if (Array.isArray(value)) {
170
- const results = [];
171
- for (const item of value) {
172
- const link = assertLink(item);
173
- if (link === undefined) {
174
- return {
175
- kind: 'MissingData',
176
- reason:
177
- 'No link for ' +
178
- storeRecordName +
179
- ' on root ' +
180
- root.__link +
181
- '. Link is ' +
182
- JSON.stringify(item),
183
- recordLink: root,
184
- };
185
- } else if (link === null) {
186
- results.push(null);
187
- continue;
188
- }
189
-
190
- const result = readData(
189
+ const data = readLinkedFieldData(
190
+ environment,
191
+ field,
192
+ storeRecord,
193
+ root,
194
+ variables,
195
+ nestedRefetchQueries,
196
+ networkRequest,
197
+ networkRequestOptions,
198
+ (ast, root) =>
199
+ readData(
191
200
  environment,
192
- field.selections,
193
- link,
201
+ ast,
202
+ root,
194
203
  variables,
195
204
  nestedRefetchQueries,
196
205
  networkRequest,
197
206
  networkRequestOptions,
198
207
  mutableEncounteredRecords,
199
- );
200
- if (result.kind === 'MissingData') {
201
- return {
202
- kind: 'MissingData',
203
- reason:
204
- 'Missing data for ' +
205
- storeRecordName +
206
- ' on root ' +
207
- root.__link +
208
- '. Link is ' +
209
- JSON.stringify(item),
210
- nestedReason: result,
211
- recordLink: result.recordLink,
212
- };
213
- }
214
- results.push(result.data);
215
- }
216
- target[field.alias ?? field.fieldName] = results;
217
- break;
218
- }
219
- let link = assertLink(value);
220
-
221
- if (field.condition) {
222
- const data = readData(
223
- environment,
224
- field.condition.readerAst,
225
- root,
226
- variables,
227
- nestedRefetchQueries,
228
- networkRequest,
229
- networkRequestOptions,
230
- mutableEncounteredRecords,
231
- );
232
- if (data.kind === 'MissingData') {
233
- return {
234
- kind: 'MissingData',
235
- reason:
236
- 'Missing data for ' +
237
- storeRecordName +
238
- ' on root ' +
239
- root.__link,
240
- nestedReason: data,
241
- recordLink: data.recordLink,
242
- };
243
- }
244
- const condition = field.condition.resolver({
245
- data: data.data,
246
- parameters: {},
247
- });
248
- if (condition === true) {
249
- link = root;
250
- } else if (condition === false) {
251
- link = null;
252
- } else {
253
- link = condition;
254
- }
255
- }
256
-
257
- if (link === undefined) {
258
- // TODO make this configurable, and also generated and derived from the schema
259
- const missingFieldHandler = environment.missingFieldHandler;
260
-
261
- const altLink = missingFieldHandler?.(
262
- storeRecord,
263
- root,
264
- field.fieldName,
265
- field.arguments,
266
- variables,
267
- );
268
- logMessage(environment, {
269
- kind: 'MissingFieldHandlerCalled',
270
- root,
271
- storeRecord,
272
- fieldName: field.fieldName,
273
- arguments: field.arguments,
274
- variables,
275
- });
276
-
277
- if (altLink === undefined) {
278
- return {
279
- kind: 'MissingData',
280
- reason:
281
- 'No link for ' +
282
- storeRecordName +
283
- ' on root ' +
284
- root.__link +
285
- '. Link is ' +
286
- JSON.stringify(value),
287
- recordLink: root,
288
- };
289
- } else {
290
- link = altLink;
291
- }
292
- } else if (link === null) {
293
- target[field.alias ?? field.fieldName] = null;
294
- break;
208
+ ),
209
+ );
210
+ if (data.kind === 'MissingData') {
211
+ return data;
295
212
  }
296
- const targetId = link;
297
- const data = readData(
213
+ target[field.alias ?? field.fieldName] = data.data;
214
+ break;
215
+ }
216
+ case 'ImperativelyLoadedField': {
217
+ const data = readImperativelyLoadedField(
298
218
  environment,
299
- field.selections,
300
- targetId,
219
+ field,
220
+ root,
301
221
  variables,
302
222
  nestedRefetchQueries,
303
223
  networkRequest,
@@ -305,306 +225,45 @@ function readData<TReadFromStore>(
305
225
  mutableEncounteredRecords,
306
226
  );
307
227
  if (data.kind === 'MissingData') {
308
- return {
309
- kind: 'MissingData',
310
- reason:
311
- 'Missing data for ' + storeRecordName + ' on root ' + root.__link,
312
- nestedReason: data,
313
- recordLink: data.recordLink,
314
- };
228
+ return data;
315
229
  }
316
- target[field.alias ?? field.fieldName] = data.data;
230
+ target[field.alias] = data.data;
317
231
  break;
318
232
  }
319
- case 'ImperativelyLoadedField': {
320
- // First, we read the data using the refetch reader AST (i.e. read out the
321
- // id field).
322
- const data = readData(
233
+ case 'Resolver': {
234
+ const data = readResolverFieldData(
323
235
  environment,
324
- field.refetchReaderArtifact.readerAst,
236
+ field,
325
237
  root,
326
238
  variables,
327
- // Refetch fields just read the id, and don't need refetch query artifacts
328
- [],
329
- // This is probably indicative of the fact that we are doing redundant checks
330
- // on the status of this network request...
239
+ nestedRefetchQueries,
331
240
  networkRequest,
332
241
  networkRequestOptions,
333
242
  mutableEncounteredRecords,
334
243
  );
335
244
  if (data.kind === 'MissingData') {
336
- return {
337
- kind: 'MissingData',
338
- reason:
339
- 'Missing data for ' + field.alias + ' on root ' + root.__link,
340
- nestedReason: data,
341
- recordLink: data.recordLink,
342
- };
343
- } else {
344
- const refetchQueryIndex = field.refetchQuery;
345
- const refetchQuery = nestedRefetchQueries[refetchQueryIndex];
346
- if (refetchQuery == null) {
347
- throw new Error(
348
- 'refetchQuery is null in RefetchField. This is indicative of a bug in Isograph.',
349
- );
350
- }
351
- const refetchQueryArtifact = refetchQuery.artifact;
352
- const allowedVariables = refetchQuery.allowedVariables;
353
-
354
- // Second, we allow the user to call the resolver, which will ultimately
355
- // use the resolver reader AST to get the resolver parameters.
356
- target[field.alias] = (args: any) => [
357
- // Stable id
358
- root.__link + '__' + field.name,
359
- // Fetcher
360
- field.refetchReaderArtifact.resolver(
361
- environment,
362
- refetchQueryArtifact,
363
- data.data,
364
- filterVariables({ ...args, ...variables }, allowedVariables),
365
- root,
366
- // TODO these params should be removed
367
- null,
368
- [],
369
- ),
370
- ];
371
- }
372
- break;
373
- }
374
- case 'Resolver': {
375
- const usedRefetchQueries = field.usedRefetchQueries;
376
- const resolverRefetchQueries = usedRefetchQueries.map((index) => {
377
- const resolverRefetchQuery = nestedRefetchQueries[index];
378
- if (resolverRefetchQuery == null) {
379
- throw new Error(
380
- 'resolverRefetchQuery is null in Resolver. This is indicative of a bug in Isograph.',
381
- );
382
- }
383
- return resolverRefetchQuery;
384
- });
385
-
386
- switch (field.readerArtifact.kind) {
387
- case 'EagerReaderArtifact': {
388
- const data = readData(
389
- environment,
390
- field.readerArtifact.readerAst,
391
- root,
392
- generateChildVariableMap(variables, field.arguments),
393
- resolverRefetchQueries,
394
- networkRequest,
395
- networkRequestOptions,
396
- mutableEncounteredRecords,
397
- );
398
- if (data.kind === 'MissingData') {
399
- return {
400
- kind: 'MissingData',
401
- reason:
402
- 'Missing data for ' + field.alias + ' on root ' + root.__link,
403
- nestedReason: data,
404
- recordLink: data.recordLink,
405
- };
406
- } else {
407
- const firstParameter = {
408
- data: data.data,
409
- parameters: variables,
410
- };
411
- target[field.alias] =
412
- field.readerArtifact.resolver(firstParameter);
413
- }
414
- break;
415
- }
416
- case 'ComponentReaderArtifact': {
417
- target[field.alias] = getOrCreateCachedComponent(
418
- environment,
419
- field.readerArtifact.componentName,
420
- {
421
- kind: 'FragmentReference',
422
- readerWithRefetchQueries: wrapResolvedValue({
423
- kind: 'ReaderWithRefetchQueries',
424
- readerArtifact: field.readerArtifact,
425
- nestedRefetchQueries: resolverRefetchQueries,
426
- }),
427
- root,
428
- variables: generateChildVariableMap(variables, field.arguments),
429
- networkRequest,
430
- } as const,
431
- networkRequestOptions,
432
- );
433
- break;
434
- }
435
- default: {
436
- let _: never = field.readerArtifact;
437
- _;
438
- throw new Error('Unexpected kind');
439
- }
245
+ return data;
440
246
  }
247
+ target[field.alias] = data.data;
441
248
  break;
442
249
  }
443
250
  case 'LoadablySelectedField': {
444
- const refetchReaderParams = readData(
251
+ const data = readLoadablySelectedFieldData(
445
252
  environment,
446
- field.refetchReaderAst,
253
+ field,
447
254
  root,
448
255
  variables,
449
- // Refetch fields just read the id, and don't need refetch query artifacts
450
- [],
451
256
  networkRequest,
452
257
  networkRequestOptions,
453
258
  mutableEncounteredRecords,
454
259
  );
455
- if (refetchReaderParams.kind === 'MissingData') {
456
- return {
457
- kind: 'MissingData',
458
- reason:
459
- 'Missing data for ' + field.alias + ' on root ' + root.__link,
460
- nestedReason: refetchReaderParams,
461
- recordLink: refetchReaderParams.recordLink,
462
- };
463
- } else {
464
- target[field.alias] = (args: any, fetchOptions?: FetchOptions) => {
465
- // TODO we should use the reader AST for this
466
- const includeReadOutData = (variables: any, readOutData: any) => {
467
- variables.id = readOutData.id;
468
- return variables;
469
- };
470
- const localVariables = includeReadOutData(
471
- args ?? {},
472
- refetchReaderParams.data,
473
- );
474
- writeQueryArgsToVariables(
475
- localVariables,
476
- field.queryArguments,
477
- variables,
478
- );
479
-
480
- return [
481
- // Stable id
482
- root.__typename +
483
- ':' +
484
- root.__link +
485
- '/' +
486
- field.name +
487
- '/' +
488
- stableStringifyArgs(localVariables),
489
- // Fetcher
490
- () => {
491
- const fragmentReferenceAndDisposeFromEntrypoint = (
492
- entrypoint: IsographEntrypoint<any, any>,
493
- ): [FragmentReference<any, any>, CleanupFn] => {
494
- const [networkRequest, disposeNetworkRequest] =
495
- maybeMakeNetworkRequest(
496
- environment,
497
- entrypoint,
498
- localVariables,
499
- fetchOptions,
500
- );
501
-
502
- const fragmentReference: FragmentReference<any, any> = {
503
- kind: 'FragmentReference',
504
- readerWithRefetchQueries: wrapResolvedValue({
505
- kind: 'ReaderWithRefetchQueries',
506
- readerArtifact:
507
- entrypoint.readerWithRefetchQueries.readerArtifact,
508
- nestedRefetchQueries:
509
- entrypoint.readerWithRefetchQueries
510
- .nestedRefetchQueries,
511
- } as const),
512
-
513
- // TODO localVariables is not guaranteed to have an id field
514
- root,
515
- variables: localVariables,
516
- networkRequest,
517
- };
518
- return [fragmentReference, disposeNetworkRequest];
519
- };
520
-
521
- if (field.entrypoint.kind === 'Entrypoint') {
522
- return fragmentReferenceAndDisposeFromEntrypoint(
523
- field.entrypoint,
524
- );
525
- } else {
526
- const isographArtifactPromiseWrapper =
527
- getOrLoadIsographArtifact(
528
- environment,
529
- field.entrypoint.typeAndField,
530
- field.entrypoint.loader,
531
- );
532
- const state = getPromiseState(isographArtifactPromiseWrapper);
533
- if (state.kind === 'Ok') {
534
- return fragmentReferenceAndDisposeFromEntrypoint(
535
- state.value,
536
- );
537
- } else {
538
- // Promise is pending or thrown
539
-
540
- let entrypointLoaderState:
541
- | {
542
- kind: 'EntrypointNotLoaded';
543
- }
544
- | {
545
- kind: 'NetworkRequestStarted';
546
- disposeNetworkRequest: CleanupFn;
547
- }
548
- | { kind: 'Disposed' } = { kind: 'EntrypointNotLoaded' };
549
-
550
- const networkRequest = wrapPromise(
551
- isographArtifactPromiseWrapper.promise.then(
552
- (entrypoint) => {
553
- if (
554
- entrypointLoaderState.kind === 'EntrypointNotLoaded'
555
- ) {
556
- const [networkRequest, disposeNetworkRequest] =
557
- maybeMakeNetworkRequest(
558
- environment,
559
- entrypoint,
560
- localVariables,
561
- fetchOptions,
562
- );
563
- entrypointLoaderState = {
564
- kind: 'NetworkRequestStarted',
565
- disposeNetworkRequest,
566
- };
567
- return networkRequest.promise;
568
- }
569
- },
570
- ),
571
- );
572
- const readerWithRefetchPromise =
573
- isographArtifactPromiseWrapper.promise.then(
574
- (entrypoint) => entrypoint.readerWithRefetchQueries,
575
- );
576
-
577
- const fragmentReference: FragmentReference<any, any> = {
578
- kind: 'FragmentReference',
579
- readerWithRefetchQueries: wrapPromise(
580
- readerWithRefetchPromise,
581
- ),
582
-
583
- // TODO localVariables is not guaranteed to have an id field
584
- root,
585
- variables: localVariables,
586
- networkRequest,
587
- };
588
-
589
- return [
590
- fragmentReference,
591
- () => {
592
- if (
593
- entrypointLoaderState.kind === 'NetworkRequestStarted'
594
- ) {
595
- entrypointLoaderState.disposeNetworkRequest();
596
- }
597
- entrypointLoaderState = { kind: 'Disposed' };
598
- },
599
- ];
600
- }
601
- }
602
- },
603
- ];
604
- };
260
+ if (data.kind === 'MissingData') {
261
+ return data;
605
262
  }
263
+ target[field.alias] = data.data;
606
264
  break;
607
265
  }
266
+
608
267
  default: {
609
268
  // Ensure we have covered all variants
610
269
  let _: never = field;
@@ -616,7 +275,177 @@ function readData<TReadFromStore>(
616
275
  return {
617
276
  kind: 'Success',
618
277
  data: target as any,
619
- encounteredRecords: mutableEncounteredRecords,
278
+ };
279
+ }
280
+
281
+ export function readLoadablySelectedFieldData(
282
+ environment: IsographEnvironment,
283
+ field: LoadablySelectedField,
284
+ root: StoreLink,
285
+ variables: Variables,
286
+ networkRequest: PromiseWrapper<void, any>,
287
+ networkRequestOptions: NetworkRequestReaderOptions,
288
+ mutableEncounteredRecords: EncounteredIds,
289
+ ): ReadDataResult<unknown> {
290
+ const refetchReaderParams = readData(
291
+ environment,
292
+ field.refetchReaderAst,
293
+ root,
294
+ variables,
295
+ // Refetch fields just read the id, and don't need refetch query artifacts
296
+ [],
297
+ networkRequest,
298
+ networkRequestOptions,
299
+ mutableEncounteredRecords,
300
+ );
301
+
302
+ if (refetchReaderParams.kind === 'MissingData') {
303
+ return {
304
+ kind: 'MissingData',
305
+ reason: 'Missing data for ' + field.alias + ' on root ' + root.__link,
306
+ nestedReason: refetchReaderParams,
307
+ recordLink: refetchReaderParams.recordLink,
308
+ };
309
+ }
310
+
311
+ return {
312
+ kind: 'Success',
313
+ data: (
314
+ args: any,
315
+ // TODO get the associated type for FetchOptions from the loadably selected field
316
+ fetchOptions?: FetchOptions<any>,
317
+ ) => {
318
+ // TODO we should use the reader AST for this
319
+ const includeReadOutData = (variables: any, readOutData: any) => {
320
+ variables.id = readOutData.id;
321
+ return variables;
322
+ };
323
+ const localVariables = includeReadOutData(
324
+ args ?? {},
325
+ refetchReaderParams.data,
326
+ );
327
+ writeQueryArgsToVariables(
328
+ localVariables,
329
+ field.queryArguments,
330
+ variables,
331
+ );
332
+
333
+ return [
334
+ // Stable id
335
+ root.__typename +
336
+ ':' +
337
+ root.__link +
338
+ '/' +
339
+ field.name +
340
+ '/' +
341
+ stableStringifyArgs(localVariables),
342
+ // Fetcher
343
+ () => {
344
+ const fragmentReferenceAndDisposeFromEntrypoint = (
345
+ entrypoint: IsographEntrypoint<any, any, any>,
346
+ ): [FragmentReference<any, any>, CleanupFn] => {
347
+ const readerWithRefetchQueries =
348
+ entrypoint.readerWithRefetchQueries.kind ===
349
+ 'ReaderWithRefetchQueriesLoader'
350
+ ? wrapPromise(entrypoint.readerWithRefetchQueries.loader())
351
+ : wrapResolvedValue(entrypoint.readerWithRefetchQueries);
352
+ const [networkRequest, disposeNetworkRequest] =
353
+ maybeMakeNetworkRequest(
354
+ environment,
355
+ entrypoint,
356
+ localVariables,
357
+ readerWithRefetchQueries,
358
+ fetchOptions ?? null,
359
+ );
360
+
361
+ const fragmentReference: FragmentReference<any, any> = {
362
+ kind: 'FragmentReference',
363
+ readerWithRefetchQueries,
364
+
365
+ // TODO localVariables is not guaranteed to have an id field
366
+ root,
367
+ variables: localVariables,
368
+ networkRequest,
369
+ };
370
+ return [fragmentReference, disposeNetworkRequest];
371
+ };
372
+
373
+ if (field.entrypoint.kind === 'Entrypoint') {
374
+ return fragmentReferenceAndDisposeFromEntrypoint(field.entrypoint);
375
+ } else {
376
+ const isographArtifactPromiseWrapper = getOrLoadIsographArtifact(
377
+ environment,
378
+ field.entrypoint.typeAndField,
379
+ field.entrypoint.loader,
380
+ );
381
+ const state = getPromiseState(isographArtifactPromiseWrapper);
382
+ if (state.kind === 'Ok') {
383
+ return fragmentReferenceAndDisposeFromEntrypoint(state.value);
384
+ } else {
385
+ // Promise is pending or thrown
386
+
387
+ let entrypointLoaderState:
388
+ | {
389
+ kind: 'EntrypointNotLoaded';
390
+ }
391
+ | {
392
+ kind: 'NetworkRequestStarted';
393
+ disposeNetworkRequest: CleanupFn;
394
+ }
395
+ | { kind: 'Disposed' } = { kind: 'EntrypointNotLoaded' };
396
+
397
+ const readerWithRefetchQueries = wrapPromise(
398
+ isographArtifactPromiseWrapper.promise.then((entrypoint) =>
399
+ entrypoint.readerWithRefetchQueries.kind ===
400
+ 'ReaderWithRefetchQueriesLoader'
401
+ ? entrypoint.readerWithRefetchQueries.loader()
402
+ : entrypoint.readerWithRefetchQueries,
403
+ ),
404
+ );
405
+ const networkRequest = wrapPromise(
406
+ isographArtifactPromiseWrapper.promise.then((entrypoint) => {
407
+ if (entrypointLoaderState.kind === 'EntrypointNotLoaded') {
408
+ const [networkRequest, disposeNetworkRequest] =
409
+ maybeMakeNetworkRequest(
410
+ environment,
411
+ entrypoint,
412
+ localVariables,
413
+ readerWithRefetchQueries,
414
+ fetchOptions ?? null,
415
+ );
416
+ entrypointLoaderState = {
417
+ kind: 'NetworkRequestStarted',
418
+ disposeNetworkRequest,
419
+ };
420
+ return networkRequest.promise;
421
+ }
422
+ }),
423
+ );
424
+
425
+ const fragmentReference: FragmentReference<any, any> = {
426
+ kind: 'FragmentReference',
427
+ readerWithRefetchQueries,
428
+
429
+ // TODO localVariables is not guaranteed to have an id field
430
+ root,
431
+ variables: localVariables,
432
+ networkRequest,
433
+ };
434
+
435
+ return [
436
+ fragmentReference,
437
+ () => {
438
+ if (entrypointLoaderState.kind === 'NetworkRequestStarted') {
439
+ entrypointLoaderState.disposeNetworkRequest();
440
+ }
441
+ entrypointLoaderState = { kind: 'Disposed' };
442
+ },
443
+ ];
444
+ }
445
+ }
446
+ },
447
+ ];
448
+ },
620
449
  };
621
450
  }
622
451
 
@@ -643,7 +472,9 @@ function generateChildVariableMap(
643
472
  type Writable<T> = { -readonly [P in keyof T]: T[P] };
644
473
  const childVars: Writable<Variables> = {};
645
474
  for (const [name, value] of fieldArguments) {
646
- if (value.kind === 'Variable') {
475
+ if (value.kind === 'Object') {
476
+ childVars[name] = generateChildVariableMap(variables, value.value);
477
+ } else if (value.kind === 'Variable') {
647
478
  const variable = variables[value.name];
648
479
  // Variable could be null if it was not provided but has a default case,
649
480
  // so we allow the loop to continue rather than throwing an error.
@@ -667,6 +498,14 @@ function writeQueryArgsToVariables(
667
498
  }
668
499
  for (const [name, argType] of queryArgs) {
669
500
  switch (argType.kind) {
501
+ case 'Object': {
502
+ writeQueryArgsToVariables(
503
+ (targetVariables[name] = {}),
504
+ argType.value,
505
+ variables,
506
+ );
507
+ break;
508
+ }
670
509
  case 'Variable': {
671
510
  targetVariables[name] = variables[argType.name];
672
511
  break;
@@ -692,6 +531,389 @@ function writeQueryArgsToVariables(
692
531
  }
693
532
  }
694
533
 
534
+ export function readResolverFieldData(
535
+ environment: IsographEnvironment,
536
+ field: ReaderNonLoadableResolverField,
537
+ root: StoreLink,
538
+ variables: Variables,
539
+ nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
540
+ networkRequest: PromiseWrapper<void, any>,
541
+ networkRequestOptions: NetworkRequestReaderOptions,
542
+ mutableEncounteredRecords: EncounteredIds,
543
+ ): ReadDataResult<unknown> {
544
+ const usedRefetchQueries = field.usedRefetchQueries;
545
+ const resolverRefetchQueries = usedRefetchQueries.map((index) => {
546
+ const resolverRefetchQuery = nestedRefetchQueries[index];
547
+ if (resolverRefetchQuery == null) {
548
+ throw new Error(
549
+ 'resolverRefetchQuery is null in Resolver. This is indicative of a bug in Isograph.',
550
+ );
551
+ }
552
+ return resolverRefetchQuery;
553
+ });
554
+
555
+ const readerWithRefetchQueries = {
556
+ kind: 'ReaderWithRefetchQueries',
557
+ readerArtifact: field.readerArtifact,
558
+ nestedRefetchQueries: resolverRefetchQueries,
559
+ } satisfies ReaderWithRefetchQueries<any, any>;
560
+
561
+ const fragment = {
562
+ kind: 'FragmentReference',
563
+ readerWithRefetchQueries: wrapResolvedValue(readerWithRefetchQueries),
564
+ root,
565
+ variables: generateChildVariableMap(variables, field.arguments),
566
+ networkRequest,
567
+ } satisfies FragmentReference<any, any>;
568
+
569
+ switch (field.readerArtifact.kind) {
570
+ case 'EagerReaderArtifact': {
571
+ const data = readData(
572
+ environment,
573
+ field.readerArtifact.readerAst,
574
+ root,
575
+ generateChildVariableMap(variables, field.arguments),
576
+ resolverRefetchQueries,
577
+ networkRequest,
578
+ networkRequestOptions,
579
+ mutableEncounteredRecords,
580
+ );
581
+ if (data.kind === 'MissingData') {
582
+ return {
583
+ kind: 'MissingData',
584
+ reason: 'Missing data for ' + field.alias + ' on root ' + root.__link,
585
+ nestedReason: data,
586
+ recordLink: data.recordLink,
587
+ };
588
+ }
589
+ const firstParameter = {
590
+ data: data.data,
591
+ parameters: variables,
592
+ startUpdate: field.readerArtifact.hasUpdatable
593
+ ? getOrCreateCachedStartUpdate(
594
+ environment,
595
+ fragment,
596
+ readerWithRefetchQueries.readerArtifact.fieldName,
597
+ networkRequestOptions,
598
+ )
599
+ : undefined,
600
+ };
601
+ return {
602
+ kind: 'Success',
603
+ data: field.readerArtifact.resolver(firstParameter),
604
+ };
605
+ }
606
+ case 'ComponentReaderArtifact': {
607
+ return {
608
+ kind: 'Success',
609
+ data: getOrCreateCachedComponent(
610
+ environment,
611
+ field.readerArtifact.fieldName,
612
+ fragment,
613
+ networkRequestOptions,
614
+ ),
615
+ };
616
+ }
617
+ default: {
618
+ let _: never = field.readerArtifact;
619
+ _;
620
+ throw new Error('Unexpected kind');
621
+ }
622
+ }
623
+ }
624
+
625
+ export function readScalarFieldData(
626
+ field: ReaderScalarField,
627
+ storeRecord: StoreRecord,
628
+ root: StoreLink,
629
+ variables: Variables,
630
+ ): ReadDataResult<
631
+ string | number | boolean | StoreLink | DataTypeValue[] | null
632
+ > {
633
+ const storeRecordName = getParentRecordKey(field, variables);
634
+ const value = storeRecord[storeRecordName];
635
+ // TODO consider making scalars into discriminated unions. This probably has
636
+ // to happen for when we handle errors.
637
+ if (value === undefined) {
638
+ return {
639
+ kind: 'MissingData',
640
+ reason: 'No value for ' + storeRecordName + ' on root ' + root.__link,
641
+ recordLink: root,
642
+ };
643
+ }
644
+ return { kind: 'Success', data: value };
645
+ }
646
+
647
+ export function readLinkedFieldData(
648
+ environment: IsographEnvironment,
649
+ field: ReaderLinkedField,
650
+ storeRecord: StoreRecord,
651
+ root: StoreLink,
652
+ variables: Variables,
653
+ nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
654
+ networkRequest: PromiseWrapper<void, any>,
655
+ networkRequestOptions: NetworkRequestReaderOptions,
656
+ readData: <TReadFromStore>(
657
+ ast: ReaderAst<TReadFromStore>,
658
+ root: StoreLink,
659
+ ) => ReadDataResult<object>,
660
+ ): ReadDataResult<unknown> {
661
+ const storeRecordName = getParentRecordKey(field, variables);
662
+ const value = storeRecord[storeRecordName];
663
+ if (Array.isArray(value)) {
664
+ const results = [];
665
+ for (const item of value) {
666
+ const link = assertLink(item);
667
+ if (link === undefined) {
668
+ return {
669
+ kind: 'MissingData',
670
+ reason:
671
+ 'No link for ' +
672
+ storeRecordName +
673
+ ' on root ' +
674
+ root.__link +
675
+ '. Link is ' +
676
+ JSON.stringify(item),
677
+ recordLink: root,
678
+ };
679
+ } else if (link === null) {
680
+ results.push(null);
681
+ continue;
682
+ }
683
+
684
+ const result = readData(field.selections, link);
685
+ if (result.kind === 'MissingData') {
686
+ return {
687
+ kind: 'MissingData',
688
+ reason:
689
+ 'Missing data for ' +
690
+ storeRecordName +
691
+ ' on root ' +
692
+ root.__link +
693
+ '. Link is ' +
694
+ JSON.stringify(item),
695
+ nestedReason: result,
696
+ recordLink: result.recordLink,
697
+ };
698
+ }
699
+ results.push(result.data);
700
+ }
701
+ return {
702
+ kind: 'Success',
703
+ data: results,
704
+ };
705
+ }
706
+ let link = assertLink(value);
707
+
708
+ if (field.condition) {
709
+ const data = readData(field.condition.readerAst, root);
710
+ if (data.kind === 'MissingData') {
711
+ return {
712
+ kind: 'MissingData',
713
+ reason:
714
+ 'Missing data for ' + storeRecordName + ' on root ' + root.__link,
715
+ nestedReason: data,
716
+ recordLink: data.recordLink,
717
+ };
718
+ }
719
+
720
+ const readerWithRefetchQueries = {
721
+ kind: 'ReaderWithRefetchQueries',
722
+ readerArtifact: field.condition,
723
+ // TODO this is wrong
724
+ // should map field.condition.usedRefetchQueries
725
+ // but it doesn't exist
726
+ nestedRefetchQueries: [],
727
+ } satisfies ReaderWithRefetchQueries<any, any>;
728
+
729
+ const fragment = {
730
+ kind: 'FragmentReference',
731
+ readerWithRefetchQueries: wrapResolvedValue(readerWithRefetchQueries),
732
+ root,
733
+ variables: generateChildVariableMap(
734
+ variables,
735
+ // TODO this is wrong
736
+ // should use field.arguments
737
+ // but it doesn't exist
738
+ [],
739
+ ),
740
+ networkRequest,
741
+ } satisfies FragmentReference<any, any>;
742
+
743
+ const condition = field.condition.resolver({
744
+ data: data.data,
745
+ parameters: {},
746
+ ...(field.condition.hasUpdatable
747
+ ? {
748
+ startUpdate: getOrCreateCachedStartUpdate(
749
+ environment,
750
+ fragment,
751
+ readerWithRefetchQueries.readerArtifact.fieldName,
752
+ networkRequestOptions,
753
+ ),
754
+ }
755
+ : undefined),
756
+ });
757
+ link = condition;
758
+ }
759
+
760
+ if (link === undefined) {
761
+ // TODO make this configurable, and also generated and derived from the schema
762
+ const missingFieldHandler = environment.missingFieldHandler;
763
+
764
+ const altLink = missingFieldHandler?.(
765
+ storeRecord,
766
+ root,
767
+ field.fieldName,
768
+ field.arguments,
769
+ variables,
770
+ );
771
+ logMessage(environment, () => ({
772
+ kind: 'MissingFieldHandlerCalled',
773
+ root,
774
+ storeRecord,
775
+ fieldName: field.fieldName,
776
+ arguments: field.arguments,
777
+ variables,
778
+ }));
779
+
780
+ if (altLink === undefined) {
781
+ return {
782
+ kind: 'MissingData',
783
+ reason:
784
+ 'No link for ' +
785
+ storeRecordName +
786
+ ' on root ' +
787
+ root.__link +
788
+ '. Link is ' +
789
+ JSON.stringify(value),
790
+ recordLink: root,
791
+ };
792
+ } else {
793
+ link = altLink;
794
+ }
795
+ } else if (link === null) {
796
+ return {
797
+ kind: 'Success',
798
+ data: null,
799
+ };
800
+ }
801
+ const targetId = link;
802
+ const { refetchQueryIndex } = field;
803
+ if (refetchQueryIndex != null) {
804
+ // if field.refetchQueryIndex is not null, then the field is a client pointer, i.e.
805
+ // it is like a loadable field that returns the selections.
806
+ const refetchReaderParams = readData(
807
+ [
808
+ {
809
+ kind: 'Scalar',
810
+ fieldName: 'id',
811
+ alias: null,
812
+ arguments: null,
813
+ isUpdatable: false,
814
+ },
815
+ ],
816
+ targetId,
817
+ );
818
+
819
+ if (refetchReaderParams.kind === 'MissingData') {
820
+ return {
821
+ kind: 'MissingData',
822
+ reason:
823
+ 'Missing data for ' + field.alias + ' on root ' + targetId.__link,
824
+ nestedReason: refetchReaderParams,
825
+ recordLink: refetchReaderParams.recordLink,
826
+ };
827
+ }
828
+
829
+ const refetchQuery = nestedRefetchQueries[refetchQueryIndex];
830
+ if (refetchQuery == null) {
831
+ throw new Error(
832
+ 'refetchQuery is null in RefetchField. This is indicative of a bug in Isograph.',
833
+ );
834
+ }
835
+ const refetchQueryArtifact = refetchQuery.artifact;
836
+ const allowedVariables = refetchQuery.allowedVariables;
837
+
838
+ return {
839
+ kind: 'Success',
840
+ data: (
841
+ args: any,
842
+ // TODO get the associated type for FetchOptions from the loadably selected field
843
+ fetchOptions?: FetchOptions<any>,
844
+ ) => {
845
+ const includeReadOutData = (variables: any, readOutData: any) => {
846
+ variables.id = readOutData.id;
847
+ return variables;
848
+ };
849
+ const localVariables = includeReadOutData(
850
+ args ?? {},
851
+ refetchReaderParams.data,
852
+ );
853
+ writeQueryArgsToVariables(localVariables, field.arguments, variables);
854
+
855
+ return [
856
+ // Stable id
857
+ targetId.__typename +
858
+ ':' +
859
+ targetId.__link +
860
+ '/' +
861
+ field.fieldName +
862
+ '/' +
863
+ stableStringifyArgs(localVariables),
864
+ // Fetcher
865
+ (): ItemCleanupPair<FragmentReference<any, any>> | undefined => {
866
+ const variables = includeReadOutData(
867
+ filterVariables({ ...args, ...localVariables }, allowedVariables),
868
+ refetchReaderParams.data,
869
+ );
870
+
871
+ const readerWithRefetchQueries = wrapResolvedValue({
872
+ kind: 'ReaderWithRefetchQueries',
873
+ readerArtifact: {
874
+ kind: 'EagerReaderArtifact',
875
+ fieldName: field.fieldName,
876
+ readerAst: field.selections,
877
+ resolver: ({ data }: { data: any }) => data,
878
+ hasUpdatable: false,
879
+ },
880
+ nestedRefetchQueries,
881
+ } as const);
882
+
883
+ const [networkRequest, disposeNetworkRequest] =
884
+ maybeMakeNetworkRequest(
885
+ environment,
886
+ refetchQueryArtifact,
887
+ variables,
888
+ readerWithRefetchQueries,
889
+ fetchOptions ?? null,
890
+ );
891
+
892
+ const fragmentReference: FragmentReference<any, any> = {
893
+ kind: 'FragmentReference',
894
+ readerWithRefetchQueries: readerWithRefetchQueries,
895
+ root: targetId,
896
+ variables,
897
+ networkRequest,
898
+ };
899
+ return [fragmentReference, disposeNetworkRequest];
900
+ },
901
+ ];
902
+ },
903
+ };
904
+ }
905
+ const data = readData(field.selections, targetId);
906
+ if (data.kind === 'MissingData') {
907
+ return {
908
+ kind: 'MissingData',
909
+ reason: 'Missing data for ' + storeRecordName + ' on root ' + root.__link,
910
+ nestedReason: data,
911
+ recordLink: data.recordLink,
912
+ };
913
+ }
914
+ return data;
915
+ }
916
+
695
917
  export type NetworkRequestReaderOptions = {
696
918
  suspendIfInFlight: boolean;
697
919
  throwOnNetworkError: boolean;
@@ -720,3 +942,69 @@ function stableStringifyArgs(args: object) {
720
942
  }
721
943
  return s;
722
944
  }
945
+
946
+ export function readImperativelyLoadedField(
947
+ environment: IsographEnvironment,
948
+ field: ReaderImperativelyLoadedField,
949
+ root: StoreLink,
950
+ variables: Variables,
951
+ nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
952
+ networkRequest: PromiseWrapper<void, any>,
953
+ networkRequestOptions: NetworkRequestReaderOptions,
954
+ mutableEncounteredRecords: EncounteredIds,
955
+ ): ReadDataResult<unknown> {
956
+ // First, we read the data using the refetch reader AST (i.e. read out the
957
+ // id field).
958
+ const data = readData(
959
+ environment,
960
+ field.refetchReaderArtifact.readerAst,
961
+ root,
962
+ variables,
963
+ // Refetch fields just read the id, and don't need refetch query artifacts
964
+ [],
965
+ // This is probably indicative of the fact that we are doing redundant checks
966
+ // on the status of this network request...
967
+ networkRequest,
968
+ networkRequestOptions,
969
+ mutableEncounteredRecords,
970
+ );
971
+ if (data.kind === 'MissingData') {
972
+ return {
973
+ kind: 'MissingData',
974
+ reason: 'Missing data for ' + field.alias + ' on root ' + root.__link,
975
+ nestedReason: data,
976
+ recordLink: data.recordLink,
977
+ };
978
+ } else {
979
+ const { refetchQueryIndex } = field;
980
+ const refetchQuery = nestedRefetchQueries[refetchQueryIndex];
981
+ if (refetchQuery == null) {
982
+ throw new Error(
983
+ 'Refetch query not found. This is indicative of a bug in Isograph.',
984
+ );
985
+ }
986
+ const refetchQueryArtifact = refetchQuery.artifact;
987
+ const allowedVariables = refetchQuery.allowedVariables;
988
+
989
+ // Second, we allow the user to call the resolver, which will ultimately
990
+ // use the resolver reader AST to get the resolver parameters.
991
+ return {
992
+ kind: 'Success',
993
+ data: (args: any) => [
994
+ // Stable id
995
+ root.__typename + ':' + root.__link + '__' + field.name,
996
+ // Fetcher
997
+ field.refetchReaderArtifact.resolver(
998
+ environment,
999
+ refetchQueryArtifact,
1000
+ data.data,
1001
+ filterVariables({ ...args, ...variables }, allowedVariables),
1002
+ root,
1003
+ // TODO these params should be removed
1004
+ null,
1005
+ [],
1006
+ ),
1007
+ ],
1008
+ };
1009
+ }
1010
+ }