@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/es/index.js +365 -429
  3. package/dist/index.js +455 -461
  4. package/docs.md +19 -13
  5. package/package.json +6 -6
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +40 -160
  7. package/src/__tests__/generated-snapshot.test.js +15 -195
  8. package/src/components/__tests__/data.test.js +159 -965
  9. package/src/components/__tests__/gql-router.test.js +64 -0
  10. package/src/components/__tests__/intercept-data.test.js +9 -66
  11. package/src/components/__tests__/track-data.test.js +6 -5
  12. package/src/components/data.js +9 -119
  13. package/src/components/data.md +38 -60
  14. package/src/components/gql-router.js +66 -0
  15. package/src/components/intercept-context.js +2 -3
  16. package/src/components/intercept-data.js +2 -34
  17. package/src/components/intercept-data.md +7 -105
  18. package/src/hooks/__tests__/use-data.test.js +826 -0
  19. package/src/hooks/__tests__/use-gql.test.js +233 -0
  20. package/src/hooks/use-data.js +143 -0
  21. package/src/hooks/use-gql.js +75 -0
  22. package/src/index.js +7 -9
  23. package/src/util/__tests__/get-gql-data-from-response.test.js +187 -0
  24. package/src/util/__tests__/memory-cache.test.js +134 -35
  25. package/src/util/__tests__/request-fulfillment.test.js +21 -36
  26. package/src/util/__tests__/request-handler.test.js +30 -30
  27. package/src/util/__tests__/request-tracking.test.js +29 -30
  28. package/src/util/__tests__/response-cache.test.js +521 -561
  29. package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
  30. package/src/util/get-gql-data-from-response.js +69 -0
  31. package/src/util/gql-error.js +36 -0
  32. package/src/util/gql-router-context.js +6 -0
  33. package/src/util/gql-types.js +60 -0
  34. package/src/util/memory-cache.js +20 -15
  35. package/src/util/request-fulfillment.js +4 -0
  36. package/src/util/request-handler.js +4 -28
  37. package/src/util/request-handler.md +0 -32
  38. package/src/util/request-tracking.js +2 -3
  39. package/src/util/response-cache.js +50 -110
  40. package/src/util/result-from-cache-entry.js +38 -0
  41. package/src/util/types.js +14 -35
  42. package/LICENSE +0 -21
  43. package/src/components/__tests__/intercept-cache.test.js +0 -124
  44. package/src/components/__tests__/internal-data.test.js +0 -1030
  45. package/src/components/intercept-cache.js +0 -79
  46. package/src/components/intercept-cache.md +0 -103
  47. package/src/components/internal-data.js +0 -219
  48. package/src/util/__tests__/no-cache.test.js +0 -112
  49. package/src/util/no-cache.js +0 -66
  50. package/src/util/no-cache.md +0 -66
package/dist/index.js CHANGED
@@ -82,7 +82,7 @@ module.exports =
82
82
  /******/
83
83
  /******/
84
84
  /******/ // Load entry module and return exports
85
- /******/ return __webpack_require__(__webpack_require__.s = 14);
85
+ /******/ return __webpack_require__(__webpack_require__.s = 19);
86
86
  /******/ })
87
87
  /************************************************************************/
88
88
  /******/ ([
@@ -95,13 +95,55 @@ module.exports = require("react");
95
95
  /* 1 */
96
96
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
97
97
 
98
+ "use strict";
99
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return GqlErrors; });
100
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return GqlError; });
101
+ /* harmony import */ var _khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(10);
102
+ /* harmony import */ var _khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0__);
103
+
104
+
105
+ /**
106
+ * Error kinds for GqlError.
107
+ */
108
+ const GqlErrors = Object.freeze({ ..._khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0__["Errors"],
109
+ Network: "Network",
110
+ Parse: "Parse",
111
+ BadResponse: "BadResponse",
112
+ ErrorResult: "ErrorResult"
113
+ });
114
+ /**
115
+ * An error from the GQL API.
116
+ */
117
+
118
+ class GqlError extends _khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0__["KindError"] {
119
+ constructor(message, kind, {
120
+ metadata,
121
+ cause
122
+ } = {}) {
123
+ super(message, kind, {
124
+ metadata,
125
+ cause,
126
+ prefix: "Gql"
127
+ });
128
+ }
129
+
130
+ }
131
+
132
+ /***/ }),
133
+ /* 2 */
134
+ /***/ (function(module, exports) {
135
+
136
+ module.exports = require("@khanacademy/wonder-blocks-core");
137
+
138
+ /***/ }),
139
+ /* 3 */
140
+ /***/ (function(module, __webpack_exports__, __webpack_require__) {
141
+
98
142
  "use strict";
99
143
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return ResponseCache; });
100
144
  /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
101
145
  /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__);
102
- /* harmony import */ var _memory_cache_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7);
103
- /* harmony import */ var _no_cache_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(5);
104
-
146
+ /* harmony import */ var _memory_cache_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
105
147
 
106
148
 
107
149
 
@@ -126,24 +168,22 @@ class ResponseCache {
126
168
  return _default;
127
169
  }
128
170
 
