@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/dist/PromiseWrapper.d.ts +13 -0
- package/dist/PromiseWrapper.js +22 -0
- package/dist/cache.d.ts +30 -0
- package/dist/cache.js +241 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +333 -0
- package/package.json +28 -0
- package/src/PromiseWrapper.ts +29 -0
- package/src/cache.ts +450 -0
- package/src/index.tsx +647 -0
- package/tsconfig.pkg.json +11 -0
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
|
+
}
|