@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.
- package/CHANGELOG.md +14 -0
- package/dist/es/index.js +210 -439
- package/dist/index.js +235 -478
- package/docs.md +19 -13
- package/package.json +6 -7
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +40 -160
- package/src/__tests__/generated-snapshot.test.js +15 -195
- package/src/components/__tests__/data.test.js +159 -965
- package/src/components/__tests__/intercept-data.test.js +9 -66
- package/src/components/__tests__/track-data.test.js +6 -5
- package/src/components/data.js +9 -119
- package/src/components/data.md +38 -60
- package/src/components/intercept-context.js +2 -3
- package/src/components/intercept-data.js +2 -34
- package/src/components/intercept-data.md +7 -105
- package/src/hooks/__tests__/use-data.test.js +826 -0
- package/src/hooks/use-data.js +143 -0
- package/src/index.js +1 -3
- package/src/util/__tests__/memory-cache.test.js +134 -35
- package/src/util/__tests__/request-fulfillment.test.js +21 -36
- package/src/util/__tests__/request-handler.test.js +30 -30
- package/src/util/__tests__/request-tracking.test.js +29 -30
- package/src/util/__tests__/response-cache.test.js +521 -561
- package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
- package/src/util/memory-cache.js +20 -15
- package/src/util/request-fulfillment.js +4 -0
- package/src/util/request-handler.js +4 -28
- package/src/util/request-handler.md +0 -32
- package/src/util/request-tracking.js +2 -3
- package/src/util/response-cache.js +50 -110
- package/src/util/result-from-cache-entry.js +38 -0
- package/src/util/types.js +14 -35
- package/LICENSE +0 -21
- package/src/components/__tests__/intercept-cache.test.js +0 -124
- package/src/components/__tests__/internal-data.test.js +0 -1030
- package/src/components/intercept-cache.js +0 -79
- package/src/components/intercept-cache.md +0 -103
- package/src/components/internal-data.js +0 -219
- package/src/util/__tests__/no-cache.test.js +0 -112
- package/src/util/no-cache.js +0 -66
- package/src/util/no-cache.md +0 -66
package/dist/es/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Server } from '@khanacademy/wonder-blocks-core';
|
|
2
|
-
import
|
|
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
|
|
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.
|
|
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
|
-
}
|
|
93
|
-
|
|
91
|
+
}
|
|
94
92
|
|
|
95
93
|
let removedCount = 0;
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
170
|
+
constructor(hydrationCache = null, ssrOnlyCache = null) {
|
|
196
171
|
this.initialize = source => {
|
|
197
|
-
if (this.
|
|
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.
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// This allows us to use our hydrated cache during hydration.
|
|
233
|
-
// If we just returned null when the custom cache didn't have it,
|
|
234
|
-
// we would never hydrate properly.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const internalEntry = this._defaultCache(handler).retrieve(handler, options); // If we are not server-side and we hydrated something that the custom
|
|
238
|
-
// cache didn't have, we need to make sure the custom cache contains
|
|
239
|
-
// that value.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (this._ssrOnlyCache == null && handler.cache != null && internalEntry != null) {
|
|
243
|
-
// Yes, if this throws, we will have a problem. We want that.
|
|
244
|
-
// Bad cache implementations should be overt.
|
|
245
|
-
handler.cache.store(handler, options, internalEntry); // We now delete this from our in-memory cache as we don't need it.
|
|
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
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
this.
|
|
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
|
-
//
|
|
262
|
-
|
|
263
|
-
const customCache = this._ssrOnlyCache == null ? handler.cache : null;
|
|
264
|
-
const removedCustom = !!(customCache != null && customCache.remove(handler, options)); // Delete the entry from our internal cache.
|
|
265
|
-
// Even if we have a custom cache, we want to make sure we still
|
|
266
|
-
// removed the same value from internal cache since this could be
|
|
267
|
-
// getting called before hydration for some complex advanced usage
|
|
268
|
-
// reason.
|
|
269
|
-
|
|
270
|
-
return this._defaultCache(handler).remove(handler, options) || removedCustom;
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// Even if we have a custom cache, we want to make sure we still
|
|
279
|
-
// removed the same value from internal cache since this could be
|
|
280
|
-
// getting called before hydration for some complex advanced usage
|
|
281
|
-
// reason.
|
|
282
|
-
|
|
283
|
-
const removedCount = this._defaultCache(handler).removeAll(handler, predicate); // We have no idea which keys were removed from which caches,
|
|
284
|
-
// so we can't dedupe the remove counts based on keys.
|
|
285
|
-
// That's why we return the total records deleted rather than the
|
|
286
|
-
// total keys deleted.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return removedCount + removedCountCustom;
|
|
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.
|
|
235
|
+
return this._hydrationCache.cloneData();
|
|
295
236
|
};
|
|
296
237
|
|
|
297
238
|
this._ssrOnlyCache = Server.isServerSide() ? ssrOnlyCache || new MemoryCache() : undefined;
|
|
298
|
-
this.
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Returns the default cache to use for the given handler.
|
|
302
|
-
*/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
_defaultCache(handler) {
|
|
306
|
-
if (handler.hydrate) {
|
|
307
|
-
return this._hydrationAndDefaultCache;
|
|
308
|
-
} // If the handler doesn't want to hydrate, we return the SSR-only cache.
|
|
309
|
-
// If we are client-side, we return our non-caching implementation.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return this._ssrOnlyCache || NoCache.Default;
|
|
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
|
|
319
|
-
// We are
|
|
320
|
-
//
|
|
321
|
-
handler.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
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
|
|
438
|
-
_default
|
|
368
|
+
if (!_default) {
|
|
369
|
+
_default = new RequestTracker();
|
|
439
370
|
}
|
|
440
371
|
|
|
441
|
-
return _default
|
|
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,
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
732
|
-
data: cachedData && cachedData.data,
|
|
733
|
-
error: cachedData && cachedData.error
|
|
548
|
+
status: "loading"
|
|
734
549
|
};
|
|
735
550
|
}
|
|
736
551
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
error
|
|
742
|
-
} = this.state;
|
|
743
|
-
|
|
744
|
-
if (loading) {
|
|
745
|
-
return {
|
|
746
|
-
loading: true
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (data != null) {
|
|
751
|
-
return {
|
|
752
|
-
loading: false,
|
|
753
|
-
data
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (error == null) {
|
|
758
|
-
// We should never get here ever.
|
|
759
|
-
throw new Error("Loaded result has invalid state where data and error are missing");
|
|
760
|
-
}
|
|
552
|
+
const {
|
|
553
|
+
data,
|
|
554
|
+
error
|
|
555
|
+
} = cacheEntry;
|
|
761
556
|
|
|
557
|
+
if (data != null) {
|
|
762
558
|
return {
|
|
763
|
-
|
|
764
|
-
|
|
559
|
+
status: "success",
|
|
560
|
+
data
|
|
765
561
|
};
|
|
766
562
|
}
|
|
767
563
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
_renderWithTrackingContext(result) {
|
|
776
|
-
return /*#__PURE__*/createElement(TrackerContext.Consumer, null, track => {
|
|
777
|
-
/**
|
|
778
|
-
* If data tracking wasn't enabled, don't do it.
|
|
779
|
-
*/
|
|
780
|
-
if (track != null) {
|
|
781
|
-
track(this.props.handler, this.props.options);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
return this._renderContent(result);
|
|
785
|
-
});
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
572
|
+
return {
|
|
573
|
+
status: "error",
|
|
574
|
+
error
|
|
575
|
+
};
|
|
576
|
+
};
|
|
793
577
|
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
|
|
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
|
-
|
|
851
|
-
return ResponseCache.Default.getEntry;
|
|
852
|
-
}
|
|
618
|
+
const maybeTrack = useContext(TrackerContext);
|
|
853
619
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
874
|
-
*
|
|
875
|
-
* However, just deriving a new component creates issues
|
|
876
|
-
* where InternalData starts rerendering too often.
|
|
877
|
-
* Couldn't track down why, so suppressing the error
|
|
878
|
-
* instead.
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
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,
|
|
759
|
+
export { Data, InterceptData, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache, useData };
|