129
- constructor(memoryCache = null, ssrOnlyCache = null) {
171
+ constructor(hydrationCache = null, ssrOnlyCache = null) {
130
172
  this.initialize = source => {
131
- if (this._hydrationAndDefaultCache.inUse) {
173
+ if (this._hydrationCache.inUse) {
132
174
  throw new Error("Cannot initialize data response cache more than once");
133
175
  }
134
176
 
135
177
  try {
136
- this._hydrationAndDefaultCache = new _memory_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"](source);
178
+ this._hydrationCache = new _memory_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"](source);
137
179
  } catch (e) {
138
180
  throw new Error(`An error occurred trying to initialize the data response cache: ${e}`);
139
181
  }
140
182
  };
141
183
 
142
- this.cacheData = (handler, options, data) => {
143
- return this._setCacheEntry(handler, options, {
144
- data
145
- });
146
- };
184
+ this.cacheData = (handler, options, data) => this._setCacheEntry(handler, options, {
185
+ data
186
+ });
147
187
 
148
188
  this.cacheError = (handler, options, error) => {
149
189
  const errorMessage = typeof error === "string" ? error : error.message;
@@ -153,110 +193,64 @@ class ResponseCache {
153
193
  };
154
194
 
155
195
  this.getEntry = (handler, options) => {
156
- // If we're not server-side, and the handler has a custom cache
157
- // let's try to use it.
158
- if (this._ssrOnlyCache == null && handler.cache != null) {
159
- const entry = handler.cache.retrieve(handler, options);
160
-
161
- if (entry != null) {
162
- // Custom cache has an entry, so use it.
163
- return entry;
164
- }
165
- } // Get the internal entry for the handler.
166
- // This allows us to use our hydrated cache during hydration.
167
- // If we just returned null when the custom cache didn't have it,
168
- // we would never hydrate properly.
169
-
170
-
171
- const internalEntry = this._defaultCache(handler).retrieve(handler, options); // If we are not server-side and we hydrated something that the custom
172
- // cache didn't have, we need to make sure the custom cache contains
173
- // that value.
174
-
175
-
176
- if (this._ssrOnlyCache == null && handler.cache != null && internalEntry != null) {
177
- // Yes, if this throws, we will have a problem. We want that.
178
- // Bad cache implementations should be overt.
179
- 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 : _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__["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.
180
206
  // This does mean that if another handler of the same type but
181
- // without a custom cache won't get the value, but that's not an
182
- // expected valid usage of this framework - two handlers with
183
- // different caching options shouldn't be using the same type name.
184
-
185
- this._hydrationAndDefaultCache.remove(handler, options);
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);
186
212
  }
187
213
 
188
214
  return internalEntry;
189
215
  };
190
216
 
191
217
  this.remove = (handler, options) => {
218
+ var _this$_ssrOnlyCache$r, _this$_ssrOnlyCache;
219
+
192
220
  // NOTE(somewhatabstract): We could invoke removeAll with a predicate
193
221
  // to match the key of the entry we're removing, but that's an
194
222
  // inefficient way to remove a single item, so let's not do that.
195
- // If we're not server-side, and the handler has a custom cache
196
- // let's try to use it.
197
- const customCache = this._ssrOnlyCache == null ? handler.cache : null;
198
- const removedCustom = !!(customCache != null && customCache.remove(handler, options)); // Delete the entry from our internal cache.
199
- // Even if we have a custom cache, we want to make sure we still
200
- // removed the same value from internal cache since this could be
201
- // getting called before hydration for some complex advanced usage
202
- // reason.
203
-
204
- 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;
205
225
  };
206
226
 
207
227
  this.removeAll = (handler, predicate) => {
208
- // If we're not server-side, and the handler has a custom cache
209
- // let's try to use it.
210
- const customCache = this._ssrOnlyCache == null ? handler.cache : null;
211
- const removedCountCustom = (customCache == null ? void 0 : customCache.removeAll(handler, predicate)) || 0; // Apply the predicate to what we have in our internal cached.
212
- // Even if we have a custom cache, we want to make sure we still
213
- // removed the same value from internal cache since this could be
214
- // getting called before hydration for some complex advanced usage
215
- // reason.
216
-
217
- const removedCount = this._defaultCache(handler).removeAll(handler, predicate); // We have no idea which keys were removed from which caches,
218
- // so we can't dedupe the remove counts based on keys.
219
- // That's why we return the total records deleted rather than the
220
- // total keys deleted.
221
-
222
-
223
- 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;
224
232
  };
225
233
 
226
234
  this.cloneHydratableData = () => {
227
235
  // We return our hydration cache only.
228
- return this._hydrationAndDefaultCache.cloneData();
236
+ return this._hydrationCache.cloneData();
229
237
  };
230
238
 
231
239
  this._ssrOnlyCache = _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__["Server"].isServerSide() ? ssrOnlyCache || new _memory_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"]() : undefined;
232
- this._hydrationAndDefaultCache = memoryCache || new _memory_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"]();
233
- }
234
- /**
235
- * Returns the default cache to use for the given handler.
236
- */
237
-
238
-
239
- _defaultCache(handler) {
240
- if (handler.hydrate) {
241
- return this._hydrationAndDefaultCache;
242
- } // If the handler doesn't want to hydrate, we return the SSR-only cache.
243
- // If we are client-side, we return our non-caching implementation.
244
-
245
-
246
- return this._ssrOnlyCache || _no_cache_js__WEBPACK_IMPORTED_MODULE_2__[/* default */ "a"].Default;
240
+ this._hydrationCache = hydrationCache || new _memory_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"]();
247
241
  }
248
242
 
249
243
  _setCacheEntry(handler, options, entry) {
250
244
  const frozenEntry = Object.freeze(entry);
251
245
 
252
- if (this._ssrOnlyCache == null && handler.cache != null) {
253
- // We are not server-side, and our handler has its own cache,
254
- // so we use that to store values.
255
- handler.cache.store(handler, options, frozenEntry);
256
- } else {
257
- // We are either server-side, or our handler doesn't provide
258
- // a caching override.
259
- this._defaultCache(handler).store(handler, options, frozenEntry);
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
+ }
260
254
  }
261
255
 
262
256
  return frozenEntry;
@@ -271,13 +265,7 @@ class ResponseCache {
271
265
  }
272
266
 
273
267
  /***/ }),
274
- /* 2 */
275
- /***/ (function(module, exports) {
276
-
277
- module.exports = require("@khanacademy/wonder-blocks-core");
278
-
279
- /***/ }),
280
- /* 3 */
268
+ /* 4 */
281
269
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
282
270
 
283
271
  "use strict";
@@ -285,8 +273,8 @@ module.exports = require("@khanacademy/wonder-blocks-core");
285
273
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return RequestTracker; });
286
274
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
287
275
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
288
- /* harmony import */ var _response_cache_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1);
289
- /* harmony import */ var _request_fulfillment_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);
276
+ /* harmony import */ var _response_cache_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
277
+ /* harmony import */ var _request_fulfillment_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
290
278
 
291
279
 
292
280
 
@@ -429,7 +417,7 @@ class RequestTracker {
429
417
  }
430
418
 
431
419
  /***/ }),
432
- /* 4 */
420
+ /* 5 */
433
421
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
434
422
 
435
423
  "use strict";
@@ -446,54 +434,150 @@ const InterceptContext = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["create
446
434
  /* harmony default export */ __webpack_exports__["a"] = (InterceptContext);
447
435
 
448
436
  /***/ }),
449
- /* 5 */
437
+ /* 6 */
450
438
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
451
439
 
452
440
  "use strict";
