@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
@@ -0,0 +1,68 @@
1
+ // @flow
2
+ import {resultFromCacheEntry} from "../result-from-cache-entry.js";
3
+
4
+ describe("#resultFromCacheEntry", () => {
5
+ it("should return loading status if cache entry is null", () => {
6
+ // Arrange
7
+ const cacheEntry = null;
8
+
9
+ // Act
10
+ const result = resultFromCacheEntry(cacheEntry);
11
+
12
+ // Assert
13
+ expect(result).toStrictEqual({
14
+ status: "loading",
15
+ });
16
+ });
17
+
18
+ it("should return success status if cache entry has data", () => {
19
+ // Arrange
20
+ const cacheEntry = {
21
+ data: "DATA",
22
+ error: null,
23
+ };
24
+
25
+ // Act
26
+ const result = resultFromCacheEntry(cacheEntry);
27
+
28
+ // Assert
29
+ expect(result).toStrictEqual({
30
+ status: "success",
31
+ data: "DATA",
32
+ });
33
+ });
34
+
35
+ it("should return error status if cache entry has no data and no error", () => {
36
+ // Arrange
37
+ const cacheEntry: any = {
38
+ data: null,
39
+ error: null,
40
+ };
41
+
42
+ // Act
43
+ const result = resultFromCacheEntry(cacheEntry);
44
+
45
+ // Assert
46
+ expect(result).toStrictEqual({
47
+ status: "error",
48
+ error: "Loaded result has invalid state where data and error are missing",
49
+ });
50
+ });
51
+
52
+ it("should return error status if cache entry has error", () => {
53
+ // Arrange
54
+ const cacheEntry: any = {
55
+ data: null,
56
+ error: "ERROR",
57
+ };
58
+
59
+ // Act
60
+ const result = resultFromCacheEntry(cacheEntry);
61
+
62
+ // Assert
63
+ expect(result).toStrictEqual({
64
+ status: "error",
65
+ error: "ERROR",
66
+ });
67
+ });
68
+ });
@@ -25,12 +25,11 @@ function deepClone<T: {...}>(source: T | $ReadOnly<T>): $ReadOnly<T> {
25
25
  *
26
26
  * Special case cache implementation for the memory cache.
27
27
  *
28
- * This is only used within our framework. Handlers don't need to
29
- * provide this as a custom cache as the framework will default to this in the
30
- * absence of a custom cache. We use this for SSR too (see ./response-cache.js).
28
+ * This is only used within our framework for SSR (see ./response-cache.js).
31
29
  */
32
30
  export default class MemoryCache<TOptions, TData: ValidData>
33
- implements ICache<TOptions, TData> {
31
+ implements ICache<TOptions, TData>
32
+ {
34
33
  _cache: Cache;
35
34
 
36
35
  constructor(source: ?$ReadOnly<Cache> = null) {
@@ -52,6 +51,11 @@ export default class MemoryCache<TOptions, TData: ValidData>
52
51
  }
53
52
  }
54
53
 
54
+ /**
55
+ * Indicate if this cache is being used or now.
56
+ *
57
+ * When the cache has entries, returns `true`; otherwise, returns `false`.
58
+ */
55
59
  get inUse(): boolean {
56
60
  return Object.keys(this._cache).length > 0;
57
61
  }
@@ -67,9 +71,7 @@ export default class MemoryCache<TOptions, TData: ValidData>
67
71
  ): void => {
68
72
  const requestType = handler.type;
69
73
 
70
- const frozenEntry = Object.isFrozen(entry)
71
- ? entry
72
- : Object.freeze(entry);
74
+ const frozenEntry = Object.freeze(entry);
73
75
 
74
76
  // Ensure we have a cache location for this handler type.
75
77
  this._cache[requestType] = this._cache[requestType] || {};
@@ -156,16 +158,19 @@ export default class MemoryCache<TOptions, TData: ValidData>
156
158
  return 0;
157
159
  }
158
160
 
159
- // Apply the predicate to what we have cached.
160
161
  let removedCount = 0;
161
- for (const [key, entry] of Object.entries(handlerCache)) {
162
- if (
163
- typeof predicate !== "function" ||
164
- predicate(key, (entry: any))
165
- ) {
166
- removedCount++;
167
- delete handlerCache[key];
162
+ if (typeof predicate === "function") {
163
+ // Apply the predicate to what we have cached.
164
+ for (const [key, entry] of Object.entries(handlerCache)) {
165
+ if (predicate(key, (entry: any))) {
166
+ removedCount++;
167
+ delete handlerCache[key];
168
+ }
168
169
  }
170
+ } else {
171
+ // We're removing everything so delete the entire subcache.
172
+ removedCount = Object.keys(handlerCache).length;
173
+ delete this._cache[requestType];
169
174
  }
170
175
  return removedCount;
171
176
  };
@@ -76,6 +76,8 @@ export class RequestFulfillment {
76
76
  delete handlerRequests[key];
77
77
  /**
78
78
  * Let's cache the data!
79
+ *
80
+ * NOTE: This only caches when we're server side.
79
81
  */
80
82
  return cacheData<TOptions, TData>(handler, options, data);
81
83
  })
@@ -83,6 +85,8 @@ export class RequestFulfillment {
83
85
  delete handlerRequests[key];
84
86
  /**
85
87
  * Let's cache the error!
88
+ *
89
+ * NOTE: This only caches when we're server side.
86
90
  */
87
91
  return cacheError<TOptions, TData>(handler, options, error);
88
92
  });
