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