@khanacademy/wonder-blocks-data 2.3.2 → 3.0.1

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 (41) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/es/index.js +210 -439
  3. package/dist/index.js +235 -478
  4. package/docs.md +19 -13
  5. package/package.json +6 -7
  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 -119
  12. package/src/components/data.md +38 -60
  13. package/src/components/intercept-context.js +2 -3
  14. package/src/components/intercept-data.js +2 -34
  15. package/src/components/intercept-data.md +7 -105
  16. package/src/hooks/__tests__/use-data.test.js +826 -0
  17. package/src/hooks/use-data.js +143 -0
  18. package/src/index.js +1 -3
  19. package/src/util/__tests__/memory-cache.test.js +134 -35
  20. package/src/util/__tests__/request-fulfillment.test.js +21 -36
  21. package/src/util/__tests__/request-handler.test.js +30 -30
  22. package/src/util/__tests__/request-tracking.test.js +29 -30
  23. package/src/util/__tests__/response-cache.test.js +521 -561
  24. package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
  25. package/src/util/memory-cache.js +20 -15
  26. package/src/util/request-fulfillment.js +4 -0
  27. package/src/util/request-handler.js +4 -28
  28. package/src/util/request-handler.md +0 -32
  29. package/src/util/request-tracking.js +2 -3
  30. package/src/util/response-cache.js +50 -110
  31. package/src/util/result-from-cache-entry.js +38 -0
  32. package/src/util/types.js +14 -35
  33. package/LICENSE +0 -21
  34. package/src/components/__tests__/intercept-cache.test.js +0 -124
  35. package/src/components/__tests__/internal-data.test.js +0 -1030
  36. package/src/components/intercept-cache.js +0 -79
  37. package/src/components/intercept-cache.md +0 -103
  38. package/src/components/internal-data.js +0 -219
  39. package/src/util/__tests__/no-cache.test.js +0 -112
  40. package/src/util/no-cache.js +0 -66
  41. 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,180 @@ 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
- }
631
-
632
- componentDidMount() {
633
- this._mounted = true;
634
- }
635
-
636
- shouldComponentUpdate(nextProps, nextState) {
637
- /**
638
- * We only bother updating if our state changed.
639
- *
640
- * And we only update the state if props changed
641
- * or we got new data/error.
642
- */
643
- if (!this._propsMatch(nextProps)) {
644
- const newState = this._buildStateAndfulfillNeeds(nextProps);
645
-
646
- this.setState(newState);
647
- }
648
-
649
- return this.state.loading !== nextState.loading || this.state.data !== nextState.data || this.state.error !== nextState.error;
650
- }
651
-
652
- componentWillUnmount() {
653
- this._mounted = false;
654
- }
655
-
656
- _propsMatch(otherProps) {
657
- const {
658
- handler,
659
- options
660
- } = this.props;
661
- const {
662
- handler: prevHandler,
663
- options: prevOptions
664
- } = otherProps;
665
- return handler === prevHandler && handler.getKey(options) === prevHandler.getKey(prevOptions);
666
- }
667
-
668
- _buildStateAndfulfillNeeds(propsAtFulfillment) {
669
- const {
670
- getEntry,
671
- handler,
672
- options
673
- } = propsAtFulfillment;
674
- const cachedData = getEntry(handler, options);
675
-
676
- if (!Server.isServerSide() && (cachedData == null || handler.shouldRefreshCache(options, cachedData))) {
677
- /**
678
- * We're not on the server, the cache missed, or our handler says
679
- * we should refresh the cache.
680
- *
681
- * Therefore, we need to request data.
682
- *
683
- * We have to do this here from the constructor so that this
684
- * data request is tracked when performing server-side rendering.
685
- */
686
- RequestFulfillment.Default.fulfill(handler, options).then(cacheEntry => {
687
- /**
688
- * We get here, we should have updated the cache.
689
- * However, we need to update the component, but we
690
- * should only do that if the props are the same as they
691
- * were when this was called.
692
- */
693
- if (this._mounted && this._propsMatch(propsAtFulfillment)) {
694
- this.setState({
695
- loading: false,
696
- data: cacheEntry.data,
697
- error: cacheEntry.error
698
- });
699
- }
700
-
701
- return null;
702
- }).catch(e => {
703
- /**
704
- * We should never get here, but if we do.
705
- */
706
- // eslint-disable-next-line no-console
707
- console.error(`Unexpected error occurred during data fulfillment: ${e}`);
708
-
709
- if (this._mounted && this._propsMatch(propsAtFulfillment)) {
710
- this.setState({
711
- loading: false,
712
- data: null,
713
- error: typeof e === "string" ? e : e.message
714
- });
715
- }
716
-
717
- return null;
718
- });
719
- }
720
- /**
721
- * This is the default response for the server and for the initial
722
- * client-side render if we have cachedData.
723
- *
724
- * This ensures we don't make promises we don't want when doing
725
- * server-side rendering. Instead, we either have data from the cache
726
- * or we don't.
727
- */
728
-
539
+ const InterceptContext = /*#__PURE__*/React.createContext({});
729
540
 
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) {
730
547
  return {
731
- loading: cachedData == null,
732
- data: cachedData && cachedData.data,
733
- error: cachedData && cachedData.error
548
+ status: "loading"
734
549
  };
735
550
  }
