@khanacademy/wonder-blocks-data 2.3.3 → 3.1.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.
- package/CHANGELOG.md +21 -0
- package/dist/es/index.js +365 -429
- package/dist/index.js +455 -461
- package/docs.md +19 -13
- package/package.json +6 -6
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +40 -160
- package/src/__tests__/generated-snapshot.test.js +15 -195
- package/src/components/__tests__/data.test.js +159 -965
- package/src/components/__tests__/gql-router.test.js +64 -0
- package/src/components/__tests__/intercept-data.test.js +9 -66
- package/src/components/__tests__/track-data.test.js +6 -5
- package/src/components/data.js +9 -119
- package/src/components/data.md +38 -60
- package/src/components/gql-router.js +66 -0
- package/src/components/intercept-context.js +2 -3
- package/src/components/intercept-data.js +2 -34
- package/src/components/intercept-data.md +7 -105
- package/src/hooks/__tests__/use-data.test.js +826 -0
- package/src/hooks/__tests__/use-gql.test.js +233 -0
- package/src/hooks/use-data.js +143 -0
- package/src/hooks/use-gql.js +75 -0
- package/src/index.js +7 -9
- package/src/util/__tests__/get-gql-data-from-response.test.js +187 -0
- package/src/util/__tests__/memory-cache.test.js +134 -35
- package/src/util/__tests__/request-fulfillment.test.js +21 -36
- package/src/util/__tests__/request-handler.test.js +30 -30
- package/src/util/__tests__/request-tracking.test.js +29 -30
- package/src/util/__tests__/response-cache.test.js +521 -561
- package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
- package/src/util/get-gql-data-from-response.js +69 -0
- package/src/util/gql-error.js +36 -0
- package/src/util/gql-router-context.js +6 -0
- package/src/util/gql-types.js +60 -0
- package/src/util/memory-cache.js +20 -15
- package/src/util/request-fulfillment.js +4 -0
- package/src/util/request-handler.js +4 -28
- package/src/util/request-handler.md +0 -32
- package/src/util/request-tracking.js +2 -3
- package/src/util/response-cache.js +50 -110
- package/src/util/result-from-cache-entry.js +38 -0
- package/src/util/types.js +14 -35
- package/LICENSE +0 -21
- package/src/components/__tests__/intercept-cache.test.js +0 -124
- package/src/components/__tests__/internal-data.test.js +0 -1030
- package/src/components/intercept-cache.js +0 -79
- package/src/components/intercept-cache.md +0 -103
- package/src/components/internal-data.js +0 -219
- package/src/util/__tests__/no-cache.test.js +0 -112
- package/src/util/no-cache.js +0 -66
- package/src/util/no-cache.md +0 -66
package/dist/es/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Server } from '@khanacademy/wonder-blocks-core';
|
|
2
|
-
import
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useState, useContext, useRef, useEffect, useMemo } from 'react';
|
|
3
4
|
import _extends from '@babel/runtime/helpers/extends';
|
|
5
|
+
import { Errors, KindError } from '@khanacademy/wonder-stuff-core';
|
|
4
6
|
|
|
5
7
|
function deepClone(source) {
|
|
6
8
|
/**
|
|
@@ -19,9 +21,7 @@ function deepClone(source) {
|
|
|
19
21
|
*
|
|
20
22
|
* Special case cache implementation for the memory cache.
|
|
21
23
|
*
|
|
22
|
-
* This is only used within our framework
|
|
23
|
-
* provide this as a custom cache as the framework will default to this in the
|
|
24
|
-
* absence of a custom cache. We use this for SSR too (see ./response-cache.js).
|
|
24
|
+
* This is only used within our framework for SSR (see ./response-cache.js).
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ class MemoryCache {
|
|
|
29
29
|
constructor(source = null) {
|
|
30
30
|
this.store = (handler, options, entry) => {
|
|
31
31
|
const requestType = handler.type;
|
|
32
|
-
const frozenEntry = Object.
|
|
32
|
+
const frozenEntry = Object.freeze(entry); // Ensure we have a cache location for this handler type.
|
|
33
33
|
|
|
34
34
|
this._cache[requestType] = this._cache[requestType] || {}; // Cache the data.
|
|
35
35
|
|
|
@@ -89,16 +89,22 @@ class MemoryCache {
|
|
|
89
89
|
|
|
90
90
|
if (!handlerCache) {
|
|
91
91
|
return 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
92
|
+
}
|
|
94
93
|
|
|
95
94
|
let removedCount = 0;
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
if (typeof predicate === "function") {
|
|
97
|
+
// Apply the predicate to what we have cached.
|
|
98
|
+
for (const [key, entry] of Object.entries(handlerCache)) {
|
|
99
|
+
if (predicate(key, entry)) {
|
|
100
|
+
removedCount++;
|
|
101
|
+
delete handlerCache[key];
|
|
102
|
+
}
|
|
101
103
|
}
|
|
104
|
+
} else {
|
|
105
|
+
// We're removing everything so delete the entire subcache.
|
|
106
|
+
removedCount = Object.keys(handlerCache).length;
|
|
107
|
+
delete this._cache[requestType];
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
return removedCount;
|
|
@@ -128,6 +134,12 @@ class MemoryCache {
|
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Indicate if this cache is being used or now.
|
|
139
|
+
*
|
|
140
|
+
* When the cache has entries, returns `true`; otherwise, returns `false`.
|
|
141
|
+
*/
|
|
142
|
+
|
|
131
143
|
|
|
132
144
|
get inUse() {
|
|
133
145
|
return Object.keys(this._cache).length > 0;
|
|
@@ -135,47 +147,11 @@ class MemoryCache {
|
|
|
135
147
|
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
let defaultInstance = null;
|
|
139
|
-
/**
|
|
140
|
-
* This is a cache implementation to use when no caching is wanted.
|
|
141
|
-
*
|
|
142
|
-
* Use this with your request handler if you want to support server-side
|
|
143
|
-
* rendering of your data requests, but want to ensure data is never cached
|
|
144
|
-
* on the client-side.
|
|
145
|
-
*
|
|
146
|
-
* This is better than having `shouldRefreshCache` always return `true` in the
|
|
147
|
-
* handler as this ensures that cache space and memory are never used for the
|
|
148
|
-
* requested data after hydration has finished.
|
|
149
|
-
*/
|
|
150
|
-
|
|
151
|
-
class NoCache {
|
|
152
|
-
constructor() {
|
|
153
|
-
this.store = (handler, options, entry) => {
|
|
154
|
-
/* empty */
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
this.retrieve = (handler, options) => null;
|
|
158
|
-
|
|
159
|
-
this.remove = (handler, options) => false;
|
|
160
|
-
|
|
161
|
-
this.removeAll = (handler, predicate) => 0;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
static get Default() {
|
|
165
|
-
if (defaultInstance == null) {
|
|
166
|
-
defaultInstance = new NoCache();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return defaultInstance;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
150
|
/**
|
|
175
151
|
* The default instance is stored here.
|
|
176
152
|
* It's created below in the Default() static property.
|
|
177
153
|
*/
|
|
178
|
-
let _default;
|
|
154
|
+
let _default$2;
|
|
179
155
|
/**
|
|
180
156
|
* Implements the response cache.
|
|
181
157
|
*
|
|
@@ -185,31 +161,29 @@ let _default;
|
|
|
185
161
|
|
|
186
162
|
class ResponseCache {
|
|
187
163
|
static get Default() {
|
|
188
|
-
if (!_default) {
|
|
189
|
-
_default = new ResponseCache();
|
|
164
|
+
if (!_default$2) {
|
|
165
|
+
_default$2 = new ResponseCache();
|
|
190
166
|
}
|
|
191
167
|
|
|
192
|
-
return _default;
|
|
168
|
+
return _default$2;
|
|
193
169
|
}
|
|
194
170
|
|
|
195
|
-
constructor(
|
|
171
|
+
constructor(hydrationCache = null, ssrOnlyCache = null) {
|
|
196
172
|
this.initialize = source => {
|
|
197
|
-
if (this.
|
|
173
|
+
if (this._hydrationCache.inUse) {
|
|
198
174
|
throw new Error("Cannot initialize data response cache more than once");
|
|
199
175
|
}
|
|
200
176
|
|
|
201
177
|
try {
|
|
202
|
-
this.
|
|
178
|
+
this._hydrationCache = new MemoryCache(source);
|
|
203
179
|
} catch (e) {
|
|
204
180
|
throw new Error(`An error occurred trying to initialize the data response cache: ${e}`);
|
|
205
181
|
}
|
|
206
182
|
};
|
|
207
183
|
|
|
208
|
-
this.cacheData = (handler, options, data) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
};
|
|
184
|
+
this.cacheData = (handler, options, data) => this._setCacheEntry(handler, options, {
|
|
185
|
+
data
|
|
186
|
+
});
|
|
213
187
|
|
|
214
188
|
this.cacheError = (handler, options, error) => {
|
|
215
189
|
const errorMessage = typeof error === "string" ? error : error.message;
|
|
@@ -219,110 +193,64 @@ class ResponseCache {
|
|
|
219
193
|
};
|
|
220
194
|
|
|
221
195
|
this.getEntry = (handler, options) => {
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// This allows us to use our hydrated cache during hydration.
|
|
233
|
-
// If we just returned null when the custom cache didn't have it,
|
|
234
|
-
// we would never hydrate properly.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const internalEntry = this._defaultCache(handler).retrieve(handler, options); // If we are not server-side and we hydrated something that the custom
|
|
238
|
-
// cache didn't have, we need to make sure the custom cache contains
|
|
239
|
-
// that value.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (this._ssrOnlyCache == null && handler.cache != null && internalEntry != null) {
|
|
243
|
-
// Yes, if this throws, we will have a problem. We want that.
|
|
244
|
-
// Bad cache implementations should be overt.
|
|
245
|
-
handler.cache.store(handler, options, internalEntry); // We now delete this from our in-memory cache as we don't need it.
|
|
196
|
+
// Get the cached entry for this value.
|
|
197
|
+
// If the handler wants WB Data to hydrate (i.e. handler.hydrate is
|
|
198
|
+
// true), we use our hydration cache. Otherwise, if we're server-side
|
|
199
|
+
// we use our SSR-only cache. Otherwise, there's no entry to return.
|
|
200
|
+
const cache = handler.hydrate ? this._hydrationCache : Server.isServerSide() ? this._ssrOnlyCache : undefined;
|
|
201
|
+
const internalEntry = cache == null ? void 0 : cache.retrieve(handler, options); // If we are not server-side and we hydrated something, let's clear
|
|
202
|
+
// that from the hydration cache to save memory.
|
|
203
|
+
|
|
204
|
+
if (this._ssrOnlyCache == null && internalEntry != null) {
|
|
205
|
+
// We now delete this from our hydration cache as we don't need it.
|
|
246
206
|
// This does mean that if another handler of the same type but
|
|
247
|
-
// without
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
this.
|
|
207
|
+
// without some sort of linked cache won't get the value, but
|
|
208
|
+
// that's not an expected use-case. If two different places use the
|
|
209
|
+
// same handler and options (i.e. the same request), then the
|
|
210
|
+
// handler should cater to that to ensure they share the result.
|
|
211
|
+
this._hydrationCache.remove(handler, options);
|
|
252
212
|
}
|
|
253
213
|
|
|
254
214
|
return internalEntry;
|
|
255
215
|
};
|
|
256
216
|
|
|
257
217
|
this.remove = (handler, options) => {
|
|
218
|
+
var _this$_ssrOnlyCache$r, _this$_ssrOnlyCache;
|
|
219
|
+
|
|
258
220
|
// NOTE(somewhatabstract): We could invoke removeAll with a predicate
|
|
259
221
|
// to match the key of the entry we're removing, but that's an
|
|
260
222
|
// inefficient way to remove a single item, so let's not do that.
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
const customCache = this._ssrOnlyCache == null ? handler.cache : null;
|
|
264
|
-
const removedCustom = !!(customCache != null && customCache.remove(handler, options)); // Delete the entry from our internal cache.
|
|
265
|
-
// Even if we have a custom cache, we want to make sure we still
|
|
266
|
-
// removed the same value from internal cache since this could be
|
|
267
|
-
// getting called before hydration for some complex advanced usage
|
|
268
|
-
// reason.
|
|
269
|
-
|
|
270
|
-
return this._defaultCache(handler).remove(handler, options) || removedCustom;
|
|
223
|
+
// Delete the entry from the appropriate cache.
|
|
224
|
+
return handler.hydrate ? this._hydrationCache.remove(handler, options) : (_this$_ssrOnlyCache$r = (_this$_ssrOnlyCache = this._ssrOnlyCache) == null ? void 0 : _this$_ssrOnlyCache.remove(handler, options)) != null ? _this$_ssrOnlyCache$r : false;
|
|
271
225
|
};
|
|
272
226
|
|
|
273
227
|
this.removeAll = (handler, predicate) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// Even if we have a custom cache, we want to make sure we still
|
|
279
|
-
// removed the same value from internal cache since this could be
|
|
280
|
-
// getting called before hydration for some complex advanced usage
|
|
281
|
-
// reason.
|
|
282
|
-
|
|
283
|
-
const removedCount = this._defaultCache(handler).removeAll(handler, predicate); // We have no idea which keys were removed from which caches,
|
|
284
|
-
// so we can't dedupe the remove counts based on keys.
|
|
285
|
-
// That's why we return the total records deleted rather than the
|
|
286
|
-
// total keys deleted.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return removedCount + removedCountCustom;
|
|
228
|
+
var _this$_ssrOnlyCache$r2, _this$_ssrOnlyCache2;
|
|
229
|
+
|
|
230
|
+
// Apply the predicate to what we have in the appropriate cache.
|
|
231
|
+
return handler.hydrate ? this._hydrationCache.removeAll(handler, predicate) : (_this$_ssrOnlyCache$r2 = (_this$_ssrOnlyCache2 = this._ssrOnlyCache) == null ? void 0 : _this$_ssrOnlyCache2.removeAll(handler, predicate)) != null ? _this$_ssrOnlyCache$r2 : 0;
|
|
290
232
|
};
|
|
291
233
|
|
|
292
234
|
this.cloneHydratableData = () => {
|
|
293
235
|
// We return our hydration cache only.
|
|
294
|
-
return this.
|
|
236
|
+
return this._hydrationCache.cloneData();
|
|
295
237
|
};
|
|
296
238
|
|
|
297
239
|
this._ssrOnlyCache = Server.isServerSide() ? ssrOnlyCache || new MemoryCache() : undefined;
|
|
298
|
-
this.
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Returns the default cache to use for the given handler.
|
|
302
|
-
*/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
_defaultCache(handler) {
|
|
306
|
-
if (handler.hydrate) {
|
|
307
|
-
return this._hydrationAndDefaultCache;
|
|
308
|
-
} // If the handler doesn't want to hydrate, we return the SSR-only cache.
|
|
309
|
-
// If we are client-side, we return our non-caching implementation.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return this._ssrOnlyCache || NoCache.Default;
|
|
240
|
+
this._hydrationCache = hydrationCache || new MemoryCache();
|
|
313
241
|
}
|
|
314
242
|
|
|
315
243
|
_setCacheEntry(handler, options, entry) {
|
|
316
244
|
const frozenEntry = Object.freeze(entry);
|
|
317
245
|
|
|
318
|
-
if (this._ssrOnlyCache
|
|
319
|
-
// We are
|
|
320
|
-
//
|
|
321
|
-
handler.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
246
|
+
if (this._ssrOnlyCache != null) {
|
|
247
|
+
// We are server-side.
|
|
248
|
+
// We need to store this value.
|
|
249
|
+
if (handler.hydrate) {
|
|
250
|
+
this._hydrationCache.store(handler, options, frozenEntry);
|
|
251
|
+
} else {
|
|
252
|
+
this._ssrOnlyCache.store(handler, options, frozenEntry);
|
|
253
|
+
}
|
|
326
254
|
}
|
|
327
255
|
|
|
328
256
|
return frozenEntry;
|
|
@@ -386,6 +314,8 @@ class RequestFulfillment {
|
|
|
386
314
|
delete handlerRequests[key];
|
|
387
315
|
/**
|
|
388
316
|
* Let's cache the data!
|
|
317
|
+
*
|
|
318
|
+
* NOTE: This only caches when we're server side.
|
|
389
319
|
*/
|
|
390
320
|
|
|
391
321
|
return cacheData(handler, options, data);
|
|
@@ -393,6 +323,8 @@ class RequestFulfillment {
|
|
|
393
323
|
delete handlerRequests[key];
|
|
394
324
|
/**
|
|
395
325
|
* Let's cache the error!
|
|
326
|
+
*
|
|
327
|
+
* NOTE: This only caches when we're server side.
|
|
396
328
|
*/
|
|
397
329
|
|
|
398
330
|
return cacheError(handler, options, error);
|
|
@@ -418,13 +350,13 @@ class RequestFulfillment {
|
|
|
418
350
|
*
|
|
419
351
|
* INTERNAL USE ONLY
|
|
420
352
|
*/
|
|
421
|
-
const TrackerContext = new createContext(null);
|
|
353
|
+
const TrackerContext = new React.createContext(null);
|
|
422
354
|
/**
|
|
423
355
|
* The default instance is stored here.
|
|
424
356
|
* It's created below in the Default() static property.
|
|
425
357
|
*/
|
|
426
358
|
|
|
427
|
-
let _default
|
|
359
|
+
let _default;
|
|
428
360
|
/**
|
|
429
361
|
* Implements request tracking and fulfillment.
|
|
430
362
|
*
|
|
@@ -434,11 +366,11 @@ let _default$2;
|
|
|
434
366
|
|
|
435
367
|
class RequestTracker {
|
|
436
368
|
static get Default() {
|
|
437
|
-
if (!_default
|
|
438
|
-
_default
|
|
369
|
+
if (!_default) {
|
|
370
|
+
_default = new RequestTracker();
|
|
439
371
|
}
|
|
440
372
|
|
|
441
|
-
return _default
|
|
373
|
+
return _default;
|
|
442
374
|
}
|
|
443
375
|
/**
|
|
444
376
|
* These are the caches for tracked requests, their handlers, and responses.
|
|
@@ -557,9 +489,8 @@ class RequestTracker {
|
|
|
557
489
|
* use with the Wonder Blocks Data framework.
|
|
558
490
|
*/
|
|
559
491
|
class RequestHandler {
|
|
560
|
-
constructor(type,
|
|
492
|
+
constructor(type, hydrate = true) {
|
|
561
493
|
this._type = type;
|
|
562
|
-
this._cache = cache || null;
|
|
563
494
|
this._hydrate = !!hydrate;
|
|
564
495
|
}
|
|
565
496
|
|
|
@@ -567,26 +498,10 @@ class RequestHandler {
|
|
|
567
498
|
return this._type;
|
|
568
499
|
}
|
|
569
500
|
|
|
570
|
-
get cache() {
|
|
571
|
-
return this._cache;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
501
|
get hydrate() {
|
|
575
502
|
return this._hydrate;
|
|
576
503
|
}
|
|
577
504
|
|
|
578
|
-
shouldRefreshCache(options, cachedEntry) {
|
|
579
|
-
/**
|
|
580
|
-
* By default, the cache needs a refresh if the current entry is an
|
|
581
|
-
* error.
|
|
582
|
-
*
|
|
583
|
-
* This means that an error will cause a re-request on render.
|
|
584
|
-
* Useful if the server rendered an error, as it means the client
|
|
585
|
-
* will update after rehydration.
|
|
586
|
-
*/
|
|
587
|
-
return cachedEntry == null || cachedEntry.error != null;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
505
|
getKey(options) {
|
|
591
506
|
try {
|
|
592
507
|
return options === undefined ? "undefined" : JSON.stringify(options);
|
|
@@ -604,13 +519,13 @@ class RequestHandler {
|
|
|
604
519
|
/**
|
|
605
520
|
* Component to enable data request tracking when server-side rendering.
|
|
606
521
|
*/
|
|
607
|
-
class TrackData extends Component {
|
|
522
|
+
class TrackData extends React.Component {
|
|
608
523
|
render() {
|
|
609
524
|
if (!Server.isServerSide()) {
|
|
610
525
|
throw new Error("This component is not for use during client-side rendering");
|
|
611
526
|
}
|
|
612
527
|
|
|
613
|
-
return /*#__PURE__*/createElement(TrackerContext.Provider, {
|
|
528
|
+
return /*#__PURE__*/React.createElement(TrackerContext.Provider, {
|
|
614
529
|
value: RequestTracker.Default.trackDataRequest
|
|
615
530
|
}, this.props.children);
|
|
616
531
|
}
|
|
@@ -618,286 +533,180 @@ class TrackData extends Component {
|
|
|
618
533
|
}
|
|
619
534
|
|
|
620
535
|
/**
|
|
621
|
-
*
|
|
622
|
-
* It is wrapped by Data in order to support intercepts and be exported for use.
|
|
536
|
+
* InterceptContext defines a map from handler type to interception methods.
|
|
623
537
|
*
|
|
624
538
|
* INTERNAL USE ONLY
|
|
625
539
|
*/
|
|
626
|
-
|
|
627
|
-
constructor(props) {
|
|
628
|
-
super(props);
|
|
629
|
-
this.state = this._buildStateAndfulfillNeeds(props);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
componentDidMount() {
|
|
633
|
-
this._mounted = true;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
shouldComponentUpdate(nextProps, nextState) {
|
|
637
|
-
/**
|
|
638
|
-
* We only bother updating if our state changed.
|
|
639
|
-
*
|
|
640
|
-
* And we only update the state if props changed
|
|
641
|
-
* or we got new data/error.
|
|
642
|
-
*/
|
|
643
|
-
if (!this._propsMatch(nextProps)) {
|
|
644
|
-
const newState = this._buildStateAndfulfillNeeds(nextProps);
|
|
645
|
-
|
|
646
|
-
this.setState(newState);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return this.state.loading !== nextState.loading || this.state.data !== nextState.data || this.state.error !== nextState.error;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
componentWillUnmount() {
|
|
653
|
-
this._mounted = false;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
_propsMatch(otherProps) {
|
|
657
|
-
const {
|
|
658
|
-
handler,
|
|
659
|
-
options
|
|
660
|
-
} = this.props;
|
|
661
|
-
const {
|
|
662
|
-
handler: prevHandler,
|
|
663
|
-
options: prevOptions
|
|
664
|
-
} = otherProps;
|
|
665
|
-
return handler === prevHandler && handler.getKey(options) === prevHandler.getKey(prevOptions);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
_buildStateAndfulfillNeeds(propsAtFulfillment) {
|
|
669
|
-
const {
|
|
670
|
-
getEntry,
|
|
671
|
-
handler,
|
|
672
|
-
options
|
|
673
|
-
} = propsAtFulfillment;
|
|
674
|
-
const cachedData = getEntry(handler, options);
|
|
675
|
-
|
|
676
|
-
if (!Server.isServerSide() && (cachedData == null || handler.shouldRefreshCache(options, cachedData))) {
|
|
677
|
-
/**
|
|
678
|
-
* We're not on the server, the cache missed, or our handler says
|
|
679
|
-
* we should refresh the cache.
|
|
680
|
-
*
|
|
681
|
-
* Therefore, we need to request data.
|
|
682
|
-
*
|
|
683
|
-
* We have to do this here from the constructor so that this
|
|
684
|
-
* data request is tracked when performing server-side rendering.
|
|
685
|
-
*/
|
|
686
|
-
RequestFulfillment.Default.fulfill(handler, options).then(cacheEntry => {
|
|
687
|
-
/**
|
|
688
|
-
* We get here, we should have updated the cache.
|
|
689
|
-
* However, we need to update the component, but we
|
|
690
|
-
* should only do that if the props are the same as they
|
|
691
|
-
* were when this was called.
|
|
692
|
-
*/
|
|
693
|
-
if (this._mounted && this._propsMatch(propsAtFulfillment)) {
|
|
694
|
-
this.setState({
|
|
695
|
-
loading: false,
|
|
696
|
-
data: cacheEntry.data,
|
|
697
|
-
error: cacheEntry.error
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
return null;
|
|
702
|
-
}).catch(e => {
|
|
703
|
-
/**
|
|
704
|
-
* We should never get here, but if we do.
|
|
705
|
-
*/
|
|
706
|
-
// eslint-disable-next-line no-console
|
|
707
|
-
console.error(`Unexpected error occurred during data fulfillment: ${e}`);
|
|
708
|
-
|
|
709
|
-
if (this._mounted && this._propsMatch(propsAtFulfillment)) {
|
|
710
|
-
this.setState({
|
|
711
|
-
loading: false,
|
|
712
|
-
data: null,
|
|
713
|
-
error: typeof e === "string" ? e : e.message
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return null;
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* This is the default response for the server and for the initial
|
|
722
|
-
* client-side render if we have cachedData.
|
|
723
|
-
*
|
|
724
|
-
* This ensures we don't make promises we don't want when doing
|
|
725
|
-
* server-side rendering. Instead, we either have data from the cache
|
|
726
|
-
* or we don't.
|
|
727
|
-
*/
|
|
728
|
-
|
|
540
|
+
const InterceptContext = /*#__PURE__*/React.createContext({});
|
|
729
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Turns a cache entry into a stateful result.
|
|
544
|
+
*/
|
|
545
|
+
const resultFromCacheEntry = cacheEntry => {
|
|
546
|
+
// No cache entry means we didn't load one yet.
|
|
547
|
+
if (cacheEntry == null) {
|
|
730
548
|
return {
|
|
731
|
-
|
|
732
|
-
data: cachedData && cachedData.data,
|
|
733
|
-
error: cachedData && cachedData.error
|
|
549
|
+
status: "loading"
|
|
734
550
|
};
|
|
735
551
|
}
|
|
736
552
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
error
|
|
742
|
-
} = this.state;
|
|
743
|
-
|
|
744
|
-
if (loading) {
|
|
745
|
-
return {
|
|
746
|
-
loading: true
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (data != null) {
|
|
751
|
-
return {
|
|
752
|
-
loading: false,
|
|
753
|
-
data
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (error == null) {
|
|
758
|
-
// We should never get here ever.
|
|
759
|
-
throw new Error("Loaded result has invalid state where data and error are missing");
|
|
760
|
-
}
|
|
553
|
+
const {
|
|
554
|
+
data,
|
|
555
|
+
error
|
|
556
|
+
} = cacheEntry;
|
|
761
557
|
|
|
558
|
+
if (data != null) {
|
|
762
559
|
return {
|
|
763
|
-
|
|
764
|
-
|
|
560
|
+
status: "success",
|
|
561
|
+
data
|
|
765
562
|
};
|
|
766
563
|
}
|
|
767
564
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
_renderWithTrackingContext(result) {
|
|
776
|
-
return /*#__PURE__*/createElement(TrackerContext.Consumer, null, track => {
|
|
777
|
-
/**
|
|
778
|
-
* If data tracking wasn't enabled, don't do it.
|
|
779
|
-
*/
|
|
780
|
-
if (track != null) {
|
|
781
|
-
track(this.props.handler, this.props.options);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
return this._renderContent(result);
|
|
785
|
-
});
|
|
565
|
+
if (error == null) {
|
|
566
|
+
// We should never get here ever.
|
|
567
|
+
return {
|
|
568
|
+
status: "error",
|
|
569
|
+
error: "Loaded result has invalid state where data and error are missing"
|
|
570
|
+
};
|
|
786
571
|
}
|
|
787
572
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
573
|
+
return {
|
|
574
|
+
status: "error",
|
|
575
|
+
error
|
|
576
|
+
};
|
|
577
|
+
};
|
|
793
578
|
|
|
794
|
-
|
|
795
|
-
|
|
579
|
+
const useData = (handler, options) => {
|
|
580
|
+
// If we're server-side or hydrating, we'll have a cached entry to use.
|
|
581
|
+
// So we get that and use it to initialize our state.
|
|
582
|
+
// This works in both hydration and SSR because the very first call to
|
|
583
|
+
// this will have cached data in those cases as it will be present on the
|
|
584
|
+
// initial render - and subsequent renders on the client it will be null.
|
|
585
|
+
const cachedResult = ResponseCache.Default.getEntry(handler, options);
|
|
586
|
+
const [result, setResult] = useState(cachedResult); // Lookup to see if there's an interceptor for the handler.
|
|
587
|
+
// If we have one, we need to replace the handler with one that
|
|
588
|
+
// uses the interceptor.
|
|
589
|
+
|
|
590
|
+
const interceptorMap = useContext(InterceptContext);
|
|
591
|
+
const interceptor = interceptorMap[handler.type]; // If we have an interceptor, we need to replace the handler with one that
|
|
592
|
+
// uses the interceptor. This helper function generates a new handler.
|
|
593
|
+
// We need this before we track the request as we want the interceptor
|
|
594
|
+
// to also work for tracked requests to simplify testing the server-side
|
|
595
|
+
// request fulfillment.
|
|
596
|
+
|
|
597
|
+
const getMaybeInterceptedHandler = () => {
|
|
598
|
+
if (interceptor == null) {
|
|
599
|
+
return handler;
|
|
796
600
|
}
|
|
797
601
|
|
|
798
|
-
|
|
799
|
-
|
|
602
|
+
const fulfillRequestFn = options => {
|
|
603
|
+
var _interceptor$fulfillR;
|
|
800
604
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
/**
|
|
804
|
-
* InterceptContext defines a map from handler type to interception methods.
|
|
805
|
-
*
|
|
806
|
-
* INTERNAL USE ONLY
|
|
807
|
-
*/
|
|
808
|
-
const InterceptContext = /*#__PURE__*/createContext({});
|
|
809
|
-
|
|
810
|
-
/**
|
|
811
|
-
* This component is the main component of Wonder Blocks Data. With this, data
|
|
812
|
-
* requirements can be placed in a React application in a manner that will
|
|
813
|
-
* support server-side rendering and efficient caching.
|
|
814
|
-
*/
|
|
815
|
-
class Data extends Component {
|
|
816
|
-
_getHandlerFromInterceptor(interceptor) {
|
|
817
|
-
const {
|
|
818
|
-
handler
|
|
819
|
-
} = this.props;
|
|
820
|
-
|
|
821
|
-
if (!interceptor) {
|
|
822
|
-
return handler;
|
|
823
|
-
}
|
|
605
|
+
return (_interceptor$fulfillR = interceptor.fulfillRequest(options)) != null ? _interceptor$fulfillR : handler.fulfillRequest(options);
|
|
606
|
+
};
|
|
824
607
|
|
|
825
|
-
const {
|
|
826
|
-
fulfillRequest,
|
|
827
|
-
shouldRefreshCache
|
|
828
|
-
} = interceptor;
|
|
829
|
-
const fulfillRequestFn = fulfillRequest ? options => {
|
|
830
|
-
const interceptedResult = fulfillRequest(options);
|
|
831
|
-
return interceptedResult != null ? interceptedResult : handler.fulfillRequest(options);
|
|
832
|
-
} : options => handler.fulfillRequest(options);
|
|
833
|
-
const shouldRefreshCacheFn = shouldRefreshCache ? (options, cacheEntry) => {
|
|
834
|
-
const interceptedResult = shouldRefreshCache(options, cacheEntry);
|
|
835
|
-
return interceptedResult != null ? interceptedResult : handler.shouldRefreshCache(options, cacheEntry);
|
|
836
|
-
} : (options, cacheEntry) => handler.shouldRefreshCache(options, cacheEntry);
|
|
837
608
|
return {
|
|
838
609
|
fulfillRequest: fulfillRequestFn,
|
|
839
|
-
shouldRefreshCache: shouldRefreshCacheFn,
|
|
840
610
|
getKey: options => handler.getKey(options),
|
|
841
611
|
type: handler.type,
|
|
842
|
-
cache: handler.cache,
|
|
843
612
|
hydrate: handler.hydrate
|
|
844
613
|
};
|
|
845
|
-
}
|
|
614
|
+
}; // We only track data requests when we are server-side and we don't
|
|
615
|
+
// already have a result, as given by the cachedData (which is also the
|
|
616
|
+
// initial value for the result state).
|
|
846
617
|
|
|
847
|
-
_getCacheLookupFnFromInterceptor(interceptor) {
|
|
848
|
-
const getEntry = interceptor && interceptor.getEntry;
|
|
849
618
|
|
|
850
|
-
|
|
851
|
-
return ResponseCache.Default.getEntry;
|
|
852
|
-
}
|
|
619
|
+
const maybeTrack = useContext(TrackerContext);
|
|
853
620
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
621
|
+
if (result == null && Server.isServerSide()) {
|
|
622
|
+
maybeTrack == null ? void 0 : maybeTrack(getMaybeInterceptedHandler(), options);
|
|
623
|
+
} // We need to update our request when the handler changes or the key
|
|
624
|
+
// to the options change, so we keep track of those.
|
|
625
|
+
// However, even if we are hydrating from cache, we still need to make the
|
|
626
|
+
// request at least once, so we do not initialize these references.
|
|
857
627
|
|
|
858
|
-
const interceptedData = getEntry(options, cacheEntry); // 3. Return the appropriate response.
|
|
859
628
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
629
|
+
const handlerRef = useRef();
|
|
630
|
+
const keyRef = useRef();
|
|
631
|
+
const interceptorRef = useRef(); // This effect will ensure that we fulfill the request as desired.
|
|
863
632
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
// If we are server-side, then just skip the effect. We track requests
|
|
635
|
+
// during SSR and fulfill them outside of the React render cycle.
|
|
636
|
+
// NOTE: This shouldn't happen since effects would not run on the server
|
|
637
|
+
// but let's be defensive - I think it makes the code clearer.
|
|
638
|
+
|
|
639
|
+
/* istanbul ignore next */
|
|
640
|
+
if (Server.isServerSide()) {
|
|
641
|
+
return;
|
|
642
|
+
} // Update our refs to the current handler and key.
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
handlerRef.current = handler;
|
|
646
|
+
keyRef.current = handler.getKey(options);
|
|
647
|
+
interceptorRef.current = interceptor; // If we're not hydrating a result, we want to make sure we set our
|
|
648
|
+
// result to null so that we're in the loading state.
|
|
649
|
+
|
|
650
|
+
if (cachedResult == null) {
|
|
651
|
+
// Mark ourselves as loading.
|
|
652
|
+
setResult(null);
|
|
653
|
+
} // We aren't server-side, so let's make the request.
|
|
654
|
+
// The request handler is in control of whether that request actually
|
|
655
|
+
// happens or not.
|
|
868
656
|
|
|
869
|
-
const handler = this._getHandlerFromInterceptor(interceptor);
|
|
870
657
|
|
|
871
|
-
|
|
658
|
+
let cancel = false;
|
|
659
|
+
RequestFulfillment.Default.fulfill(getMaybeInterceptedHandler(), options).then(updateEntry => {
|
|
660
|
+
if (cancel) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
setResult(updateEntry);
|
|
665
|
+
return;
|
|
666
|
+
}).catch(e => {
|
|
667
|
+
if (cancel) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
872
670
|
/**
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
* However, just deriving a new component creates issues
|
|
876
|
-
* where InternalData starts rerendering too often.
|
|
877
|
-
* Couldn't track down why, so suppressing the error
|
|
878
|
-
* instead.
|
|
671
|
+
* We should never get here as errors in fulfillment are part
|
|
672
|
+
* of the `then`, but if we do.
|
|
879
673
|
*/
|
|
674
|
+
// eslint-disable-next-line no-console
|
|
880
675
|
|
|
881
676
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
677
|
+
console.error(`Unexpected error occurred during data fulfillment: ${e}`);
|
|
678
|
+
setResult({
|
|
679
|
+
data: null,
|
|
680
|
+
error: typeof e === "string" ? e : e.message
|
|
681
|
+
});
|
|
682
|
+
return;
|
|
888
683
|
});
|
|
889
|
-
|
|
684
|
+
return () => {
|
|
685
|
+
cancel = true;
|
|
686
|
+
}; // - handler.getKey is a proxy for options
|
|
687
|
+
// - We don't want to trigger on cachedResult changing, we're
|
|
688
|
+
// just using that as a flag for render state if the other things
|
|
689
|
+
// trigger this effect.
|
|
690
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
691
|
+
}, [handler, handler.getKey(options), interceptor]);
|
|
692
|
+
return resultFromCacheEntry(result);
|
|
693
|
+
};
|
|
890
694
|
|
|
891
|
-
|
|
695
|
+
/**
|
|
696
|
+
* This component is the main component of Wonder Blocks Data. With this, data
|
|
697
|
+
* requirements can be placed in a React application in a manner that will
|
|
698
|
+
* support server-side rendering and efficient caching.
|
|
699
|
+
*/
|
|
700
|
+
const Data = props => {
|
|
701
|
+
const data = useData(props.handler, props.options);
|
|
702
|
+
return props.children(data);
|
|
703
|
+
};
|
|
892
704
|
|
|
893
705
|
/**
|
|
894
706
|
* This component provides a mechanism to intercept the data requests for the
|
|
895
707
|
* type of a given handler and provide alternative results. This is mostly
|
|
896
708
|
* useful for testing.
|
|
897
709
|
*
|
|
898
|
-
* Results from this interceptor will end up in the cache. If you
|
|
899
|
-
* wish to only override the cache, use `InterceptCache` instead.
|
|
900
|
-
*
|
|
901
710
|
* This component is not recommended for use in production code as it
|
|
902
711
|
* can prevent predictable functioning of the Wonder Blocks Data framework.
|
|
903
712
|
* One possible side-effect is that inflight requests from the interceptor could
|
|
@@ -909,21 +718,20 @@ class Data extends Component {
|
|
|
909
718
|
* new instance will replace this interceptor for its children. All methods
|
|
910
719
|
* will be replaced.
|
|
911
720
|
*/
|
|
912
|
-
class InterceptData extends Component {
|
|
721
|
+
class InterceptData extends React.Component {
|
|
913
722
|
render() {
|
|
914
|
-
return /*#__PURE__*/createElement(InterceptContext.Consumer, null, value => {
|
|
723
|
+
return /*#__PURE__*/React.createElement(InterceptContext.Consumer, null, value => {
|
|
915
724
|
const handlerType = this.props.handler.type;
|
|
916
725
|
|
|
917
726
|
const interceptor = _extends({}, value[handlerType], {
|
|
918
|
-
fulfillRequest: this.props.fulfillRequest
|
|
919
|
-
shouldRefreshCache: this.props.shouldRefreshCache || null
|
|
727
|
+
fulfillRequest: this.props.fulfillRequest
|
|
920
728
|
});
|
|
921
729
|
|
|
922
730
|
const newValue = _extends({}, value, {
|
|
923
731
|
[handlerType]: interceptor
|
|
924
732
|
});
|
|
925
733
|
|
|
926
|
-
return /*#__PURE__*/createElement(InterceptContext.Provider, {
|
|
734
|
+
return /*#__PURE__*/React.createElement(InterceptContext.Provider, {
|
|
927
735
|
value: newValue
|
|
928
736
|
}, this.props.children);
|
|
929
737
|
});
|
|
@@ -931,42 +739,170 @@ class InterceptData extends Component {
|
|
|
931
739
|
|
|
932
740
|
}
|
|
933
741
|
|
|
742
|
+
const GqlRouterContext = /*#__PURE__*/React.createContext(null);
|
|
743
|
+
|
|
934
744
|
/**
|
|
935
|
-
*
|
|
936
|
-
* type of a given handler and provide alternative values. This is mostly
|
|
937
|
-
* useful for testing.
|
|
938
|
-
*
|
|
939
|
-
* This does not modify the cache in any way. If you want to intercept
|
|
940
|
-
* requests and cache based on the intercept, then use `InterceptData`.
|
|
745
|
+
* Configure GraphQL routing for GraphQL hooks and components.
|
|
941
746
|
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
* These components do not chain. If a different `InterceptCache` instance is
|
|
946
|
-
* rendered within this one that intercepts the same handler type, then that
|
|
947
|
-
* new instance will replace this interceptor for its children.
|
|
747
|
+
* These can be nested. Components and hooks relying on the GraphQL routing
|
|
748
|
+
* will use the configuration from their closest ancestral GqlRouter.
|
|
948
749
|
*/
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
750
|
+
const GqlRouter = ({
|
|
751
|
+
defaultContext: thisDefaultContext,
|
|
752
|
+
fetch: thisFetch,
|
|
753
|
+
children
|
|
754
|
+
}) => {
|
|
755
|
+
// We don't care if we're nested. We always force our callers to define
|
|
756
|
+
// everything. It makes for a clearer API and requires less error checking
|
|
757
|
+
// code (assuming our flow types are correct). We also don't default fetch
|
|
758
|
+
// to anything - our callers can tell us what function to use quite easily.
|
|
759
|
+
// If code that consumes this wants more nuanced nesting, it can implement
|
|
760
|
+
// it within its own GqlRouter than then defers to this one.
|
|
761
|
+
// We want to always use the same object if things haven't changed to avoid
|
|
762
|
+
// over-rendering consumers of our context, let's memoize the configuration.
|
|
763
|
+
// By doing this, if a component under children that uses this context
|
|
764
|
+
// uses React.memo, we won't force it to re-render every time we render
|
|
765
|
+
// because we'll only change the context value if something has actually
|
|
766
|
+
// changed.
|
|
767
|
+
const configuration = React.useMemo(() => ({
|
|
768
|
+
fetch: thisFetch,
|
|
769
|
+
defaultContext: thisDefaultContext
|
|
770
|
+
}), [thisDefaultContext, thisFetch]);
|
|
771
|
+
return /*#__PURE__*/React.createElement(GqlRouterContext.Provider, {
|
|
772
|
+
value: configuration
|
|
773
|
+
}, children);
|
|
774
|
+
};
|
|
957
775
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
776
|
+
/**
|
|
777
|
+
* Error kinds for GqlError.
|
|
778
|
+
*/
|
|
779
|
+
const GqlErrors = Object.freeze(_extends({}, Errors, {
|
|
780
|
+
Network: "Network",
|
|
781
|
+
Parse: "Parse",
|
|
782
|
+
BadResponse: "BadResponse",
|
|
783
|
+
ErrorResult: "ErrorResult"
|
|
784
|
+
}));
|
|
785
|
+
/**
|
|
786
|
+
* An error from the GQL API.
|
|
787
|
+
*/
|
|
961
788
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
789
|
+
class GqlError extends KindError {
|
|
790
|
+
constructor(message, kind, {
|
|
791
|
+
metadata,
|
|
792
|
+
cause
|
|
793
|
+
} = {}) {
|
|
794
|
+
super(message, kind, {
|
|
795
|
+
metadata,
|
|
796
|
+
cause,
|
|
797
|
+
prefix: "Gql"
|
|
965
798
|
});
|
|
966
799
|
}
|
|
967
800
|
|
|
968
801
|
}
|
|
969
802
|
|
|
803
|
+
/**
|
|
804
|
+
* Validate a GQL operation response and extract the data.
|
|
805
|
+
*/
|
|
806
|
+
|
|
807
|
+
const getGqlDataFromResponse = async response => {
|
|
808
|
+
// Get the response as text, that way we can use the text in error
|
|
809
|
+
// messaging, should our parsing fail.
|
|
810
|
+
const bodyText = await response.text();
|
|
811
|
+
let result;
|
|
812
|
+
|
|
813
|
+
try {
|
|
814
|
+
result = JSON.parse(bodyText);
|
|
815
|
+
} catch (e) {
|
|
816
|
+
throw new GqlError("Failed to parse response", GqlErrors.Parse, {
|
|
817
|
+
metadata: {
|
|
818
|
+
statusCode: response.status,
|
|
819
|
+
bodyText
|
|
820
|
+
},
|
|
821
|
+
cause: e
|
|
822
|
+
});
|
|
823
|
+
} // Check for a bad status code.
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
if (response.status >= 300) {
|
|
827
|
+
throw new GqlError("Response unsuccessful", GqlErrors.Network, {
|
|
828
|
+
metadata: {
|
|
829
|
+
statusCode: response.status,
|
|
830
|
+
result
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
} // Check that we have a valid result payload.
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
if ( // Flow shouldn't be warning about this.
|
|
837
|
+
// $FlowIgnore[method-unbinding]
|
|
838
|
+
!Object.prototype.hasOwnProperty.call(result, "data") && // Flow shouldn't be warning about this.
|
|
839
|
+
// $FlowIgnore[method-unbinding]
|
|
840
|
+
!Object.prototype.hasOwnProperty.call(result, "errors")) {
|
|
841
|
+
throw new GqlError("Server response missing", GqlErrors.BadResponse, {
|
|
842
|
+
metadata: {
|
|
843
|
+
statusCode: response.status,
|
|
844
|
+
result
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
} // If the response payload has errors, throw an error.
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
if (result.errors != null && Array.isArray(result.errors) && result.errors.length > 0) {
|
|
851
|
+
throw new GqlError("GraphQL errors", GqlErrors.ErrorResult, {
|
|
852
|
+
metadata: {
|
|
853
|
+
statusCode: response.status,
|
|
854
|
+
result
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
} // We got here, so return the data.
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
return result.data;
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Hook to obtain a gqlFetch function for performing GraphQL requests.
|
|
865
|
+
*
|
|
866
|
+
* The fetch function will resolve null if the request was aborted, otherwise
|
|
867
|
+
* it will resolve the data returned by the GraphQL server.
|
|
868
|
+
*/
|
|
869
|
+
const useGql = () => {
|
|
870
|
+
// This hook only works if the `GqlRouter` has been used to setup context.
|
|
871
|
+
const gqlRouterContext = useContext(GqlRouterContext);
|
|
872
|
+
|
|
873
|
+
if (gqlRouterContext == null) {
|
|
874
|
+
throw new GqlError("No GqlRouter", GqlErrors.Internal);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const {
|
|
878
|
+
fetch,
|
|
879
|
+
defaultContext
|
|
880
|
+
} = gqlRouterContext; // Let's memoize the gqlFetch function we create based off our context.
|
|
881
|
+
// That way, even if the context happens to change, if its values don't
|
|
882
|
+
// we give the same function instance back to our callers instead of
|
|
883
|
+
// making a new one. That then means they can safely use the return value
|
|
884
|
+
// in hooks deps without fear of it triggering extra renders.
|
|
885
|
+
|
|
886
|
+
const gqlFetch = useMemo(() => (operation, options = Object.freeze({})) => {
|
|
887
|
+
const {
|
|
888
|
+
variables,
|
|
889
|
+
context
|
|
890
|
+
} = options; // Invoke the fetch and extract the data.
|
|
891
|
+
|
|
892
|
+
return fetch(operation, variables, _extends({}, defaultContext, context)).then(getGqlDataFromResponse, error => {
|
|
893
|
+
// Return null if the request was aborted.
|
|
894
|
+
// The only way to detect this reliably, it seems, is to
|
|
895
|
+
// check the error name and see if it's "AbortError" (this
|
|
896
|
+
// is also what Apollo does).
|
|
897
|
+
// Even then, it's reliant on the fetch supporting aborts.
|
|
898
|
+
if (error.name === "AbortError") {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}, [fetch, defaultContext]);
|
|
903
|
+
return gqlFetch;
|
|
904
|
+
};
|
|
905
|
+
|
|
970
906
|
const initializeCache = source => ResponseCache.Default.initialize(source);
|
|
971
907
|
const fulfillAllDataRequests = () => {
|
|
972
908
|
if (!Server.isServerSide()) {
|
|
@@ -985,4 +921,4 @@ const hasUnfulfilledRequests = () => {
|
|
|
985
921
|
const removeFromCache = (handler, options) => ResponseCache.Default.remove(handler, options);
|
|
986
922
|
const removeAllFromCache = (handler, predicate) => ResponseCache.Default.removeAll(handler, predicate);
|
|
987
923
|
|
|
988
|
-
export { Data,
|
|
924
|
+
export { Data, GqlError, GqlErrors, GqlRouter, InterceptData, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache, useData, useGql };
|