453
- /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return NoCache; });
454
- let defaultInstance = null;
455
- /**
456
- * This is a cache implementation to use when no caching is wanted.
457
- *
458
- * Use this with your request handler if you want to support server-side
459
- * rendering of your data requests, but want to ensure data is never cached
460
- * on the client-side.
461
- *
462
- * This is better than having `shouldRefreshCache` always return `true` in the
463
- * handler as this ensures that cache space and memory are never used for the
464
- * requested data after hydration has finished.
465
- */
441
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return useData; });
442
+ /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
443
+ /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__);
444
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(0);
445
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
446
+ /* harmony import */ var _util_request_fulfillment_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
447
+ /* harmony import */ var _components_intercept_context_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5);
448
+ /* harmony import */ var _util_request_tracking_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4);
449
+ /* harmony import */ var _util_result_from_cache_entry_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(17);
450
+ /* harmony import */ var _util_response_cache_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(3);
466
451
 
467
- class NoCache {
468
- constructor() {
469
- this.store = (handler, options, entry) => {
470
- /* empty */
471
- };
472
452
 
473
- this.retrieve = (handler, options) => null;
474
453
 
475
- this.remove = (handler, options) => false;
476
454
 
477
- this.removeAll = (handler, predicate) => 0;
478
- }
479
455
 
480
- static get Default() {
481
- if (defaultInstance == null) {
482
- defaultInstance = new NoCache();
456
+
457
+
458
+ const useData = (handler, options) => {
459
+ // If we're server-side or hydrating, we'll have a cached entry to use.
460
+ // So we get that and use it to initialize our state.
461
+ // This works in both hydration and SSR because the very first call to
462
+ // this will have cached data in those cases as it will be present on the
463
+ // initial render - and subsequent renders on the client it will be null.
464
+ const cachedResult = _util_response_cache_js__WEBPACK_IMPORTED_MODULE_6__[/* ResponseCache */ "a"].Default.getEntry(handler, options);
465
+ const [result, setResult] = Object(react__WEBPACK_IMPORTED_MODULE_1__["useState"])(cachedResult); // Lookup to see if there's an interceptor for the handler.
466
+ // If we have one, we need to replace the handler with one that
467
+ // uses the interceptor.
468
+
469
+ const interceptorMap = Object(react__WEBPACK_IMPORTED_MODULE_1__["useContext"])(_components_intercept_context_js__WEBPACK_IMPORTED_MODULE_3__[/* default */ "a"]);
470
+ const interceptor = interceptorMap[handler.type]; // If we have an interceptor, we need to replace the handler with one that
471
+ // uses the interceptor. This helper function generates a new handler.
472
+ // We need this before we track the request as we want the interceptor
473
+ // to also work for tracked requests to simplify testing the server-side
474
+ // request fulfillment.
475
+
476
+ const getMaybeInterceptedHandler = () => {
477
+ if (interceptor == null) {
478
+ return handler;
483
479
  }
484
480
 
485
- return defaultInstance;
486
- }
481
+ const fulfillRequestFn = options => {
482
+ var _interceptor$fulfillR;
487
483
 
488
- }
484
+ return (_interceptor$fulfillR = interceptor.fulfillRequest(options)) != null ? _interceptor$fulfillR : handler.fulfillRequest(options);
485
+ };
486
+
487
+ return {
488
+ fulfillRequest: fulfillRequestFn,
489
+ getKey: options => handler.getKey(options),
490
+ type: handler.type,
491
+ hydrate: handler.hydrate
492
+ };
493
+ }; // We only track data requests when we are server-side and we don't
494
+ // already have a result, as given by the cachedData (which is also the
495
+ // initial value for the result state).
496
+
497
+
498
+ const maybeTrack = Object(react__WEBPACK_IMPORTED_MODULE_1__["useContext"])(_util_request_tracking_js__WEBPACK_IMPORTED_MODULE_4__[/* TrackerContext */ "b"]);
499
+
500
+ if (result == null && _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__["Server"].isServerSide()) {
501
+ maybeTrack == null ? void 0 : maybeTrack(getMaybeInterceptedHandler(), options);
502
+ } // We need to update our request when the handler changes or the key
503
+ // to the options change, so we keep track of those.
504
+ // However, even if we are hydrating from cache, we still need to make the
505
+ // request at least once, so we do not initialize these references.
506
+
507
+
508
+ const handlerRef = Object(react__WEBPACK_IMPORTED_MODULE_1__["useRef"])();
509
+ const keyRef = Object(react__WEBPACK_IMPORTED_MODULE_1__["useRef"])();
510
+ const interceptorRef = Object(react__WEBPACK_IMPORTED_MODULE_1__["useRef"])(); // This effect will ensure that we fulfill the request as desired.
511
+
512
+ Object(react__WEBPACK_IMPORTED_MODULE_1__["useEffect"])(() => {
513
+ // If we are server-side, then just skip the effect. We track requests
514
+ // during SSR and fulfill them outside of the React render cycle.
515
+ // NOTE: This shouldn't happen since effects would not run on the server
516
+ // but let's be defensive - I think it makes the code clearer.
517
+
518
+ /* istanbul ignore next */
519
+ if (_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__["Server"].isServerSide()) {
520
+ return;
521
+ } // Update our refs to the current handler and key.
522
+
523
+
524
+ handlerRef.current = handler;
525
+ keyRef.current = handler.getKey(options);
526
+ interceptorRef.current = interceptor; // If we're not hydrating a result, we want to make sure we set our
527
+ // result to null so that we're in the loading state.
528
+
529
+ if (cachedResult == null) {
530
+ // Mark ourselves as loading.
531
+ setResult(null);
532
+ } // We aren't server-side, so let's make the request.
533
+ // The request handler is in control of whether that request actually
534
+ // happens or not.
535
+
536
+
537
+ let cancel = false;
538
+ _util_request_fulfillment_js__WEBPACK_IMPORTED_MODULE_2__[/* RequestFulfillment */ "a"].Default.fulfill(getMaybeInterceptedHandler(), options).then(updateEntry => {
539
+ if (cancel) {
540
+ return;
541
+ }
542
+
543
+ setResult(updateEntry);
544
+ return;
545
+ }).catch(e => {
546
+ if (cancel) {
547
+ return;
548
+ }
549
+ /**
550
+ * We should never get here as errors in fulfillment are part
551
+ * of the `then`, but if we do.
552
+ */
553
+ // eslint-disable-next-line no-console
554
+
555
+
556
+ console.error(`Unexpected error occurred during data fulfillment: ${e}`);
557
+ setResult({
558
+ data: null,
559
+ error: typeof e === "string" ? e : e.message
560
+ });
561
+ return;
562
+ });
563
+ return () => {
564
+ cancel = true;
565
+ }; // - handler.getKey is a proxy for options
566
+ // - We don't want to trigger on cachedResult changing, we're
567
+ // just using that as a flag for render state if the other things
568
+ // trigger this effect.
569
+ // eslint-disable-next-line react-hooks/exhaustive-deps
570
+ }, [handler, handler.getKey(options), interceptor]);
571
+ return Object(_util_result_from_cache_entry_js__WEBPACK_IMPORTED_MODULE_5__[/* resultFromCacheEntry */ "a"])(result);
572
+ };
489
573
 
490
574
  /***/ }),