736
551
 
737
- _resultFromState() {
738
- const {
739
- loading,
740
- data,
741
- error
742
- } = this.state;
743
-
744
- if (loading) {
745
- return {
746
- loading: true
747
- };
748
- }
749
-
750
- if (data != null) {
751
- return {
752
- loading: false,
753
- data
754
- };
755
- }
756
-
757
- if (error == null) {
758
- // We should never get here ever.
759
- throw new Error("Loaded result has invalid state where data and error are missing");
760
- }
552
+ const {
553
+ data,
554
+ error
555
+ } = cacheEntry;
761
556
 
557
+ if (data != null) {
762
558
  return {
763
- loading: false,
764
- error
559
+ status: "success",
560
+ data
765
561
  };
766
562
  }
767
563
 
768
- _renderContent(result) {
769
- const {
770
- children
771
- } = this.props;
772
- return children(result);
773
- }
774
-
775
- _renderWithTrackingContext(result) {
776
- return /*#__PURE__*/createElement(TrackerContext.Consumer, null, track => {
777
- /**
778
- * If data tracking wasn't enabled, don't do it.
779
- */
780
- if (track != null) {
781
- track(this.props.handler, this.props.options);
782
- }
783
-
784
- return this._renderContent(result);
785
- });
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
+ };
786
570
  }
787
571
 
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
-
572
+ return {
573
+ status: "error",
574
+ error
575
+ };
576
+ };
793
577
 
794
- if (result.loading && Server.isServerSide()) {
795
- return this._renderWithTrackingContext(result);
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); // Lookup to see if there's an interceptor for the handler.
586
+ // If we have one, we need to replace the handler with one that
587
+ // uses the interceptor.
588
+
589
+ const interceptorMap = useContext(InterceptContext);
590
+ const interceptor = interceptorMap[handler.type]; // If we have an interceptor, we need to replace the handler with one that
591
+ // uses the interceptor. This helper function generates a new handler.
592
+ // We need this before we track the request as we want the interceptor
593
+ // to also work for tracked requests to simplify testing the server-side
594
+ // request fulfillment.
595
+
596
+ const getMaybeInterceptedHandler = () => {
597
+ if (interceptor == null) {
598
+ return handler;
796
599
  }
797
600
 
798
- return this._renderContent(result);
799
- }
601
+ const fulfillRequestFn = options => {
602
+ var _interceptor$fulfillR;
800
603
 
801
- }
802
-
803
- /**
804
- * InterceptContext defines a map from handler type to interception methods.
805
- *
806
- * INTERNAL USE ONLY
807
- */
808
- const InterceptContext = /*#__PURE__*/createContext({});
809
-
810
- /**
811
- * This component is the main component of Wonder Blocks Data. With this, data
812
- * requirements can be placed in a React application in a manner that will
813
- * support server-side rendering and efficient caching.
814
- */
815
- class Data extends Component {
816
- _getHandlerFromInterceptor(interceptor) {
817
- const {
818
- handler
819
- } = this.props;
820
-
821
- if (!interceptor) {
822
- return handler;
823
- }
604
+ return (_interceptor$fulfillR = interceptor.fulfillRequest(options)) != null ? _interceptor$fulfillR : handler.fulfillRequest(options);
605
+ };
824
606
 
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
607
  return {
838
608
  fulfillRequest: fulfillRequestFn,
839
- shouldRefreshCache: shouldRefreshCacheFn,
840
609
  getKey: options => handler.getKey(options),
841
610
  type: handler.type,
842
- cache: handler.cache,
843
611
  hydrate: handler.hydrate
844
612
  };
845
- }
613
+ }; // We only track data requests when we are server-side and we don't
614
+ // already have a result, as given by the cachedData (which is also the
615
+ // initial value for the result state).
846
616
 