@@ -1,5 +1,5 @@
1
1
  // @flow
2
- import type {ValidData, CacheEntry, IRequestHandler, ICache} from "./types.js";
2
+ import type {ValidData, IRequestHandler} from "./types.js";
3
3
 
4
4
  /**
5
5
  * Base implementation for creating a request handler.
@@ -8,18 +8,13 @@ import type {ValidData, CacheEntry, IRequestHandler, ICache} from "./types.js";
8
8
  * use with the Wonder Blocks Data framework.
9
9
  */
10
10
  export default class RequestHandler<TOptions, TData: ValidData>
11
- implements IRequestHandler<TOptions, TData> {
11
+ implements IRequestHandler<TOptions, TData>
12
+ {
12
13
  _type: string;
13
- _cache: ?ICache<TOptions, TData>;
14
14
  _hydrate: boolean;
15
15
 
16
- constructor(
17
- type: string,
18
- cache?: ICache<TOptions, TData>,
19
- hydrate?: boolean = true,
20
- ) {
16
+ constructor(type: string, hydrate?: boolean = true) {
21
17
  this._type = type;
22
- this._cache = cache || null;
23
18
  this._hydrate = !!hydrate;
24
19
  }
25
20
 
@@ -27,29 +22,10 @@ export default class RequestHandler<TOptions, TData: ValidData>
27
22
  return this._type;
28
23
  }
29
24
 
30
- get cache(): ?ICache<TOptions, TData> {
31
- return this._cache;
32
- }
33
-
34
25
  get hydrate(): boolean {
35
26
  return this._hydrate;
36
27
  }
37
28
 
38
- shouldRefreshCache(
39
- options: TOptions,
40
- cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
41
- ): boolean {
42
- /**
43
- * By default, the cache needs a refresh if the current entry is an
44
- * error.
45
- *
46
- * This means that an error will cause a re-request on render.
47
- * Useful if the server rendered an error, as it means the client
48
- * will update after rehydration.
49
- */
50
- return cachedEntry == null || cachedEntry.error != null;
51
- }
52
-
53
29
  getKey(options: TOptions): string {
54
30
  try {
55
31
  return options === undefined
@@ -14,12 +14,6 @@ interface IRequestHandler<TOptions, TData> {
14
14
  */
15
15
  get type(): string;
16
16
 
17
- /**
18
- * A custom cache to use with data that this handler requests.
19
- * This only affects client-side caching of data.
20
- */
21
- get cache(): ?ICache<TOptions, TData>;
22
-
23
17
  /**
24
18
  * When true, server-side results are cached and hydrated in the client.
25
19
  * When false, the server-side cache is not used and results are not
@@ -29,17 +23,6 @@ interface IRequestHandler<TOptions, TData> {
29
23
  */
30
24
  get hydrate(): boolean;
31
25
 
32
- /**
33
- * Determine if the cached data should be refreshed.
34
- *
35
- * If this returns true, the framework will use the currently cached value
36
- * but also request a new value.
37
- */
38
- shouldRefreshCache(
39
- options: TOptions,
40
- cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
41
- ): boolean;
42
-
43
26
  /**
44
27
  * Get the key to use for a given request. This should be idempotent for a
45
28
  * given options set if you want caching to work across requests.
@@ -52,13 +35,6 @@ The constructor requires a `type` to identify your handler. This should be uniqu
52
35
  among the handlers that are used across your application, otherwise, requests
53
36
  may be fulfilled by the wrong handler.
54
37
 
55
- There is also an optional constructor argument, `cache`, which can be used to
56
- provide a custom cache for use with data the handler fulfills. Custom caches
57
- must implement the `ICache<TOptions, TData>` interface. If this is omitted, the
58
- core Wonder Blocks Data in-memory cache will be used. If you want to avoid
59
- caching in memory, see `NoCache`, which is a caching strategy that eliminates
60
- the use of caching entirely.
61
-
62
38
  The `fulfillRequest` method of this class is not implemented and will throw if
63
39
  called. Subclasses will need to implement this method.
64
40
 
@@ -73,11 +49,3 @@ SSR process is tracking the data for hydration. An example of setting this to
73
49
  false might be when you are using Apollo Client. In that scenario, you may use
74
50
  Apollo Cache to store and hydrate the data, while using Wonder Blocks Data to
75
51
  track and fulfill any query requests made via Apollo Client.
76
-
77
- Finally, the `shouldRefreshCache` method is provided for cases where a handler
78
- may want control over cache freshness. By default, this will return `true` for
79
- error results or a missing value. However, in some cases, handlers may want to
80
- make sure cached entries are not stale, and so may return `true` from the
81
- `shouldRefreshCache` method to instruct the framework to make a new request.
82
- The existing cached value will still be used, but an updated value will be
83
- requested.
@@ -25,9 +25,8 @@ type RequestCache = {
25
25
  *
26
26
  * INTERNAL USE ONLY
27
27
  */
28
- export const TrackerContext: React.Context<?TrackerFn> = new React.createContext<?TrackerFn>(
29
- null,
30
- );
28
+ export const TrackerContext: React.Context<?TrackerFn> =
29
+ new React.createContext<?TrackerFn>(null);
31
30
 
32
31
  /**
33
32
  * The default instance is stored here.
@@ -1,13 +1,11 @@
1
1
  // @flow
2
2
  import {Server} from "@khanacademy/wonder-blocks-core";
3
3
  import MemoryCache from "./memory-cache.js";
4
- import NoCache from "./no-cache.js";
5
4
 
6
5
  import type {
7
6
  ValidData,
8
7
  CacheEntry,
9
8
  Cache,
10
- ICache,
11
9
  IRequestHandler,
12
10
  ResponseCache as ResCache,
13
11
  } from "./types.js";
@@ -31,32 +29,17 @@ export class ResponseCache {
31
29
  return _default;
32
30
  }
33
31
 
34
- _hydrationAndDefaultCache: MemoryCache<any, any>;
32
+ _hydrationCache: MemoryCache<any, any>;
35
33
  _ssrOnlyCache: ?MemoryCache<any, any>;
36
34
 
37
35
  constructor(
38
- memoryCache: ?MemoryCache<any, any> = null,
36
+ hydrationCache: ?MemoryCache<any, any> = null,
39
37
  ssrOnlyCache: ?MemoryCache<any, any> = null,
40
38
  ) {
41
39
  this._ssrOnlyCache = Server.isServerSide()
42
40
  ? ssrOnlyCache || new MemoryCache()
43
41
  : undefined;
44
- this._hydrationAndDefaultCache = memoryCache || new MemoryCache();
45
- }
46
-
47
- /**
48
- * Returns the default cache to use for the given handler.
49
- */
50
- _defaultCache<TOptions, TData: ValidData>(
51
- handler: IRequestHandler<TOptions, TData>,
52
- ): ICache<TOptions, TData> {
53
- if (handler.hydrate) {
54
- return this._hydrationAndDefaultCache;
55
- }
56
-
57
- // If the handler doesn't want to hydrate, we return the SSR-only cache.
58
- // If we are client-side, we return our non-caching implementation.
59
- return this._ssrOnlyCache || NoCache.Default;
42
+ this._hydrationCache = hydrationCache || new MemoryCache();
60
43
  }
61
44
 
62
45
  _setCacheEntry<TOptions, TData: ValidData>(
@@ -65,15 +48,14 @@ export class ResponseCache {
65
48
  entry: CacheEntry<TData>,
66
49
  ): CacheEntry<TData> {
67
50
  const frozenEntry = Object.freeze(entry);
68
-
69
- if (this._ssrOnlyCache == null && handler.cache != null) {
70
- // We are not server-side, and our handler has its own cache,
71
- // so we use that to store values.
72
- handler.cache.store(handler, options, frozenEntry);
73
- } else {
74
- // We are either server-side, or our handler doesn't provide
75
- // a caching override.
76
- this._defaultCache(handler).store(handler, options, frozenEntry);
51
+ if (this._ssrOnlyCache != null) {
52
+ // We are server-side.
53
+ // We need to store this value.
54
+ if (handler.hydrate) {
55
+ this._hydrationCache.store(handler, options, frozenEntry);
56
+ } else {
57
+ this._ssrOnlyCache.store(handler, options, frozenEntry);
58
+ }
77
59
  }
78
60
  return frozenEntry;
79
61
  }
@@ -84,14 +66,14 @@ export class ResponseCache {
84
66
  * This can only be called if the cache is not already in use.
85
67
  */
86
68
  initialize: (source: ResCache) => void = (source) => {
87
- if (this._hydrationAndDefaultCache.inUse) {
69
+ if (this._hydrationCache.inUse) {
88
70
  throw new Error(
89
71
  "Cannot initialize data response cache more than once",
90
72
  );
91
73
  }
92
74
 
93
75
  try {
94
- this._hydrationAndDefaultCache = new MemoryCache(source);
76
+ this._hydrationCache = new MemoryCache(source);
95
77
  } catch (e) {
96
78
  throw new Error(
97
79
  `An error occurred trying to initialize the data response cache: ${e}`,
@@ -101,6 +83,8 @@ export class ResponseCache {
101
83
 
102
84
  /**
103
85
  * Cache data for a specific response.
86
+ *
87
+ * This is a noop when client-side.
104
88
  */
105
89
  cacheData: <TOptions, TData: ValidData>(
106
90
  handler: IRequestHandler<TOptions, TData>,
@@ -110,12 +94,12 @@ export class ResponseCache {
110
94
  handler: IRequestHandler<TOptions, TData>,
111
95
  options: TOptions,
112
96
  data: TData,
113
- ): CacheEntry<TData> => {
114
- return this._setCacheEntry(handler, options, {data});
115
- };
97
+ ): CacheEntry<TData> => this._setCacheEntry(handler, options, {data});
116
98
 
117
99
  /**
118
100
  * Cache an error for a specific response.
101
+ *
102
+ * This is a noop when client-side.
119
103
  */
120
104
  cacheError: <TOptions, TData: ValidData>(
121
105
  handler: IRequestHandler<TOptions, TData>,
@@ -140,42 +124,27 @@ export class ResponseCache {
140
124
  handler: IRequestHandler<TOptions, TData>,
141
125
  options: TOptions,
142
126
  ): ?$ReadOnly<CacheEntry<TData>> => {
143
- // If we're not server-side, and the handler has a custom cache
144
- // let's try to use it.
145
- if (this._ssrOnlyCache == null && handler.cache != null) {
146
- const entry = handler.cache.retrieve(handler, options);
147
- if (entry != null) {
148
- // Custom cache has an entry, so use it.
149
- return entry;
150
- }
151
- }
152
-
153
- // Get the internal entry for the handler.
154
- // This allows us to use our hydrated cache during hydration.
155
- // If we just returned null when the custom cache didn't have it,
156
- // we would never hydrate properly.
157
- const internalEntry = this._defaultCache<TOptions, TData>(
158
- handler,
159
- ).retrieve(handler, options);
160
-
161
- // If we are not server-side and we hydrated something that the custom
162
- // cache didn't have, we need to make sure the custom cache contains
163
- // that value.
164
- if (
165
- this._ssrOnlyCache == null &&
166
- handler.cache != null &&
167
- internalEntry != null
168
- ) {
169
- // Yes, if this throws, we will have a problem. We want that.
170
- // Bad cache implementations should be overt.
171
- handler.cache.store(handler, options, internalEntry);
127
+ // Get the cached entry for this value.
128
+ // If the handler wants WB Data to hydrate (i.e. handler.hydrate is
129
+ // true), we use our hydration cache. Otherwise, if we're server-side
130
+ // we use our SSR-only cache. Otherwise, there's no entry to return.
131
+ const cache = handler.hydrate
132
+ ? this._hydrationCache
133
+ : Server.isServerSide()
134
+ ? this._ssrOnlyCache
135
+ : undefined;
136
+ const internalEntry = cache?.retrieve(handler, options);
172
137
 
173
- // We now delete this from our in-memory cache as we don't need it.
138
+ // If we are not server-side and we hydrated something, let's clear
139
+ // that from the hydration cache to save memory.
140
+ if (this._ssrOnlyCache == null && internalEntry != null) {
141
+ // We now delete this from our hydration cache as we don't need it.
174
142
  // This does mean that if another handler of the same type but
175
- // without a custom cache won't get the value, but that's not an
176
- // expected valid usage of this framework - two handlers with
177
- // different caching options shouldn't be using the same type name.
178
- this._hydrationAndDefaultCache.remove(handler, options);
143
+ // without some sort of linked cache won't get the value, but
144
+ // that's not an expected use-case. If two different places use the
145
+ // same handler and options (i.e. the same request), then the
146
+ // handler should cater to that to ensure they share the result.
147
+ this._hydrationCache.remove(handler, options);
179
148
  }
180
149
  return internalEntry;
181
150
  };
@@ -199,31 +168,19 @@ export class ResponseCache {
199
168
  // to match the key of the entry we're removing, but that's an
200
169
  // inefficient way to remove a single item, so let's not do that.
201
170
 
202
- // If we're not server-side, and the handler has a custom cache
203
- // let's try to use it.
204
- const customCache = this._ssrOnlyCache == null ? handler.cache : null;
205
- const removedCustom: boolean = !!customCache?.remove(handler, options);
206
-
207
- // Delete the entry from our internal cache.
208
- // Even if we have a custom cache, we want to make sure we still
209
- // removed the same value from internal cache since this could be
210
- // getting called before hydration for some complex advanced usage
211
- // reason.
212
- return (
213
- this._defaultCache(handler).remove(handler, options) ||
214
- removedCustom
215
- );
171
+ // Delete the entry from the appropriate cache.
172
+ return handler.hydrate
173
+ ? this._hydrationCache.remove(handler, options)
174
+ : this._ssrOnlyCache?.remove(handler, options) ?? false;
216
175
  };
217
176
 
218
177
  /**
219
178
  * Remove from cache, any entries matching the given handler and predicate.
220
179
  *
221
- * This will, if present therein, remove matching values from the custom
222
- * cache associated with the handler and the framework in-memory cache.
180
+ * This will, if present therein, remove matching values from the framework
181
+ * in-memory cache.
223
182
  *
224
- * It returns a count of all records removed. This is not a count of unique
225
- * keys, but of unique entries. So if the same key is removed from both the
226
- * framework and custom caches, that will be 2 records removed.
183
+ * It returns a count of all records removed.
227
184
  */
228
185
  removeAll: <TOptions, TData: ValidData>(
229
186
  handler: IRequestHandler<TOptions, TData>,
@@ -238,36 +195,19 @@ export class ResponseCache {
238
195
  cachedEntry: $ReadOnly<CacheEntry<TData>>,
239
196
  ) => boolean,
240
197
  ): number => {
241
- // If we're not server-side, and the handler has a custom cache
242
- // let's try to use it.
243
- const customCache = this._ssrOnlyCache == null ? handler.cache : null;
244
- const removedCountCustom: number =
245
- customCache?.removeAll(handler, predicate) || 0;
246
-
247
- // Apply the predicate to what we have in our internal cached.
248
- // Even if we have a custom cache, we want to make sure we still
249
- // removed the same value from internal cache since this could be
250
- // getting called before hydration for some complex advanced usage
251
- // reason.
252
- const removedCount = this._defaultCache(handler).removeAll(
253
- handler,
254
- predicate,
255
- );
256
-
257
- // We have no idea which keys were removed from which caches,
258
- // so we can't dedupe the remove counts based on keys.
259
- // That's why we return the total records deleted rather than the
260
- // total keys deleted.
261
- return removedCount + removedCountCustom;
198
+ // Apply the predicate to what we have in the appropriate cache.
199
+ return handler.hydrate
200
+ ? this._hydrationCache.removeAll(handler, predicate)
201
+ : this._ssrOnlyCache?.removeAll(handler, predicate) ?? 0;
262
202
  };
263
203
 
264
204
  /**
265
205
  * Deep clone the hydration cache.
266
206
  *
267
- * By design, this does not clone anything held in custom caches.
207
+ * By design, this only clones the data that is to be used for hydration.
268
208
  */
269
209
  cloneHydratableData: () => $ReadOnly<Cache> = (): $ReadOnly<Cache> => {
270
210
  // We return our hydration cache only.
271
- return this._hydrationAndDefaultCache.cloneData();
211
+ return this._hydrationCache.cloneData();
272
212
  };
273
213
  }
@@ -0,0 +1,38 @@
1
+ // @flow
2
+ import type {ValidData, CacheEntry, Result} from "./types.js";
3
+
4
+ /**
5
+ * Turns a cache entry into a stateful result.
6
+ */
7
+ export const resultFromCacheEntry = <TData: ValidData>(
8
+ cacheEntry: ?CacheEntry<TData>,
9
+ ): Result<TData> => {
10
+ // No cache entry means we didn't load one yet.
11
+ if (cacheEntry == null) {
12
+ return {
13
+ status: "loading",
14
+ };
15
+ }
16
+
17
+ const {data, error} = cacheEntry;
18
+
19
+ if (data != null) {
20
+ return {
21
+ status: "success",
22
+ data,
23
+ };
24
+ }
25
+
26
+ if (error == null) {
27
+ // We should never get here ever.
28
+ return {
29
+ status: "error",
30
+ error: "Loaded result has invalid state where data and error are missing",
31
+ };
32
+ }
33
+
34
+ return {
35
+ status: "error",
36
+ error,
37
+ };
38
+ };
package/src/util/types.js CHANGED
@@ -1,16 +1,19 @@
1
1
  // @flow
2
2
  export type ValidData = string | boolean | number | {...};
3
3
 
4
+ export type Status = "loading" | "success" | "error";
5
+
4
6
  export type Result<TData: ValidData> =
5
7
  | {|
6
- loading: true,
7
- data?: void,
8
- error?: void,
8
+ status: "loading",
9
9
  |}
10
10
  | {|
11
- loading: false,
11
+ status: "success",
12
12
  data?: TData,
13
- error?: string,
13
+ |}
14
+ | {|
15
+ status: "error",
16
+ error: string,
14
17
  |};
15
18
 
16
19
  export type CacheEntry<TData: ValidData> =
@@ -28,24 +31,12 @@ type HandlerSubcache = {
28
31
  ...
29
32
  };
30
33
 
31
- export type InterceptCacheFn<TOptions, TData: ValidData> = (
32
- options: TOptions,
33
- cacheEntry: ?$ReadOnly<CacheEntry<TData>>,
34
- ) => ?$ReadOnly<CacheEntry<TData>>;
35
-
36
34
  export type InterceptFulfillRequestFn<TOptions, TData: ValidData> = (
37
35
  options: TOptions,
38
36
  ) => ?Promise<TData>;
39
37
 
40
- export type InterceptShouldRefreshCacheFn<TOptions, TData: ValidData> = (
41
- options: TOptions,
42
- cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
43
- ) => ?boolean;
44
-
45
38
  export type Interceptor = {|
46
- getEntry?: ?InterceptCacheFn<any, any>,
47
- fulfillRequest?: ?InterceptFulfillRequestFn<any, any>,
48
- shouldRefreshCache?: ?InterceptShouldRefreshCacheFn<any, any>,
39
+ fulfillRequest: InterceptFulfillRequestFn<any, any>,
49
40
  |};
50
41
 
51
42
  export type InterceptContextData = {
@@ -120,31 +111,19 @@ export interface IRequestHandler<TOptions, TData: ValidData> {
120
111
  */
121
112
  get type(): string;
122
113
 
123
- /**
124
- * A custom cache to use with data that this handler requests.
125
- * This only affects client-side caching of data.
126
- */
127
- get cache(): ?ICache<TOptions, TData>;
128
-
129
114
  /**
130
115
  * When true, server-side results are cached and hydrated in the client.
131
116
  * When false, the server-side cache is not used and results are not
132
117
  * hydrated.
133
118
  * This should only be set to false if something is ensuring that the
134
119
  * hydrated client result will match the server result.
135
- */
136
- get hydrate(): boolean;
137
-
138
- /**
139
- * Determine if the cached data should be refreshed.
140
120
  *
141
- * If this returns true, the framework will fulfill a new request by
142
- * calling `fulfillRequest`.
121
+ * For example, if Apollo is used to handle GraphQL requests in SSR mode,
122
+ * it has its own cache that is used to hydrate the client. Setting this
123
+ * to false makes sure we don't store the data twice, which would
124
+ * unnecessarily bloat the data sent back to the client.
143
125
  */
144
- shouldRefreshCache(
145
- options: TOptions,
146
- cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
147
- ): boolean;
126
+ get hydrate(): boolean;
148
127
 
149
128
  /**
150
129
  * Get the key to use for a given request. This should be idempotent for a