491
- /* 6 */
575
+ /* 7 */
492
576
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
493
577
 
494
578
  "use strict";
495
579
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return RequestFulfillment; });
496
- /* harmony import */ var _response_cache_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
580
+ /* harmony import */ var _response_cache_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
497
581
 
498
582
 
499
583
  let _default;
@@ -546,6 +630,8 @@ class RequestFulfillment {
546
630
  delete handlerRequests[key];
547
631
  /**
548
632
  * Let's cache the data!
633
+ *
634
+ * NOTE: This only caches when we're server side.
549
635
  */
550
636
 
551
637
  return cacheData(handler, options, data);
@@ -553,6 +639,8 @@ class RequestFulfillment {
553
639
  delete handlerRequests[key];
554
640
  /**
555
641
  * Let's cache the error!
642
+ *
643
+ * NOTE: This only caches when we're server side.
556
644
  */
557
645
 
558
646
  return cacheError(handler, options, error);
@@ -574,7 +662,18 @@ class RequestFulfillment {
574
662
  }
575
663
 
576
664
  /***/ }),
577
- /* 7 */
665
+ /* 8 */
666
+ /***/ (function(module, __webpack_exports__, __webpack_require__) {
667
+
668
+ "use strict";
669
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return GqlRouterContext; });
670
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
671
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
672
+
673
+ const GqlRouterContext = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createContext"](null);
674
+
675
+ /***/ }),
676
+ /* 9 */
578
677
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
579
678
 
580
679
  "use strict";
@@ -596,9 +695,7 @@ function deepClone(source) {
596
695
  *
597
696
  * Special case cache implementation for the memory cache.
598
697
  *
599
- * This is only used within our framework. Handlers don't need to
600
- * provide this as a custom cache as the framework will default to this in the
601
- * absence of a custom cache. We use this for SSR too (see ./response-cache.js).
698
+ * This is only used within our framework for SSR (see ./response-cache.js).
602
699
  */
603
700
 
604
701
 
@@ -606,7 +703,7 @@ class MemoryCache {
606
703
  constructor(source = null) {
607
704
  this.store = (handler, options, entry) => {
608
705
  const requestType = handler.type;
609
- const frozenEntry = Object.isFrozen(entry) ? entry : Object.freeze(entry); // Ensure we have a cache location for this handler type.
706
+ const frozenEntry = Object.freeze(entry); // Ensure we have a cache location for this handler type.
610
707
 
611
708
  this._cache[requestType] = this._cache[requestType] || {}; // Cache the data.
612
709
 
@@ -666,16 +763,22 @@ class MemoryCache {
666
763
 
667
764
  if (!handlerCache) {
668
765
  return 0;
669
- } // Apply the predicate to what we have cached.
670
-
766
+ }
671
767
 
672
768
  let removedCount = 0;
673
769
 
674
- for (const [key, entry] of Object.entries(handlerCache)) {
675
- if (typeof predicate !== "function" || predicate(key, entry)) {
676
- removedCount++;
677
- delete handlerCache[key];
770
+ if (typeof predicate === "function") {
771
+ // Apply the predicate to what we have cached.
772
+ for (const [key, entry] of Object.entries(handlerCache)) {
773
+ if (predicate(key, entry)) {
774
+ removedCount++;
775
+ delete handlerCache[key];
776
+ }
678
777
  }
778
+ } else {
779
+ // We're removing everything so delete the entire subcache.
780
+ removedCount = Object.keys(handlerCache).length;
781
+ delete this._cache[requestType];
679
782
  }
680
783
 
681
784
  return removedCount;
@@ -705,6 +808,12 @@ class MemoryCache {
705
808
  }
706
809
  }
707
810
  }
811
+ /**
812
+ * Indicate if this cache is being used or now.
813
+ *
814
+ * When the cache has entries, returns `true`; otherwise, returns `false`.
815
+ */
816
+
708
817
 
709
818
  get inUse() {
710
819
  return Object.keys(this._cache).length > 0;
@@ -713,7 +822,13 @@ class MemoryCache {
713
822
  }
714
823
 
715
824
  /***/ }),
716
- /* 8 */
825
+ /* 10 */
826
+ /***/ (function(module, exports) {
827
+
828
+ module.exports = require("@khanacademy/wonder-stuff-core");
829
+
830
+ /***/ }),
831
+ /* 11 */
717
832
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
718
833
 
719
834
  "use strict";
@@ -725,9 +840,8 @@ class MemoryCache {
725
840
  * use with the Wonder Blocks Data framework.
726
841
  */
727
842
  class RequestHandler {
728
- constructor(type, cache, hydrate = true) {
843
+ constructor(type, hydrate = true) {
729
844
  this._type = type;
730
- this._cache = cache || null;
731
845
  this._hydrate = !!hydrate;
732
846
  }
733
847
 
@@ -735,26 +849,10 @@ class RequestHandler {
735
849
  return this._type;
736
850
  }
737
851
 
738
- get cache() {
739
- return this._cache;
740
- }
741
-
742
852
  get hydrate() {
743
853
  return this._hydrate;
744
854
  }
745
855
 
746
- shouldRefreshCache(options, cachedEntry) {
747
- /**
748
- * By default, the cache needs a refresh if the current entry is an
749
- * error.
750
- *
751
- * This means that an error will cause a re-request on render.
752
- * Useful if the server rendered an error, as it means the client
753
- * will update after rehydration.
754
- */
755
- return cachedEntry == null || cachedEntry.error != null;
756
- }
757
-
758
856
  getKey(options) {
759
857
  try {
760
858
  return options === undefined ? "undefined" : JSON.stringify(options);
@@ -770,7 +868,7 @@ class RequestHandler {
770
868
  }
771
869
 
772
870
  /***/ }),
773
- /* 9 */
871
+ /* 12 */
774
872
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
775
873
 
776
874
  "use strict";
@@ -779,7 +877,7 @@ class RequestHandler {
779
877
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
780
878
  /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
781
879
  /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_1__);
782
- /* harmony import */ var _util_request_tracking_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3);
880
+ /* harmony import */ var _util_request_tracking_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
783
881
 
784
882
 
785
883
 
@@ -801,18 +899,13 @@ class TrackData extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
801
899
  }
802
900
 
803
901
  /***/ }),
804
- /* 10 */
902
+ /* 13 */
805
903
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
806
904
 
807
905
  "use strict";
808
- /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Data; });
809
906
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
810
907
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
811
- /* harmony import */ var _util_response_cache_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1);
812
- /* harmony import */ var _internal_data_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
813
- /* harmony import */ var _intercept_context_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4);
814
-
815
-
908
+ /* harmony import */ var _hooks_use_data_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6);
816
909
 