847
- _getCacheLookupFnFromInterceptor(interceptor) {
848
- const getEntry = interceptor && interceptor.getEntry;
849
617
 
850
- if (!getEntry) {
851
- return ResponseCache.Default.getEntry;
852
- }
618
+ const maybeTrack = useContext(TrackerContext);
853
619
 
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.
620
+ if (result == null && Server.isServerSide()) {
621
+ maybeTrack == null ? void 0 : maybeTrack(getMaybeInterceptedHandler(), options);
622
+ } // We need to update our request when the handler changes or the key
623
+ // to the options change, so we keep track of those.
624
+ // However, even if we are hydrating from cache, we still need to make the
625
+ // request at least once, so we do not initialize these references.
857
626
 
858
- const interceptedData = getEntry(options, cacheEntry); // 3. Return the appropriate response.
859
627
 
860
- return interceptedData != null ? interceptedData : cacheEntry;
861
- };
862
- }
628
+ const handlerRef = useRef();
629
+ const keyRef = useRef();
630
+ const interceptorRef = useRef(); // This effect will ensure that we fulfill the request as desired.
631
+
632
+ useEffect(() => {
633
+ // If we are server-side, then just skip the effect. We track requests
634
+ // during SSR and fulfill them outside of the React render cycle.
635
+ // NOTE: This shouldn't happen since effects would not run on the server
636
+ // but let's be defensive - I think it makes the code clearer.
637
+
638
+ /* istanbul ignore next */
639
+ if (Server.isServerSide()) {
640
+ return;
641
+ } // Update our refs to the current handler and key.
863
642
 
864
- render() {
865
- return /*#__PURE__*/createElement(InterceptContext.Consumer, null, value => {
866
- const handlerType = this.props.handler.type;
867
- const interceptor = value[handlerType];
868
643
 
869
- const handler = this._getHandlerFromInterceptor(interceptor);
644
+ handlerRef.current = handler;
645
+ keyRef.current = handler.getKey(options);
646
+ interceptorRef.current = interceptor; // If we're not hydrating a result, we want to make sure we set our
647
+ // result to null so that we're in the loading state.
870
648
 
871
- const getEntry = this._getCacheLookupFnFromInterceptor(interceptor);
649
+ if (cachedResult == null) {
650
+ // Mark ourselves as loading.
651
+ setResult(null);
652
+ } // We aren't server-side, so let's make the request.
653
+ // The request handler is in control of whether that request actually
654
+ // happens or not.
655
+
656
+
657
+ let cancel = false;
658
+ RequestFulfillment.Default.fulfill(getMaybeInterceptedHandler(), options).then(updateEntry => {
659
+ if (cancel) {
660
+ return;
661
+ }
662
+
663
+ setResult(updateEntry);
664
+ return;
665
+ }).catch(e => {
666
+ if (cancel) {
667
+ return;
668
+ }
872
669
  /**
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.
670
+ * We should never get here as errors in fulfillment are part
671
+ * of the `then`, but if we do.
879
672
  */
673
+ // eslint-disable-next-line no-console
880
674
 
881
675
 
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));
676
+ console.error(`Unexpected error occurred during data fulfillment: ${e}`);
677
+ setResult({
678
+ data: null,
679
+ error: typeof e === "string" ? e : e.message
680
+ });
681
+ return;
888
682
  });
889
- }
683
+ return () => {
684
+ cancel = true;
685
+ }; // - handler.getKey is a proxy for options
686
+ // - We don't want to trigger on cachedResult changing, we're
687
+ // just using that as a flag for render state if the other things
688
+ // trigger this effect.
689
+ // eslint-disable-next-line react-hooks/exhaustive-deps
690
+ }, [handler, handler.getKey(options), interceptor]);
691
+ return resultFromCacheEntry(result);
692
+ };
890
693
 
891
- }
694
+ /**
695
+ * This component is the main component of Wonder Blocks Data. With this, data
696
+ * requirements can be placed in a React application in a manner that will
697
+ * support server-side rendering and efficient caching.
698
+ */
699
+ const Data = props => {
700
+ const data = useData(props.handler, props.options);
701
+ return props.children(data);
702
+ };
892
703
 
893
704
  /**
894
705
  * This component provides a mechanism to intercept the data requests for the
895
706
  * type of a given handler and provide alternative results. This is mostly
896
707
  * useful for testing.
897
708
  *
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
709
  * This component is not recommended for use in production code as it
902
710
  * can prevent predictable functioning of the Wonder Blocks Data framework.
903
711
  * One possible side-effect is that inflight requests from the interceptor could
@@ -909,57 +717,20 @@ class Data extends Component {
909
717
  * new instance will replace this interceptor for its children. All methods
910
718
  * will be replaced.
911
719
  */
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 {
720
+ class InterceptData extends React.Component {
950
721
  render() {
951
- return /*#__PURE__*/createElement(InterceptContext.Consumer, null, value => {
722
+ return /*#__PURE__*/React.createElement(InterceptContext.Consumer, null, value => {
952
723
  const handlerType = this.props.handler.type;
953
724
 
954
725
  const interceptor = _extends({}, value[handlerType], {
955
- getEntry: this.props.getEntry
726
+ fulfillRequest: this.props.fulfillRequest
956
727
  });
957
728
 
958
729
  const newValue = _extends({}, value, {
959
730
  [handlerType]: interceptor
960
731
  });
961
732
 
962
- return /*#__PURE__*/createElement(InterceptContext.Provider, {
733
+ return /*#__PURE__*/React.createElement(InterceptContext.Provider, {
963
734
  value: newValue
964
735
  }, this.props.children);
965
736
  });
@@ -985,4 +756,4 @@ const hasUnfulfilledRequests = () => {
985
756
  const removeFromCache = (handler, options) => ResponseCache.Default.remove(handler, options);
986
757
  const removeAllFromCache = (handler, predicate) => ResponseCache.Default.removeAll(handler, predicate);
987
758
 
988
- export { Data, InterceptCache, InterceptData, NoCache, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache };
759
+ export { Data, InterceptData, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache, useData };