@khanacademy/wonder-blocks-data 2.3.4 → 3.0.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 (39) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/es/index.js +212 -446
  3. package/dist/index.js +230 -478
  4. package/docs.md +19 -13
  5. package/package.json +2 -3
  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__/intercept-data.test.js +9 -66
  10. package/src/components/__tests__/track-data.test.js +6 -5
  11. package/src/components/data.js +9 -117
  12. package/src/components/data.md +38 -60
  13. package/src/components/intercept-data.js +2 -34
  14. package/src/components/intercept-data.md +7 -105
  15. package/src/hooks/__tests__/use-data.test.js +790 -0
  16. package/src/hooks/use-data.js +138 -0
  17. package/src/index.js +1 -3
  18. package/src/util/__tests__/memory-cache.test.js +134 -35
  19. package/src/util/__tests__/request-fulfillment.test.js +21 -36
  20. package/src/util/__tests__/request-handler.test.js +30 -30
  21. package/src/util/__tests__/request-tracking.test.js +29 -30
  22. package/src/util/__tests__/response-cache.test.js +521 -561
  23. package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
  24. package/src/util/memory-cache.js +18 -14
  25. package/src/util/request-fulfillment.js +4 -0
  26. package/src/util/request-handler.js +2 -27
  27. package/src/util/request-handler.md +0 -32
  28. package/src/util/response-cache.js +50 -110
  29. package/src/util/result-from-cache-entry.js +38 -0
  30. package/src/util/types.js +14 -35
  31. package/LICENSE +0 -21
  32. package/src/components/__tests__/intercept-cache.test.js +0 -124
  33. package/src/components/__tests__/internal-data.test.js +0 -1030
  34. package/src/components/intercept-cache.js +0 -79
  35. package/src/components/intercept-cache.md +0 -103
  36. package/src/components/internal-data.js +0 -219
  37. package/src/util/__tests__/no-cache.test.js +0 -112
  38. package/src/util/no-cache.js +0 -67
  39. package/src/util/no-cache.md +0 -66
package/dist/es/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Server } from '@khanacademy/wonder-blocks-core';
2
- import { createContext, Component, createElement } from 'react';
2
+ import * as React from 'react';
3
+ import { useState, useContext, useRef, useEffect } from 'react';
3
4
  import _extends from '@babel/runtime/helpers/extends';
4
5
 