817
910
 
818
911
 
@@ -821,93 +914,22 @@ class TrackData extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
821
914
  * requirements can be placed in a React application in a manner that will
822
915
  * support server-side rendering and efficient caching.
823
916
  */
824
- class Data extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
825
- _getHandlerFromInterceptor(interceptor) {
826
- const {
827
- handler
828
- } = this.props;
829
-
830
- if (!interceptor) {
831
- return handler;
832
- }
833
-
834
- const {
835
- fulfillRequest,
836
- shouldRefreshCache
837
- } = interceptor;
838
- const fulfillRequestFn = fulfillRequest ? options => {
839
- const interceptedResult = fulfillRequest(options);
840
- return interceptedResult != null ? interceptedResult : handler.fulfillRequest(options);
841
- } : options => handler.fulfillRequest(options);
842
- const shouldRefreshCacheFn = shouldRefreshCache ? (options, cacheEntry) => {
843
- const interceptedResult = shouldRefreshCache(options, cacheEntry);
844
- return interceptedResult != null ? interceptedResult : handler.shouldRefreshCache(options, cacheEntry);
845
- } : (options, cacheEntry) => handler.shouldRefreshCache(options, cacheEntry);
846
- return {
847
- fulfillRequest: fulfillRequestFn,
848
- shouldRefreshCache: shouldRefreshCacheFn,
849
- getKey: options => handler.getKey(options),
850
- type: handler.type,
851
- cache: handler.cache,
852
- hydrate: handler.hydrate
853
- };
854
- }
855
-
856
- _getCacheLookupFnFromInterceptor(interceptor) {
857
- const getEntry = interceptor && interceptor.getEntry;
858
-
859
- if (!getEntry) {
860
- return _util_response_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* ResponseCache */ "a"].Default.getEntry;
861
- }
862
-
863
- return (handler, options) => {
864
- // 1. Lookup the current cache value.
865
- const cacheEntry = _util_response_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* ResponseCache */ "a"].Default.getEntry(handler, options); // 2. See if our interceptor wants to override it.
866
-
867
- const interceptedData = getEntry(options, cacheEntry); // 3. Return the appropriate response.
868
-
869
- return interceptedData != null ? interceptedData : cacheEntry;
870
- };
871
- }
872
-
873
- render() {
874
- return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_intercept_context_js__WEBPACK_IMPORTED_MODULE_3__[/* default */ "a"].Consumer, null, value => {
875
- const handlerType = this.props.handler.type;
876
- const interceptor = value[handlerType];
877
-
878
- const handler = this._getHandlerFromInterceptor(interceptor);
879
-
880
- const getEntry = this._getCacheLookupFnFromInterceptor(interceptor);
881
- /**
882
- * Need to share our types with InternalData so Flow
883
- * doesn't need to infer them and find mismatches.
884
- * However, just deriving a new component creates issues
885
- * where InternalData starts rerendering too often.
886
- * Couldn't track down why, so suppressing the error
887
- * instead.
888
- */
889
-
890
-
891
- return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_internal_data_js__WEBPACK_IMPORTED_MODULE_2__[/* default */ "a"] // $FlowIgnore[incompatible-type-arg]
892
- , {
893
- handler: handler,
894
- options: this.props.options,
895
- getEntry: getEntry
896
- }, result => this.props.children(result));
897
- });
898
- }
917
+ const Data = props => {
918
+ const data = Object(_hooks_use_data_js__WEBPACK_IMPORTED_MODULE_1__[/* useData */ "a"])(props.handler, props.options);
919
+ return props.children(data);
920
+ };
899
921
 
900
- }
922
+ /* harmony default export */ __webpack_exports__["a"] = (Data);
901
923
 
902
924
  /***/ }),
903
- /* 11 */
925
+ /* 14 */
904
926
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
905
927
 
906
928
  "use strict";
907
929
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return InterceptData; });
908
930
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
909
931
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
910
- /* harmony import */ var _intercept_context_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
932
+ /* harmony import */ var _intercept_context_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5);
911
933
 
912
934
 
913
935
 
@@ -916,9 +938,6 @@ class Data extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
916
938
  * type of a given handler and provide alternative results. This is mostly
917
939
  * useful for testing.
918
940
  *
919
- * Results from this interceptor will end up in the cache. If you
920
- * wish to only override the cache, use `InterceptCache` instead.
921
- *
922
941
  * This component is not recommended for use in production code as it
923
942
  * can prevent predictable functioning of the Wonder Blocks Data framework.
924
943
  * One possible side-effect is that inflight requests from the interceptor could
@@ -935,8 +954,7 @@ class InterceptData extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
935
954
  return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_intercept_context_js__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"].Consumer, null, value => {
936
955
  const handlerType = this.props.handler.type;
937
956
  const interceptor = { ...value[handlerType],
938
- fulfillRequest: this.props.fulfillRequest || null,
939
- shouldRefreshCache: this.props.shouldRefreshCache || null
957
+ fulfillRequest: this.props.fulfillRequest
940
958
  };
941
959
  const newValue = { ...value,
942
960
  [handlerType]: interceptor
@@ -950,252 +968,223 @@ class InterceptData extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
950
968
  }
951
969
 
952
970
  /***/ }),
953
- /* 12 */
971
+ /* 15 */
954
972
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
955
973
 
956
974
  "use strict";
957
- /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return InterceptCache; });
975
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return GqlRouter; });
958
976
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
959
977
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
960
- /* harmony import */ var _intercept_context_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
978
+ /* harmony import */ var _util_gql_router_context_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8);
961
979
 
962
980
 
963
981
 
