@isograph/react 0.0.0-main-4ef7c123

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.
package/src/cache.ts ADDED
@@ -0,0 +1,450 @@
1
+ import {
2
+ Factory,
3
+ ItemCleanupPair,
4
+ ParentCache,
5
+ } from "@isograph/react-disposable-state";
6
+ import { PromiseWrapper, wrapPromise } from "./PromiseWrapper";
7
+ import {
8
+ IsographFetchableResolver,
9
+ NormalizationAst,
10
+ NormalizationLinkedField,
11
+ NormalizationScalarField,
12
+ ReaderLinkedField,
13
+ ReaderScalarField,
14
+ RefetchQueryArtifactWrapper,
15
+ } from "./index";
16
+
17
+ const cache: { [index: string]: ParentCache<any> } = {};
18
+
19
+ function getOrCreateCache<T>(
20
+ index: string,
21
+ factory: Factory<T>
22
+ ): ParentCache<T> {
23
+ console.log("getting cache for", {
24
+ index,
25
+ cache: Object.keys(cache),
26
+ found: !!cache[index],
27
+ });
28
+ if (cache[index] == null) {
29
+ cache[index] = new ParentCache(factory);
30
+ }
31
+
32
+ return cache[index];
33
+ }
34
+
35
+ /**
36
+ * Creates a copy of the provided value, ensuring any nested objects have their
37
+ * keys sorted such that equivalent values would have identical JSON.stringify
38
+ * results.
39
+ */
40
+ function stableCopy<T>(value: T): T {
41
+ if (!value || typeof value !== "object") {
42
+ return value;
43
+ }
44
+ if (Array.isArray(value)) {
45
+ // @ts-ignore
46
+ return value.map(stableCopy);
47
+ }
48
+ const keys = Object.keys(value).sort();
49
+ const stable: { [index: string]: any } = {};
50
+ for (let i = 0; i < keys.length; i++) {
51
+ // @ts-ignore
52
+ stable[keys[i]] = stableCopy(value[keys[i]]);
53
+ }
54
+ return stable as any;
55
+ }
56
+
57
+ type IsoResolver = IsographFetchableResolver<any, any, any>;
58
+
59
+ export function getOrCreateCacheForArtifact<T>(
60
+ artifact: IsoResolver,
61
+ variables: object
62
+ ): ParentCache<PromiseWrapper<T>> {
63
+ const cacheKey = artifact.queryText + JSON.stringify(stableCopy(variables));
64
+ const factory: Factory<PromiseWrapper<T>> = () =>
65
+ makeNetworkRequest<T>(artifact, variables);
66
+ return getOrCreateCache<PromiseWrapper<T>>(cacheKey, factory);
67
+ }
68
+
69
+ let network: ((queryText: string, variables: object) => Promise<any>) | null;
70
+
71
+ // This is a hack until we store this in context somehow
72
+ export function setNetwork(newNetwork: typeof network) {
73
+ network = newNetwork;
74
+ }
75
+
76
+ export function makeNetworkRequest<T>(
77
+ artifact: IsoResolver,
78
+ variables: object
79
+ ): ItemCleanupPair<PromiseWrapper<T>> {
80
+ console.log("make network request", artifact, variables);
81
+ if (network == null) {
82
+ throw new Error("Network must be set before makeNetworkRequest is called");
83
+ }
84
+
85
+ const promise = network(artifact.queryText, variables).then(
86
+ (networkResponse) => {
87
+ console.log("network response", artifact);
88
+ normalizeData(
89
+ artifact.normalizationAst,
90
+ networkResponse.data,
91
+ variables,
92
+ artifact.nestedRefetchQueries
93
+ );
94
+ return networkResponse.data;
95
+ }
96
+ );
97
+
98
+ const wrapper = wrapPromise(promise);
99
+
100
+ const response: ItemCleanupPair<PromiseWrapper<T>> = [
101
+ wrapper,
102
+ () => {
103
+ // delete from cache
104
+ },
105
+ ];
106
+ return response;
107
+ }
108
+
109
+ export type Link = {
110
+ __link: DataId;
111
+ };
112
+ export type DataTypeValue =
113
+ // N.B. undefined is here to support optional id's, but
114
+ // undefined should not *actually* be present in the store.
115
+ | undefined
116
+ // Singular scalar fields:
117
+ | number
118
+ | boolean
119
+ | string
120
+ | null
121
+ // Singular linked fields:
122
+ | Link
123
+ // Plural scalar and linked fields:
124
+ | DataTypeValue[];
125
+
126
+ export type StoreRecord = {
127
+ [index: DataId | string]: DataTypeValue;
128
+ // TODO __typename?: T, which is restricted to being a concrete string
129
+ // TODO this shouldn't always be named id
130
+ id?: DataId;
131
+ };
132
+
133
+ export type DataId = string;
134
+
135
+ export const ROOT_ID: DataId & "__ROOT" = "__ROOT";
136
+ export const store: {
137
+ [index: DataId]: StoreRecord | null;
138
+ __ROOT: StoreRecord;
139
+ } = {
140
+ __ROOT: {},
141
+ };
142
+
143
+ type NetworkResponseScalarValue = string | number | boolean;
144
+ type NetworkResponseValue =
145
+ | NetworkResponseScalarValue
146
+ | null
147
+ | NetworkResponseObject
148
+ | NetworkResponseObject[]
149
+ | NetworkResponseScalarValue[];
150
+ type NetworkResponseObject = {
151
+ // N.B. undefined is here to support optional id's, but
152
+ // undefined should not *actually* be present in the network response.
153
+ [index: string]: undefined | NetworkResponseValue;
154
+ id?: DataId;
155
+ };
156
+
157
+ function normalizeData(
158
+ normalizationAst: NormalizationAst,
159
+ networkResponse: NetworkResponseObject,
160
+ variables: Object,
161
+ nestedRefetchQueries: RefetchQueryArtifactWrapper[]
162
+ ) {
163
+ console.log(
164
+ "about to normalize",
165
+ normalizationAst,
166
+ networkResponse,
167
+ variables
168
+ );
169
+ normalizeDataIntoRecord(
170
+ normalizationAst,
171
+ networkResponse,
172
+ store.__ROOT,
173
+ ROOT_ID,
174
+ variables as any,
175
+ nestedRefetchQueries
176
+ );
177
+ console.log("after normalization", { store });
178
+ callSubscriptions();
179
+ }
180
+
181
+ export function subscribe(callback: () => void): () => void {
182
+ subscriptions.add(callback);
183
+ return () => subscriptions.delete(callback);
184
+ }
185
+
186
+ export function onNextChange(): Promise<void> {
187
+ return new Promise((resolve) => {
188
+ const unsubscribe = subscribe(() => {
189
+ unsubscribe();
190
+ resolve();
191
+ });
192
+ });
193
+ }
194
+
195
+ const subscriptions: Set<() => void> = new Set();
196
+
197
+ function callSubscriptions() {
198
+ subscriptions.forEach((callback) => callback());
199
+ }
200
+
201
+ /**
202
+ * Mutate targetParentRecord according to the normalizationAst and networkResponseParentRecord.
203
+ */
204
+ function normalizeDataIntoRecord(
205
+ normalizationAst: NormalizationAst,
206
+ networkResponseParentRecord: NetworkResponseObject,
207
+ targetParentRecord: StoreRecord,
208
+ targetParentRecordId: DataId,
209
+ variables: { [index: string]: string },
210
+ nestedRefetchQueries: RefetchQueryArtifactWrapper[]
211
+ ) {
212
+ for (const normalizationNode of normalizationAst) {
213
+ switch (normalizationNode.kind) {
214
+ case "Scalar": {
215
+ normalizeScalarField(
216
+ normalizationNode,
217
+ networkResponseParentRecord,
218
+ targetParentRecord,
219
+ variables
220
+ );
221
+ break;
222
+ }
223
+ case "Linked": {
224
+ normalizeLinkedField(
225
+ normalizationNode,
226
+ networkResponseParentRecord,
227
+ targetParentRecord,
228
+ targetParentRecordId,
229
+ variables,
230
+ nestedRefetchQueries
231
+ );
232
+ break;
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ function normalizeScalarField(
239
+ astNode: NormalizationScalarField,
240
+ networkResponseParentRecord: NetworkResponseObject,
241
+ targetStoreRecord: StoreRecord,
242
+ variables: { [index: string]: string }
243
+ ) {
244
+ const networkResponseKey = getNetworkResponseKey(astNode);
245
+ const networkResponseData = networkResponseParentRecord[networkResponseKey];
246
+ const parentRecordKey = getParentRecordKey(astNode, variables);
247
+
248
+ if (
249
+ networkResponseData == null ||
250
+ isScalarOrEmptyArray(networkResponseData)
251
+ ) {
252
+ targetStoreRecord[parentRecordKey] = networkResponseData;
253
+ } else {
254
+ throw new Error("Unexpected object array when normalizing scalar");
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Mutate targetParentRecord with a given linked field ast node.
260
+ */
261
+ function normalizeLinkedField(
262
+ astNode: NormalizationLinkedField,
263
+ networkResponseParentRecord: NetworkResponseObject,
264
+ targetParentRecord: StoreRecord,
265
+ targetParentRecordId: DataId,
266
+ variables: { [index: string]: string },
267
+ nestedRefetchQueries: RefetchQueryArtifactWrapper[]
268
+ ) {
269
+ const networkResponseKey = getNetworkResponseKey(astNode);
270
+ const networkResponseData = networkResponseParentRecord[networkResponseKey];
271
+ const parentRecordKey = getParentRecordKey(astNode, variables);
272
+
273
+ if (networkResponseData == null) {
274
+ targetParentRecord[parentRecordKey] = null;
275
+ return;
276
+ }
277
+
278
+ if (isScalarButNotEmptyArray(networkResponseData)) {
279
+ throw new Error(
280
+ "Unexpected scalar network response when normalizing a linked field"
281
+ );
282
+ }
283
+
284
+ if (Array.isArray(networkResponseData)) {
285
+ // TODO check astNode.plural or the like
286
+ const dataIds = [];
287
+ for (let i = 0; i < networkResponseData.length; i++) {
288
+ const networkResponseObject = networkResponseData[i];
289
+ const newStoreRecordId = normalizeNetworkResponseObject(
290
+ astNode,
291
+ networkResponseObject,
292
+ targetParentRecordId,
293
+ variables,
294
+ i,
295
+ nestedRefetchQueries
296
+ );
297
+ dataIds.push({ __link: newStoreRecordId });
298
+ }
299
+ targetParentRecord[parentRecordKey] = dataIds;
300
+ } else {
301
+ const newStoreRecordId = normalizeNetworkResponseObject(
302
+ astNode,
303
+ networkResponseData,
304
+ targetParentRecordId,
305
+ variables,
306
+ null,
307
+ nestedRefetchQueries
308
+ );
309
+ targetParentRecord[parentRecordKey] = {
310
+ __link: newStoreRecordId,
311
+ };
312
+ }
313
+ }
314
+
315
+ function normalizeNetworkResponseObject(
316
+ astNode: NormalizationLinkedField,
317
+ networkResponseData: NetworkResponseObject,
318
+ targetParentRecordId: string,
319
+ variables: { [index: string]: string },
320
+ index: number | null,
321
+ nestedRefetchQueries: RefetchQueryArtifactWrapper[]
322
+ ): DataId /* The id of the modified or newly created item */ {
323
+ const newStoreRecordId = getDataIdOfNetworkResponse(
324
+ targetParentRecordId,
325
+ networkResponseData,
326
+ astNode,
327
+ variables,
328
+ index
329
+ );
330
+
331
+ const newStoreRecord = store[newStoreRecordId] ?? {};
332
+ store[newStoreRecordId] = newStoreRecord;
333
+
334
+ normalizeDataIntoRecord(
335
+ astNode.selections,
336
+ networkResponseData,
337
+ newStoreRecord,
338
+ newStoreRecordId,
339
+ variables,
340
+ nestedRefetchQueries
341
+ );
342
+
343
+ return newStoreRecordId;
344
+ }
345
+
346
+ function isScalarOrEmptyArray(
347
+ data: NonNullable<NetworkResponseValue>
348
+ ): data is NetworkResponseScalarValue | NetworkResponseScalarValue[] {
349
+ // N.B. empty arrays count as empty arrays of scalar fields.
350
+ if (Array.isArray(data)) {
351
+ // This is maybe fixed in a new version of Typescript??
352
+ return (data as any).every((x: any) => isScalarOrEmptyArray(x));
353
+ }
354
+ const isScalarValue =
355
+ typeof data === "string" ||
356
+ typeof data === "number" ||
357
+ typeof data === "boolean";
358
+ return isScalarValue;
359
+ }
360
+
361
+ function isScalarButNotEmptyArray(
362
+ data: NonNullable<NetworkResponseValue>
363
+ ): data is NetworkResponseScalarValue | NetworkResponseScalarValue[] {
364
+ // N.B. empty arrays count as empty arrays of linked fields.
365
+ if (Array.isArray(data)) {
366
+ if (data.length === 0) {
367
+ return false;
368
+ }
369
+ // This is maybe fixed in a new version of Typescript??
370
+ return (data as any).every((x: any) => isScalarOrEmptyArray(x));
371
+ }
372
+ const isScalarValue =
373
+ typeof data === "string" ||
374
+ typeof data === "number" ||
375
+ typeof data === "boolean";
376
+ return isScalarValue;
377
+ }
378
+
379
+ export function getParentRecordKey(
380
+ astNode:
381
+ | NormalizationLinkedField
382
+ | NormalizationScalarField
383
+ | ReaderLinkedField
384
+ | ReaderScalarField,
385
+ variables: { [index: string]: string }
386
+ ): string {
387
+ let parentRecordKey = astNode.fieldName;
388
+ const fieldParameters = astNode.arguments;
389
+ if (fieldParameters != null) {
390
+ for (const fieldParameter of fieldParameters) {
391
+ const { argumentName, variableName } = fieldParameter;
392
+ const valueToUse = variables[variableName];
393
+ parentRecordKey += `${FIRST_SPLIT_KEY}${argumentName}${SECOND_SPLIT_KEY}${valueToUse}`;
394
+ }
395
+ }
396
+
397
+ return parentRecordKey;
398
+ }
399
+
400
+ function getNetworkResponseKey(
401
+ astNode: NormalizationLinkedField | NormalizationScalarField
402
+ ): string {
403
+ let networkResponseKey = astNode.fieldName;
404
+ const fieldParameters = astNode.arguments;
405
+ if (fieldParameters != null) {
406
+ for (const fieldParameter of fieldParameters) {
407
+ const { argumentName, variableName } = fieldParameter;
408
+ networkResponseKey += `${FIRST_SPLIT_KEY}${argumentName}${SECOND_SPLIT_KEY}${variableName}`;
409
+ }
410
+ }
411
+ return networkResponseKey;
412
+ }
413
+
414
+ // an alias might be pullRequests____first___first____after___cursor
415
+ export const FIRST_SPLIT_KEY = "____";
416
+ export const SECOND_SPLIT_KEY = "___";
417
+
418
+ // Returns a key to look up an item in the store
419
+ function getDataIdOfNetworkResponse(
420
+ parentRecordId: DataId,
421
+ dataToNormalize: NetworkResponseObject,
422
+ astNode: NormalizationLinkedField | NormalizationScalarField,
423
+ variables: { [index: string]: string },
424
+ index: number | null
425
+ ): DataId {
426
+ // Check whether the dataToNormalize has an id field. If so, that is the key.
427
+ // If not, we construct an id from the parentRecordId and the field parameters.
428
+
429
+ const dataId = dataToNormalize.id;
430
+ if (dataId != null) {
431
+ return dataId;
432
+ }
433
+
434
+ let storeKey = `${parentRecordId}.${astNode.fieldName}`;
435
+ if (index != null) {
436
+ storeKey += `.${index}`;
437
+ }
438
+
439
+ const fieldParameters = astNode.arguments;
440
+ if (fieldParameters == null) {
441
+ return storeKey;
442
+ }
443
+
444
+ for (const fieldParameter of fieldParameters) {
445
+ const { argumentName, variableName } = fieldParameter;
446
+ const valueToUse = variables[variableName];
447
+ storeKey += `${FIRST_SPLIT_KEY}${argumentName}${SECOND_SPLIT_KEY}${valueToUse}`;
448
+ }
449
+ return storeKey;
450
+ }