5
6
  function deepClone(source) {
@@ -19,9 +20,7 @@ function deepClone(source) {
19
20
  *
20
21
  * Special case cache implementation for the memory cache.
21
22
  *
22
- * This is only used within our framework. Handlers don't need to
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).
23
+ * This is only used within our framework for SSR (see ./response-cache.js).
25
24
  */
26
25
 
27
26
 
@@ -29,7 +28,7 @@ class MemoryCache {
29
28
  constructor(source = null) {
30
29
  this.store = (handler, options, entry) => {
31
30
  const requestType = handler.type;
32
- const frozenEntry = Object.isFrozen(entry) ? entry : Object.freeze(entry); // Ensure we have a cache location for this handler type.
31
+ const frozenEntry = Object.freeze(entry); // Ensure we have a cache location for this handler type.
33
32
 
34
33
  this._cache[requestType] = this._cache[requestType] || {}; // Cache the data.
35
34
 
@@ -89,16 +88,22 @@ class MemoryCache {
89
88
 
90
89
  if (!handlerCache) {
91
90
  return 0;
92
- } // Apply the predicate to what we have cached.
93
-
91
+ }
94
92
 
95
93
  let removedCount = 0;
96
94
 
97
- for (const [key, entry] of Object.entries(handlerCache)) {
98
- if (typeof predicate !== "function" || predicate(key, entry)) {
99
- removedCount++;
100
- delete handlerCache[key];
95
+ if (typeof predicate === "function") {
96
+ // Apply the predicate to what we have cached.
97
+ for (const [key, entry] of Object.entries(handlerCache)) {
98
+ if (predicate(key, entry)) {
99
+ removedCount++;
100
+ delete handlerCache[key];
101
+ }
101
102
  }
103
+ } else {
104
+ // We're removing everything so delete the entire subcache.
105
+ removedCount = Object.keys(handlerCache).length;
106
+ delete this._cache[requestType];
102
107
  }
103
108
 
104
109
  return removedCount;
@@ -128,6 +133,12 @@ class MemoryCache {
128
133
  }
129
134
  }
130
135
  }
136
+ /**
137
+ * Indicate if this cache is being used or now.
138
+ *
139
+ * When the cache has entries, returns `true`; otherwise, returns `false`.
140
+ */
141
+
131
142
 
132
143
  get inUse() {
133
144
  return Object.keys(this._cache).length > 0;
@@ -135,47 +146,11 @@ class MemoryCache {
135
146
 
136
147
  }
137
148
 
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
149
  /**
175
150
  * The default instance is stored here.
176
151
  * It's created below in the Default() static property.
177
152
  */
178
- let _default;
153
+ let _default$2;
179
154
  /**
180
155
  * Implements the response cache.
181
156
  *
@@ -185,31 +160,29 @@ let _default;
185
160
 
186
161
  class ResponseCache {
187
162
  static get Default() {
188
- if (!_default) {
189
- _default = new ResponseCache();
163
+ if (!_default$2) {
164
+ _default$2 = new ResponseCache();
190
165
  }
191
166
 
192
- return _default;
167
+ return _default$2;
193
168
  }
194
169
 
195
- constructor(memoryCache = null, ssrOnlyCache = null) {
170
+ constructor(hydrationCache = null, ssrOnlyCache = null) {
196
171
  this.initialize = source => {
197
- if (this._hydrationAndDefaultCache.inUse) {
172
+ if (this._hydrationCache.inUse) {
198
173
  throw new Error("Cannot initialize data response cache more than once");
199
174
  }
200
175
 
201
176
  try {
202
- this._hydrationAndDefaultCache = new MemoryCache(source);
177
+ this._hydrationCache = new MemoryCache(source);
203
178
  } catch (e) {
204
179
  throw new Error(`An error occurred trying to initialize the data response cache: ${e}`);
205
180
  }
206
181
  };
207
182
 
208
- this.cacheData = (handler, options, data) => {
209
- return this._setCacheEntry(handler, options, {
210
- data
211
- });
212
- };
183
+ this.cacheData = (handler, options, data) => this._setCacheEntry(handler, options, {
184
+ data
185
+ });
213
186
 
214
187
  this.cacheError = (handler, options, error) => {
215
188
  const errorMessage = typeof error === "string" ? error : error.message;
@@ -219,110 +192,64 @@ class ResponseCache {
219
192
  };
220
193
 
221
194
  this.getEntry = (handler, options) => {
222
- // If we're not server-side, and the handler has a custom cache
223
- // let's try to use it.
224
- if (this._ssrOnlyCache == null && handler.cache != null) {
225
- const entry = handler.cache.retrieve(handler, options);
226
-
227
- if (entry != null) {
228
- // Custom cache has an entry, so use it.
229
- return entry;
230
- }
231
- } // Get the internal entry for the handler.
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.
195
+ // Get the cached entry for this value.
196
+ // If the handler wants WB Data to hydrate (i.e. handler.hydrate is
197
+ // true), we use our hydration cache. Otherwise, if we're server-side
198
+ // we use our SSR-only cache. Otherwise, there's no entry to return.
199
+ const cache = handler.hydrate ? this._hydrationCache : Server.isServerSide() ? this._ssrOnlyCache : undefined;
200
+ const internalEntry = cache == null ? void 0 : cache.retrieve(handler, options); // If we are not server-side and we hydrated something, let's clear
201
+ // that from the hydration cache to save memory.
202
+
203
+ if (this._ssrOnlyCache == null && internalEntry != null) {
204
+ // We now delete this from our hydration cache as we don't need it.
246
205
  // This does mean that if another handler of the same type but
247
- // without a custom cache won't get the value, but that's not an
248
- // expected valid usage of this framework - two handlers with
249
- // different caching options shouldn't be using the same type name.
250
-
251
- this._hydrationAndDefaultCache.remove(handler, options);
206
+ // without some sort of linked cache won't get the value, but
207
+ // that's not an expected use-case. If two different places use the
208
+ // same handler and options (i.e. the same request), then the
209
+ // handler should cater to that to ensure they share the result.
210
+ this._hydrationCache.remove(handler, options);
252
211
  }
253
212
 
254
213
  return internalEntry;
255
214
  };
256
215
 
257
216
  this.remove = (handler, options) => {
217
+ var _this$_ssrOnlyCache$r, _this$_ssrOnlyCache;
218
+
258
219
  // NOTE(somewhatabstract): We could invoke removeAll with a predicate
259
220
  // to match the key of the entry we're removing, but that's an
260
221
  // inefficient way to remove a single item, so let's not do that.
261
- // If we're not server-side, and the handler has a custom cache
262
- // let's try to use it.
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;
222
+ // Delete the entry from the appropriate cache.
223
+ 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
224
  };
272
225
 
273
226
  this.removeAll = (handler, predicate) => {
274
- // If we're not server-side, and the handler has a custom cache
275
- // let's try to use it.
276
- const customCache = this._ssrOnlyCache == null ? handler.cache : null;
277
- const removedCountCustom = (customCache == null ? void 0 : customCache.removeAll(handler, predicate)) || 0; // Apply the predicate to what we have in our internal cached.
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;
227
+ var _this$_ssrOnlyCache$r2, _this$_ssrOnlyCache2;
228
+
229
+ // Apply the predicate to what we have in the appropriate cache.
230
+ 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
231
  };
291
232
 
292
233
  this.cloneHydratableData = () => {
293
234
  // We return our hydration cache only.
294
- return this._hydrationAndDefaultCache.cloneData();
235
+ return this._hydrationCache.cloneData();
295
236
  };
296
237
 
297
238
  this._ssrOnlyCache = Server.isServerSide() ? ssrOnlyCache || new MemoryCache() : undefined;
298
- this._hydrationAndDefaultCache = memoryCache || new MemoryCache();
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;
239
+ this._hydrationCache = hydrationCache || new MemoryCache();
313
240
  }
314
241
 
315
242
  _setCacheEntry(handler, options, entry) {
316
243
  const frozenEntry = Object.freeze(entry);
317
244
 
318
- if (this._ssrOnlyCache == null && handler.cache != null) {
319
- // We are not server-side, and our handler has its own cache,
320
- // so we use that to store values.
321
- handler.cache.store(handler, options, frozenEntry);
322
- } else {
323
- // We are either server-side, or our handler doesn't provide
324
- // a caching override.
325
- this._defaultCache(handler).store(handler, options, frozenEntry);
245
+ if (this._ssrOnlyCache != null) {
246
+ // We are server-side.
247
+ // We need to store this value.
248
+ if (handler.hydrate) {
249
+ this._hydrationCache.store(handler, options, frozenEntry);
250
+ } else {
251
+ this._ssrOnlyCache.store(handler, options, frozenEntry);
252
+ }
326
253
  }
327
254
 
328
255
  return frozenEntry;
@@ -386,6 +313,8 @@ class RequestFulfillment {
386
313
  delete handlerRequests[key];
387
314
  /**
388
315
  * Let's cache the data!
316
+ *
317
+ * NOTE: This only caches when we're server side.
389
318
  */
390
319
 
391
320
  return cacheData(handler, options, data);
@@ -393,6 +322,8 @@ class RequestFulfillment {
393
322
  delete handlerRequests[key];
394
323
  /**
395
324
  * Let's cache the error!
325
+ *
326
+ * NOTE: This only caches when we're server side.
396
327
  */
397
328
 
398
329
  return cacheError(handler, options, error);
@@ -418,13 +349,13 @@ class RequestFulfillment {
418
349
  *
419
350
  * INTERNAL USE ONLY
420
351
  */
421
- const TrackerContext = new createContext(null);
352
+ const TrackerContext = new React.createContext(null);
422
353
  /**
423
354
  * The default instance is stored here.
424
355
  * It's created below in the Default() static property.
425
356
  */
426
357
 
427
- let _default$2;
358
+ let _default;
428
359
  /**
429
360
  * Implements request tracking and fulfillment.
430
361
  *
@@ -434,11 +365,11 @@ let _default$2;
434
365
 
435
366
  class RequestTracker {
436
367
  static get Default() {
437
- if (!_default$2) {
438
- _default$2 = new RequestTracker();
368
+ if (!_default) {
369
+ _default = new RequestTracker();
439
370
  }
440
371
 
441
- return _default$2;
372
+ return _default;
442
373
  }
443
374
  /**
444
375
  * These are the caches for tracked requests, their handlers, and responses.
@@ -557,9 +488,8 @@ class RequestTracker {
557
488
  * use with the Wonder Blocks Data framework.
558
489
  */
559
490
  class RequestHandler {
560
- constructor(type, cache, hydrate = true) {
491
+ constructor(type, hydrate = true) {
561
492
  this._type = type;
562
- this._cache = cache || null;
563
493
  this._hydrate = !!hydrate;
564
494
  }
565
495
 
@@ -567,26 +497,10 @@ class RequestHandler {
567
497
  return this._type;
568
498
  }
569
499
 
570
- get cache() {
571
- return this._cache;
572
- }
573
-
574
500
  get hydrate() {
575
501
  return this._hydrate;
576
502
  }
577
503
 
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
504
  getKey(options) {
591
505
  try {
592
506
  return options === undefined ? "undefined" : JSON.stringify(options);
@@ -604,13 +518,13 @@ class RequestHandler {
604
518
  /**
605
519
  * Component to enable data request tracking when server-side rendering.
606
520
  */
607
- class TrackData extends Component {
521
+ class TrackData extends React.Component {
608
522
  render() {
609
523
  if (!Server.isServerSide()) {
610
524
  throw new Error("This component is not for use during client-side rendering");
611
525
  }
612
526
 
613
- return /*#__PURE__*/createElement(TrackerContext.Provider, {
527
+ return /*#__PURE__*/React.createElement(TrackerContext.Provider, {
614
528
  value: RequestTracker.Default.trackDataRequest
615
529
  }, this.props.children);
616
530
  }
@@ -618,286 +532,175 @@ class TrackData extends Component {
618
532
  }
619
533
 
620
534
  /**
621
- * This component is responsible for actually handling the data request.
622
- * It is wrapped by Data in order to support intercepts and be exported for use.
535
+ * InterceptContext defines a map from handler type to interception methods.
623
536
  *
624
537
  * INTERNAL USE ONLY
625
538
  */
626
- class InternalData extends Component {
627
- constructor(props) {
628
- super(props);
629
- this.state = this._buildStateAndfulfillNeeds(props);
630
- }
539
+ const InterceptContext = /*#__PURE__*/React.createContext({});
631
540
 
632
- componentDidMount() {
633
- this._mounted = true;
541
+ /**
542
+ * Turns a cache entry into a stateful result.
543
+ */
544
+ const resultFromCacheEntry = cacheEntry => {
545
+ // No cache entry means we didn't load one yet.
546
+ if (cacheEntry == null) {
547
+ return {
548
+ status: "loading"
549
+ };
634
550
  }
635
551
 
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
- }
552
+ const {
553
+ data,
554
+ error
555
+ } = cacheEntry;
648
556
 
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;
557
+ if (data != null) {
558
+ return {
559
+ status: "success",
560
+ data
561
+ };
654
562
  }
655
563
 
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);
564
+ if (error == null) {
565
+ // We should never get here ever.
566
+ return {
567
+ status: "error",
568
+ error: "Loaded result has invalid state where data and error are missing"
569
+ };
666
570
  }
667
571
 
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
- }
572
+ return {
573
+ status: "error",
574
+ error
575
+ };
576
+ };
716
577
 
717
- return null;
718
- });
578
+ const useData = (handler, options) => {
579
+ // If we're server-side or hydrating, we'll have a cached entry to use.
580
+ // So we get that and use it to initialize our state.
581
+ // This works in both hydration and SSR because the very first call to
582
+ // this will have cached data in those cases as it will be present on the
583
+ // initial render - and subsequent renders on the client it will be null.
584
+ const cachedResult = ResponseCache.Default.getEntry(handler, options);
585
+ const [result, setResult] = useState(cachedResult); // We only track data requests when we are server-side and we don't
586
+ // already have a result, as given by the cachedData (which is also the
587
+ // initial value for the result state).
588
+
589
+ const maybeTrack = useContext(TrackerContext);
590
+
591
+ if (result == null && Server.isServerSide()) {
592
+ maybeTrack == null ? void 0 : maybeTrack(handler, options);
593
+ } // Lookup to see if there's an interceptor for the handler.
594
+ // If we have one, we need to replace the handler with one that
595
+ // uses the interceptor.
596
+
597
+
598
+ const interceptorMap = useContext(InterceptContext);
599
+ const interceptor = interceptorMap[handler.type]; // We need to update our request when the handler changes or the key
600
+ // to the options change, so we keep track of those.
601
+ // However, even if we are hydrating from cache, we still need to make the
602
+ // request at least once, so we do not initialize these references.
603
+
604
+ const handlerRef = useRef();
605
+ const keyRef = useRef();
606
+ const interceptorRef = useRef(); // This effect will ensure that we fulfill the request as desired.
607
+
608
+ useEffect(() => {
609
+ // If we are server-side, then just skip the effect. We track requests
610
+ // during SSR and fulfill them outside of the React render cycle.
611
+ // NOTE: This shouldn't happen since effects would not run on the server
612
+ // but let's be defensive - I think it makes the code clearer.
613
+
614
+ /* istanbul ignore next */
615
+ if (Server.isServerSide()) {
616
+ return;
617
+ } // Update our refs to the current handler and key.
618
+
619
+
620
+ handlerRef.current = handler;
621
+ keyRef.current = handler.getKey(options);
622
+ interceptorRef.current = interceptor; // If we're not hydrating a result, we want to make sure we set our
623
+ // result to null so that we're in the loading state.
624
+
625
+ if (cachedResult == null) {
626
+ // Mark ourselves as loading.
627
+ setResult(null);
719
628
  }
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
629
 
630
+ const getMaybeInterceptedHandler = () => {
631
+ if (interceptor == null) {
632
+ return handler;
633
+ }
729
634
 
730
- return {
731
- loading: cachedData == null,
732
- data: cachedData && cachedData.data,
733
- error: cachedData && cachedData.error
734
- };
735
- }
736
-
737
- _resultFromState() {
738
- const {
739
- loading,
740
- data,
741
- error
742
- } = this.state;
635
+ const fulfillRequestFn = options => {
636
+ var _interceptor$fulfillR;
743
637
 
744
- if (loading) {
745
- return {
746
- loading: true
638
+ return (_interceptor$fulfillR = interceptor.fulfillRequest(options)) != null ? _interceptor$fulfillR : handler.fulfillRequest(options);
747
639
  };
748
- }
749
640
 
750
- if (data != null) {
751
641
  return {
752
- loading: false,
753
- data
642
+ fulfillRequest: fulfillRequestFn,
643
+ getKey: options => handler.getKey(options),
644
+ type: handler.type,
645
+ hydrate: handler.hydrate
754
646
  };
755
- }
647
+ }; // We aren't server-side, so let's make the request.
648
+ // The request handler is in control of whether that request actually
649
+ // happens or not.
756
650
 
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
- }
761
651
 
762
- return {
763
- loading: false,
764
- error
765
- };
766
- }
767
-
768
- _renderContent(result) {
769
- const {
770
- children
771
- } = this.props;
772
- return children(result);
773
- }
652
+ let cancel = false;
653
+ RequestFulfillment.Default.fulfill(getMaybeInterceptedHandler(), options).then(updateEntry => {
654
+ if (cancel) {
655
+ return;
656
+ }
774
657
 
775
- _renderWithTrackingContext(result) {
776
- return /*#__PURE__*/createElement(TrackerContext.Consumer, null, track => {
658
+ setResult(updateEntry);
659
+ return;
660
+ }).catch(e => {
661
+ if (cancel) {
662
+ return;
663
+ }
777
664
  /**
778
- * If data tracking wasn't enabled, don't do it.
665
+ * We should never get here as errors in fulfillment are part
666
+ * of the `then`, but if we do.
779
667
  */
780
- if (track != null) {
781
- track(this.props.handler, this.props.options);
782
- }
668
+ // eslint-disable-next-line no-console
783
669
 
784
- return this._renderContent(result);
785
- });
786
- }
787
-
788
- render() {
789
- const result = this._resultFromState(); // We only track data requests when we are server-side and we don't
790
- // already have a result. The existence of a result is indicated by the
791
- // loading flag being false.
792
670
 
793
-
794
- if (result.loading && Server.isServerSide()) {
795
- return this._renderWithTrackingContext(result);
796
- }
797
-
798
- return this._renderContent(result);
799
- }
800
-
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({});
671
+ console.error(`Unexpected error occurred during data fulfillment: ${e}`);
672
+ setResult({
673
+ data: null,
674
+ error: typeof e === "string" ? e : e.message
675
+ });
676
+ return;
677
+ });
678
+ return () => {
679
+ cancel = true;
680
+ }; // - handler.getKey is a proxy for options
681
+ // - We don't want to trigger on cachedResult changing, we're
682
+ // just using that as a flag for render state if the other things
683
+ // trigger this effect.
684
+ // eslint-disable-next-line react-hooks/exhaustive-deps
685
+ }, [handler, handler.getKey(options), interceptor]);
686
+ return resultFromCacheEntry(result);
687
+ };
809
688
 
810
689
  /**
811
690
  * This component is the main component of Wonder Blocks Data. With this, data
812
691
  * requirements can be placed in a React application in a manner that will
813
692
  * support server-side rendering and efficient caching.
814
693
  */
815
- class Data extends Component {
816
- _getHandlerFromInterceptor(interceptor) {
817
- const {
818
- handler
819
- } = this.props;
820
-
821
- if (!interceptor) {
822
- return handler;
823
- }
824
-
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
- return {
838
- fulfillRequest: fulfillRequestFn,
839
- shouldRefreshCache: shouldRefreshCacheFn,
840
- getKey: options => handler.getKey(options),
841
- type: handler.type,
842
- cache: handler.cache,
843
- hydrate: handler.hydrate
844
- };
845
- }
846
-
847
- _getCacheLookupFnFromInterceptor(interceptor) {
848
- const getEntry = interceptor && interceptor.getEntry;
849
-
850
- if (!getEntry) {
851
- return ResponseCache.Default.getEntry;
852
- }
853
-
854
- return (handler, options) => {
855
- // 1. Lookup the current cache value.
856
- const cacheEntry = ResponseCache.Default.getEntry(handler, options); // 2. See if our interceptor wants to override it.
857
-
858
- const interceptedData = getEntry(options, cacheEntry); // 3. Return the appropriate response.
859
-
860
- return interceptedData != null ? interceptedData : cacheEntry;
861
- };
862
- }
863
-
864
- render() {
865
- return /*#__PURE__*/createElement(InterceptContext.Consumer, null, value => {
866
- const handlerType = this.props.handler.type;
867
- const interceptor = value[handlerType];
868
-
869
- const handler = this._getHandlerFromInterceptor(interceptor);
870
-
871
- const getEntry = this._getCacheLookupFnFromInterceptor(interceptor);
872
- /**
873
- * Need to share our types with InternalData so Flow
874
- * doesn't need to infer them and find mismatches.
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.
879
- */
880
-
881
-
882
- return /*#__PURE__*/createElement(InternalData // $FlowIgnore[incompatible-type-arg]
883
- , {
884
- handler: handler,
885
- options: this.props.options,
886
- getEntry: getEntry
887
- }, result => this.props.children(result));
888
- });
889
- }
890
-
891
- }
694
+ const Data = props => {
695
+ const data = useData(props.handler, props.options);
696
+ return props.children(data);
697
+ };
892
698
 
893
699
  /**
894
700
  * This component provides a mechanism to intercept the data requests for the
895
701
  * type of a given handler and provide alternative results. This is mostly
896
702
  * useful for testing.
897
703
  *
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
704
  * This component is not recommended for use in production code as it
902
705
  * can prevent predictable functioning of the Wonder Blocks Data framework.
903
706
  * One possible side-effect is that inflight requests from the interceptor could
@@ -909,57 +712,20 @@ class Data extends Component {
909
712
  * new instance will replace this interceptor for its children. All methods
910
713
  * will be replaced.
911
714
  */
912
- class InterceptData extends Component {
913
- render() {
914
- return /*#__PURE__*/createElement(InterceptContext.Consumer, null, value => {
915
- const handlerType = this.props.handler.type;
916
-
917
- const interceptor = _extends({}, value[handlerType], {
918
- fulfillRequest: this.props.fulfillRequest || null,
919
- shouldRefreshCache: this.props.shouldRefreshCache || null
920
- });
921
-
922
- const newValue = _extends({}, value, {
923
- [handlerType]: interceptor
924
- });
925
-
926
- return /*#__PURE__*/createElement(InterceptContext.Provider, {
927
- value: newValue
928
- }, this.props.children);
929
- });
930
- }
931
-
932
- }
933
-
934
- /**
935
- * This component provides a mechanism to intercept cache lookups for the
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`.
941
- *
942
- * This component is generally not suitable for use in production code as it
943
- * can prevent predictable functioning of the Wonder Blocks Data framework.
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.
948
- */
949
- class InterceptCache extends Component {
715
+ class InterceptData extends React.Component {
950
716
  render() {
951
- return /*#__PURE__*/createElement(InterceptContext.Consumer, null, value => {
717
+ return /*#__PURE__*/React.createElement(InterceptContext.Consumer, null, value => {
952
718
  const handlerType = this.props.handler.type;
953
719
 
954
720
  const interceptor = _extends({}, value[handlerType], {
955
- getEntry: this.props.getEntry
721
+ fulfillRequest: this.props.fulfillRequest
956
722
  });
957
723
 
958
724
  const newValue = _extends({}, value, {
959
725
  [handlerType]: interceptor
960
726
  });
961
727
 
962
- return /*#__PURE__*/createElement(InterceptContext.Provider, {
728
+ return /*#__PURE__*/React.createElement(InterceptContext.Provider, {
963
729
  value: newValue
964
730
  }, this.props.children);
965
731
  });
@@ -985,4 +751,4 @@ const hasUnfulfilledRequests = () => {
985
751
  const removeFromCache = (handler, options) => ResponseCache.Default.remove(handler, options);
986
752
  const removeAllFromCache = (handler, predicate) => ResponseCache.Default.removeAll(handler, predicate);
987
753
 
988
- export { Data, InterceptCache, InterceptData, NoCache, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache };
754
+ export { Data, InterceptData, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache, useData };