964
982
  /**
965
- * This component provides a mechanism to intercept cache lookups for the
966
- * type of a given handler and provide alternative values. This is mostly
967
- * useful for testing.
983
+ * Configure GraphQL routing for GraphQL hooks and components.
968
984
  *
969
- * This does not modify the cache in any way. If you want to intercept
970
- * requests and cache based on the intercept, then use `InterceptData`.
971
- *
972
- * This component is generally not suitable for use in production code as it
973
- * can prevent predictable functioning of the Wonder Blocks Data framework.
974
- *
975
- * These components do not chain. If a different `InterceptCache` instance is
976
- * rendered within this one that intercepts the same handler type, then that
977
- * new instance will replace this interceptor for its children.
985
+ * These can be nested. Components and hooks relying on the GraphQL routing
986
+ * will use the configuration from their closest ancestral GqlRouter.
978
987
  */
979
- class InterceptCache extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
980
- render() {
981
- return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_intercept_context_js__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"].Consumer, null, value => {
982
- const handlerType = this.props.handler.type;
983
- const interceptor = { ...value[handlerType],
984
- getEntry: this.props.getEntry
985
- };
986
- const newValue = { ...value,
987
- [handlerType]: interceptor
988
- };
989
- return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_intercept_context_js__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"].Provider, {
990
- value: newValue
991
- }, this.props.children);
992
- });
993
- }
994
-
995
- }
988
+ const GqlRouter = ({
989
+ defaultContext: thisDefaultContext,
990
+ fetch: thisFetch,
991
+ children
992
+ }) => {
993
+ // We don't care if we're nested. We always force our callers to define
994
+ // everything. It makes for a clearer API and requires less error checking
995
+ // code (assuming our flow types are correct). We also don't default fetch
996
+ // to anything - our callers can tell us what function to use quite easily.
997
+ // If code that consumes this wants more nuanced nesting, it can implement
998
+ // it within its own GqlRouter than then defers to this one.
999
+ // We want to always use the same object if things haven't changed to avoid
1000
+ // over-rendering consumers of our context, let's memoize the configuration.
1001
+ // By doing this, if a component under children that uses this context
1002
+ // uses React.memo, we won't force it to re-render every time we render
1003
+ // because we'll only change the context value if something has actually
1004
+ // changed.
1005
+ const configuration = react__WEBPACK_IMPORTED_MODULE_0__["useMemo"](() => ({
1006
+ fetch: thisFetch,
1007
+ defaultContext: thisDefaultContext
1008
+ }), [thisDefaultContext, thisFetch]);
1009
+ return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_util_gql_router_context_js__WEBPACK_IMPORTED_MODULE_1__[/* GqlRouterContext */ "a"].Provider, {
1010
+ value: configuration
1011
+ }, children);
1012
+ };
996
1013
 
997
1014
  /***/ }),
998
- /* 13 */
1015
+ /* 16 */
999
1016
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
1000
1017
 
1001
1018
  "use strict";
1002
- /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return InternalData; });
1019
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return useGql; });
1003
1020
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
1004
1021
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
1005
- /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
1006
- /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_1__);
1007
- /* harmony import */ var _util_request_fulfillment_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);
1008
- /* harmony import */ var _util_request_tracking_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3);
1022
+ /* harmony import */ var _util_gql_router_context_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8);
1023
+ /* harmony import */ var _util_get_gql_data_from_response_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(18);
1024
+ /* harmony import */ var _util_gql_error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(1);
1009
1025
 
1010
1026
 
1011
1027
 
1012
1028
 
1013
1029
 
1014
1030
  /**
1015
- * This component is responsible for actually handling the data request.
1016
- * It is wrapped by Data in order to support intercepts and be exported for use.
1031
+ * Hook to obtain a gqlFetch function for performing GraphQL requests.
1017
1032
  *
1018
- * INTERNAL USE ONLY
1033
+ * The fetch function will resolve null if the request was aborted, otherwise
1034
+ * it will resolve the data returned by the GraphQL server.
1019
1035
  */
1020
- class InternalData extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
1021
- constructor(props) {
1022
- super(props);
1023
- this.state = this._buildStateAndfulfillNeeds(props);
1024
- }
1025
-
1026
- componentDidMount() {
1027
- this._mounted = true;
1028
- }
1029
-
1030
- shouldComponentUpdate(nextProps, nextState) {
1031
- /**
1032
- * We only bother updating if our state changed.
1033
- *
1034
- * And we only update the state if props changed
1035
- * or we got new data/error.
1036
- */
1037
- if (!this._propsMatch(nextProps)) {
1038
- const newState = this._buildStateAndfulfillNeeds(nextProps);
1039
-
1040
- this.setState(newState);
1041
- }
1042
-
1043
- return this.state.loading !== nextState.loading || this.state.data !== nextState.data || this.state.error !== nextState.error;
1044
- }
1036
+ const useGql = () => {
1037
+ // This hook only works if the `GqlRouter` has been used to setup context.
1038
+ const gqlRouterContext = Object(react__WEBPACK_IMPORTED_MODULE_0__["useContext"])(_util_gql_router_context_js__WEBPACK_IMPORTED_MODULE_1__[/* GqlRouterContext */ "a"]);
1045
1039
 
1046
- componentWillUnmount() {
1047
- this._mounted = false;
1040
+ if (gqlRouterContext == null) {
1041
+ throw new _util_gql_error_js__WEBPACK_IMPORTED_MODULE_3__[/* GqlError */ "a"]("No GqlRouter", _util_gql_error_js__WEBPACK_IMPORTED_MODULE_3__[/* GqlErrors */ "b"].Internal);
1048
1042
  }
1049
1043
 
1050
- _propsMatch(otherProps) {
1051
- const {
1052
- handler,
1053
- options
1054
- } = this.props;
1055
- const {
1056
- handler: prevHandler,
1057
- options: prevOptions
1058
- } = otherProps;
1059
- return handler === prevHandler && handler.getKey(options) === prevHandler.getKey(prevOptions);
1060
- }
1044
+ const {
1045
+ fetch,
1046
+ defaultContext
1047
+ } = gqlRouterContext; // Let's memoize the gqlFetch function we create based off our context.
1048
+ // That way, even if the context happens to change, if its values don't
1049
+ // we give the same function instance back to our callers instead of
1050
+ // making a new one. That then means they can safely use the return value
1051
+ // in hooks deps without fear of it triggering extra renders.
1061
1052
 
1062
- _buildStateAndfulfillNeeds(propsAtFulfillment) {
1053
+ const gqlFetch = Object(react__WEBPACK_IMPORTED_MODULE_0__["useMemo"])(() => (operation, options = Object.freeze({})) => {
1063
1054
  const {
1064
- getEntry,
1065
- handler,
1066
- options
1067
- } = propsAtFulfillment;
1068
- const cachedData = getEntry(handler, options);
1069
-
1070
- if (!_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_1__["Server"].isServerSide() && (cachedData == null || handler.shouldRefreshCache(options, cachedData))) {
1071
- /**
1072
- * We're not on the server, the cache missed, or our handler says
1073
- * we should refresh the cache.
1074
- *
1075
- * Therefore, we need to request data.
1076
- *
1077
- * We have to do this here from the constructor so that this
1078
- * data request is tracked when performing server-side rendering.
1079
- */
1080
- _util_request_fulfillment_js__WEBPACK_IMPORTED_MODULE_2__[/* RequestFulfillment */ "a"].Default.fulfill(handler, options).then(cacheEntry => {
1081
- /**
1082
- * We get here, we should have updated the cache.
1083
- * However, we need to update the component, but we
1084
- * should only do that if the props are the same as they
1085
- * were when this was called.
1086
- */
1087
- if (this._mounted && this._propsMatch(propsAtFulfillment)) {
1088
- this.setState({
1089
- loading: false,
1090
- data: cacheEntry.data,
1091
- error: cacheEntry.error
1092
- });
1093
- }
1094
-
1055
+ variables,
1056
+ context
1057
+ } = options; // Invoke the fetch and extract the data.
1058
+
1059
+ return fetch(operation, variables, { ...defaultContext,
1060
+ ...context
1061
+ }).then(_util_get_gql_data_from_response_js__WEBPACK_IMPORTED_MODULE_2__[/* getGqlDataFromResponse */ "a"], error => {
1062
+ // Return null if the request was aborted.
1063
+ // The only way to detect this reliably, it seems, is to
1064
+ // check the error name and see if it's "AbortError" (this
1065
+ // is also what Apollo does).
1066
+ // Even then, it's reliant on the fetch supporting aborts.
1067
+ if (error.name === "AbortError") {
1095
1068
  return null;
1096
- }).catch(e => {
1097
- /**
1098
- * We should never get here, but if we do.
1099
- */
1100
- // eslint-disable-next-line no-console
1101
- console.error(`Unexpected error occurred during data fulfillment: ${e}`);
1102
-
1103
- if (this._mounted && this._propsMatch(propsAtFulfillment)) {
1104
- this.setState({
1105
- loading: false,
1106
- data: null,
1107
- error: typeof e === "string" ? e : e.message
1108
- });
1109
- }
1110
-
1111
- return null;
1112
- });
1113
- }
1114
- /**
1115
- * This is the default response for the server and for the initial
1116
- * client-side render if we have cachedData.
1117
- *
1118
- * This ensures we don't make promises we don't want when doing
1119
- * server-side rendering. Instead, we either have data from the cache
1120
- * or we don't.
1121
- */
1069
+ }
1070
+ });
1071
+ }, [fetch, defaultContext]);
1072
+ return gqlFetch;
1073
+ };
1122
1074
 
1075
+ /***/ }),
1076
+ /* 17 */
1077
+ /***/ (function(module, __webpack_exports__, __webpack_require__) {
1123
1078
 
1079
+ "use strict";
1080
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return resultFromCacheEntry; });
1081
+ /**
1082
+ * Turns a cache entry into a stateful result.
1083
+ */
1084
+ const resultFromCacheEntry = cacheEntry => {
1085
+ // No cache entry means we didn't load one yet.
1086
+ if (cacheEntry == null) {
1124
1087
  return {
1125
- loading: cachedData == null,
1126
- data: cachedData && cachedData.data,
1127
- error: cachedData && cachedData.error
1088
+ status: "loading"
1128
1089
  };
1129
1090
  }
1130
1091
 
1131
- _resultFromState() {
1132
- const {
1133
- loading,
1134
- data,
1135
- error
1136
- } = this.state;
1137
-
1138
- if (loading) {
1139
- return {
1140
- loading: true
1141
- };
1142
- }
1143
-
1144
- if (data != null) {
1145
- return {
1146
- loading: false,
1147
- data
1148
- };
1149
- }
1150
-
1151
- if (error == null) {
1152
- // We should never get here ever.
1153
- throw new Error("Loaded result has invalid state where data and error are missing");
1154
- }
1092
+ const {
1093
+ data,
1094
+ error
1095
+ } = cacheEntry;
1155
1096
 
1097
+ if (data != null) {
1156
1098
  return {
1157
- loading: false,
1158
- error
1099
+ status: "success",
1100
+ data
1159
1101
  };
1160
1102
  }
1161
1103
 
1162
- _renderContent(result) {
1163
- const {
1164
- children
1165
- } = this.props;
1166
- return children(result);
1104
+ if (error == null) {
1105
+ // We should never get here ever.
1106
+ return {
1107
+ status: "error",
1108
+ error: "Loaded result has invalid state where data and error are missing"
1109
+ };
1167
1110
  }
1168
1111
 
1169
- _renderWithTrackingContext(result) {
1170
- return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_util_request_tracking_js__WEBPACK_IMPORTED_MODULE_3__[/* TrackerContext */ "b"].Consumer, null, track => {
1171
- /**
1172
- * If data tracking wasn't enabled, don't do it.
1173
- */
1174
- if (track != null) {
1175
- track(this.props.handler, this.props.options);
1176
- }
1112
+ return {
1113
+ status: "error",
1114
+ error
1115
+ };
1116
+ };
1117
+
1118
+ /***/ }),
1119
+ /* 18 */
1120
+ /***/ (function(module, __webpack_exports__, __webpack_require__) {
1177
1121
 
1178
- return this._renderContent(result);
1122
+ "use strict";
1123
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return getGqlDataFromResponse; });
1124
+ /* harmony import */ var _gql_error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
1125
+
1126
+ /**
1127
+ * Validate a GQL operation response and extract the data.
1128
+ */
1129
+
1130
+ const getGqlDataFromResponse = async response => {
1131
+ // Get the response as text, that way we can use the text in error
1132
+ // messaging, should our parsing fail.
1133
+ const bodyText = await response.text();
1134
+ let result;
1135
+
1136
+ try {
1137
+ result = JSON.parse(bodyText);
1138
+ } catch (e) {
1139
+ throw new _gql_error_js__WEBPACK_IMPORTED_MODULE_0__[/* GqlError */ "a"]("Failed to parse response", _gql_error_js__WEBPACK_IMPORTED_MODULE_0__[/* GqlErrors */ "b"].Parse, {
1140
+ metadata: {
1141
+ statusCode: response.status,
1142
+ bodyText
1143
+ },
1144
+ cause: e
1179
1145
  });
1180
- }
1146
+ } // Check for a bad status code.
1181
1147
 
1182
- render() {
1183
- const result = this._resultFromState(); // We only track data requests when we are server-side and we don't
1184
- // already have a result. The existence of a result is indicated by the
1185
- // loading flag being false.
1186
1148
 
1149
+ if (response.status >= 300) {
1150
+ throw new _gql_error_js__WEBPACK_IMPORTED_MODULE_0__[/* GqlError */ "a"]("Response unsuccessful", _gql_error_js__WEBPACK_IMPORTED_MODULE_0__[/* GqlErrors */ "b"].Network, {
1151
+ metadata: {
1152
+ statusCode: response.status,
1153
+ result
1154
+ }
1155
+ });
1156
+ } // Check that we have a valid result payload.
1157
+
1158
+
1159
+ if ( // Flow shouldn't be warning about this.
1160
+ // $FlowIgnore[method-unbinding]
1161
+ !Object.prototype.hasOwnProperty.call(result, "data") && // Flow shouldn't be warning about this.
1162
+ // $FlowIgnore[method-unbinding]
1163
+ !Object.prototype.hasOwnProperty.call(result, "errors")) {
1164
+ throw new _gql_error_js__WEBPACK_IMPORTED_MODULE_0__[/* GqlError */ "a"]("Server response missing", _gql_error_js__WEBPACK_IMPORTED_MODULE_0__[/* GqlErrors */ "b"].BadResponse, {
1165
+ metadata: {
1166
+ statusCode: response.status,
1167
+ result
1168
+ }
1169
+ });
1170
+ } // If the response payload has errors, throw an error.
1187
1171
 
1188
- if (result.loading && _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_1__["Server"].isServerSide()) {
1189
- return this._renderWithTrackingContext(result);
1190
- }
1191
1172
 
1192
- return this._renderContent(result);
1193
- }
1173
+ if (result.errors != null && Array.isArray(result.errors) && result.errors.length > 0) {
1174
+ throw new _gql_error_js__WEBPACK_IMPORTED_MODULE_0__[/* GqlError */ "a"]("GraphQL errors", _gql_error_js__WEBPACK_IMPORTED_MODULE_0__[/* GqlErrors */ "b"].ErrorResult, {
1175
+ metadata: {
1176
+ statusCode: response.status,
1177
+ result
1178
+ }
1179
+ });
1180
+ } // We got here, so return the data.
1194
1181
 
1195
- }
1182
+
1183
+ return result.data;
1184
+ };
1196
1185
 
1197
1186
  /***/ }),
1198
- /* 14 */
1187
+ /* 19 */
1199
1188
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
1200
1189
 
1201
1190
  "use strict";
@@ -1207,25 +1196,33 @@ __webpack_require__.r(__webpack_exports__);
1207
1196
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "removeAllFromCache", function() { return removeAllFromCache; });
1208
1197
  /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
1209
1198
  /* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__);
1210
- /* harmony import */ var _util_response_cache_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1);
1211
- /* harmony import */ var _util_request_tracking_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3);
1212
- /* harmony import */ var _util_request_handler_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
1199
+ /* harmony import */ var _util_response_cache_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
1200
+ /* harmony import */ var _util_request_tracking_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
1201
+ /* harmony import */ var _util_request_handler_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11);
1213
1202
  /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "RequestHandler", function() { return _util_request_handler_js__WEBPACK_IMPORTED_MODULE_3__["a"]; });
1214
1203
 
1215
- /* harmony import */ var _components_track_data_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9);
1204
+ /* harmony import */ var _components_track_data_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(12);
1216
1205
  /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "TrackData", function() { return _components_track_data_js__WEBPACK_IMPORTED_MODULE_4__["a"]; });
1217
1206
 
1218
- /* harmony import */ var _components_data_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(10);
1207
+ /* harmony import */ var _components_data_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(13);
1219
1208
  /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Data", function() { return _components_data_js__WEBPACK_IMPORTED_MODULE_5__["a"]; });
1220
1209
 
1221
- /* harmony import */ var _components_intercept_data_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(11);
1210
+ /* harmony import */ var _components_intercept_data_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(14);
1222
1211
  /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "InterceptData", function() { return _components_intercept_data_js__WEBPACK_IMPORTED_MODULE_6__["a"]; });
1223
1212
 
1224
- /* harmony import */ var _components_intercept_cache_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(12);
1225
- /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "InterceptCache", function() { return _components_intercept_cache_js__WEBPACK_IMPORTED_MODULE_7__["a"]; });
1213
+ /* harmony import */ var _hooks_use_data_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(6);
1214
+ /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "useData", function() { return _hooks_use_data_js__WEBPACK_IMPORTED_MODULE_7__["a"]; });
1215
+
1216
+ /* harmony import */ var _components_gql_router_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(15);
1217
+ /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "GqlRouter", function() { return _components_gql_router_js__WEBPACK_IMPORTED_MODULE_8__["a"]; });
1218
+
1219
+ /* harmony import */ var _hooks_use_gql_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(16);
1220
+ /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "useGql", function() { return _hooks_use_gql_js__WEBPACK_IMPORTED_MODULE_9__["a"]; });
1221
+
1222
+ /* harmony import */ var _util_gql_error_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(1);
1223
+ /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "GqlErrors", function() { return _util_gql_error_js__WEBPACK_IMPORTED_MODULE_10__["b"]; });
1226
1224
 
1227
- /* harmony import */ var _util_no_cache_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(5);
1228
- /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "NoCache", function() { return _util_no_cache_js__WEBPACK_IMPORTED_MODULE_8__["a"]; });
1225
+ /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "GqlError", function() { return _util_gql_error_js__WEBPACK_IMPORTED_MODULE_10__["a"]; });
1229
1226
 
1230
1227
 
1231
1228
 
@@ -1247,15 +1244,12 @@ const hasUnfulfilledRequests = () => {
1247
1244
  };
1248
1245
  const removeFromCache = (handler, options) => _util_response_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* ResponseCache */ "a"].Default.remove(handler, options);
1249
1246
  const removeAllFromCache = (handler, predicate) => _util_response_cache_js__WEBPACK_IMPORTED_MODULE_1__[/* ResponseCache */ "a"].Default.removeAll(handler, predicate);
1250
- /**
1251
- * TODO(somewhatabstract): Export each cache type we implement.
1252
- *
1253
- * Is there a base type we export, like we do for RequestHandler?
1254
- */
1255
1247
 
1256
1248
 
1257
1249
 
1258
1250
 
1251
+ // GraphQL
1252
+
1259
1253
 
1260
1254
 
1261
1255