@khanacademy/wonder-blocks-data 3.1.3 → 5.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 (49) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/es/index.js +408 -349
  3. package/dist/index.js +599 -494
  4. package/docs.md +17 -35
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +7 -46
  7. package/src/__tests__/generated-snapshot.test.js +60 -126
  8. package/src/components/__tests__/data.test.js +373 -313
  9. package/src/components/__tests__/intercept-requests.test.js +58 -0
  10. package/src/components/data.js +139 -21
  11. package/src/components/data.md +38 -69
  12. package/src/components/intercept-context.js +6 -3
  13. package/src/components/intercept-requests.js +69 -0
  14. package/src/components/intercept-requests.md +54 -0
  15. package/src/components/track-data.md +9 -23
  16. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +17 -0
  17. package/src/hooks/__tests__/use-gql.test.js +1 -0
  18. package/src/hooks/__tests__/use-request-interception.test.js +255 -0
  19. package/src/hooks/__tests__/use-server-effect.test.js +217 -0
  20. package/src/hooks/__tests__/use-shared-cache.test.js +307 -0
  21. package/src/hooks/use-gql.js +36 -23
  22. package/src/hooks/use-request-interception.js +54 -0
  23. package/src/hooks/use-server-effect.js +45 -0
  24. package/src/hooks/use-shared-cache.js +106 -0
  25. package/src/index.js +18 -20
  26. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.js.snap +19 -0
  27. package/src/util/__tests__/request-fulfillment.test.js +42 -85
  28. package/src/util/__tests__/request-tracking.test.js +72 -191
  29. package/src/util/__tests__/{result-from-cache-entry.test.js → result-from-cache-response.test.js} +9 -10
  30. package/src/util/__tests__/scoped-in-memory-cache.test.js +396 -0
  31. package/src/util/__tests__/ssr-cache.test.js +639 -0
  32. package/src/util/request-fulfillment.js +36 -44
  33. package/src/util/request-tracking.js +62 -75
  34. package/src/util/{result-from-cache-entry.js → result-from-cache-response.js} +10 -13
  35. package/src/util/scoped-in-memory-cache.js +149 -0
  36. package/src/util/ssr-cache.js +206 -0
  37. package/src/util/types.js +43 -108
  38. package/src/components/__tests__/intercept-data.test.js +0 -87
  39. package/src/components/intercept-data.js +0 -77
  40. package/src/components/intercept-data.md +0 -65
  41. package/src/hooks/__tests__/use-data.test.js +0 -826
  42. package/src/hooks/use-data.js +0 -143
  43. package/src/util/__tests__/memory-cache.test.js +0 -446
  44. package/src/util/__tests__/request-handler.test.js +0 -121
  45. package/src/util/__tests__/response-cache.test.js +0 -879
  46. package/src/util/memory-cache.js +0 -187
  47. package/src/util/request-handler.js +0 -42
  48. package/src/util/request-handler.md +0 -51
  49. package/src/util/response-cache.js +0 -213
@@ -1,187 +0,0 @@
1
- // @flow
2
- import type {
3
- ValidData,
4
- ICache,
5
- CacheEntry,
6
- Cache,
7
- IRequestHandler,
8
- } from "./types.js";
9
-
10
- function deepClone<T: {...}>(source: T | $ReadOnly<T>): $ReadOnly<T> {
11
- /**
12
- * We want to deep clone the source cache to dodge mutations by external
13
- * references. So we serialize the source cache to JSON and parse it
14
- * back into a new object.
15
- *
16
- * NOTE: This doesn't work for get/set property accessors.
17
- */
18
- const serializedInitCache = JSON.stringify(source);
19
- const cloneInitCache = JSON.parse(serializedInitCache);
20
- return Object.freeze(cloneInitCache);
21
- }
22
-
23
- /**
24
- * INTERNAL USE ONLY
25
- *
26
- * Special case cache implementation for the memory cache.
27
- *
28
- * This is only used within our framework for SSR (see ./response-cache.js).
29
- */
30
- export default class MemoryCache<TOptions, TData: ValidData>
31
- implements ICache<TOptions, TData>
32
- {
33
- _cache: Cache;
34
-
35
- constructor(source: ?$ReadOnly<Cache> = null) {
36
- this._cache = {};
37
- if (source != null) {
38
- try {
39
- /**
40
- * Object.assign only performs a shallow clone.
41
- * So we deep clone it and then assign the clone values to our
42
- * internal cache.
43
- */
44
- const cloneInitCache = deepClone(source);
45
- Object.assign(this._cache, cloneInitCache);
46
- } catch (e) {
47
- throw new Error(
48
- `An error occurred trying to initialize from a response cache snapshot: ${e}`,
49
- );
50
- }
51
- }
52
- }
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
- */
59
- get inUse(): boolean {
60
- return Object.keys(this._cache).length > 0;
61
- }
62
-
63
- store: <TOptions, TData: ValidData>(
64
- handler: IRequestHandler<TOptions, TData>,
65
- options: TOptions,
66
- entry: CacheEntry<TData>,
67
- ) => void = <TOptions, TData: ValidData>(
68
- handler: IRequestHandler<TOptions, TData>,
69
- options: TOptions,
70
- entry: CacheEntry<TData>,
71
- ): void => {
72
- const requestType = handler.type;
73
-
74
- const frozenEntry = Object.freeze(entry);
75
-
76
- // Ensure we have a cache location for this handler type.
77
- this._cache[requestType] = this._cache[requestType] || {};
78
-
79
- // Cache the data.
80
- const key = handler.getKey(options);
81
- this._cache[requestType][key] = frozenEntry;
82
- };
83
-
84
- retrieve: <TOptions, TData: ValidData>(
85
- handler: IRequestHandler<TOptions, TData>,
86
- options: TOptions,
87
- ) => ?CacheEntry<TData> = <TOptions, TData: ValidData>(
88
- handler: IRequestHandler<TOptions, TData>,
89
- options: TOptions,
90
- ): ?CacheEntry<TData> => {
91
- const requestType = handler.type;
92
-
93
- // Get the internal subcache for the handler.
94
- const handlerCache = this._cache[requestType];
95
- if (!handlerCache) {
96
- return null;
97
- }
98
-
99
- // Get the response.
100
- const key = handler.getKey(options);
101
- const internalEntry = handlerCache[key];
102
- if (internalEntry == null) {
103
- return null;
104
- }
105
-
106
- return internalEntry;
107
- };
108
-
109
- remove: <TOptions, TData: ValidData>(
110
- handler: IRequestHandler<TOptions, TData>,
111
- options: TOptions,
112
- ) => boolean = <TOptions, TData: ValidData>(
113
- handler: IRequestHandler<TOptions, TData>,
114
- options: TOptions,
115
- ): boolean => {
116
- const requestType = handler.type;
117
-
118
- // NOTE(somewhatabstract): We could invoke removeAll with a predicate
119
- // to match the key of the entry we're removing, but that's an
120
- // inefficient way to remove a single item, so let's not do that.
121
-
122
- // Get the internal subcache for the handler.
123
- const handlerCache = this._cache[requestType];
124
- if (!handlerCache) {
125
- return false;
126
- }
127
-
128
- // Get the entry.
129
- const key = handler.getKey(options);
130
- const internalEntry = handlerCache[key];
131
- if (internalEntry == null) {
132
- return false;
133
- }
134
-
135
- // Delete the entry.
136
- delete handlerCache[key];
137
- return true;
138
- };
139
-
140
- removeAll: <TOptions, TData: ValidData>(
141
- handler: IRequestHandler<TOptions, TData>,
142
- predicate?: (
143
- key: string,
144
- cachedEntry: $ReadOnly<CacheEntry<TData>>,
145
- ) => boolean,
146
- ) => number = <TOptions, TData: ValidData>(
147
- handler: IRequestHandler<TOptions, TData>,
148
- predicate?: (
149
- key: string,
150
- cachedEntry: $ReadOnly<CacheEntry<TData>>,
151
- ) => boolean,
152
- ): number => {
153
- const requestType = handler.type;
154
-
155
- // Get the internal subcache for the handler.
156
- const handlerCache = this._cache[requestType];
157
- if (!handlerCache) {
158
- return 0;
159
- }
160
-
161
- let removedCount = 0;
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
- }
169
- }
170
- } else {
171
- // We're removing everything so delete the entire subcache.
172
- removedCount = Object.keys(handlerCache).length;
173
- delete this._cache[requestType];
174
- }
175
- return removedCount;
176
- };
177
-
178
- cloneData: () => $ReadOnly<Cache> = (): $ReadOnly<Cache> => {
179
- try {
180
- return deepClone(this._cache);
181
- } catch (e) {
182
- throw new Error(
183
- `An error occurred while trying to clone the cache: ${e}`,
184
- );
185
- }
186
- };
187
- }
@@ -1,42 +0,0 @@
1
- // @flow
2
- import type {ValidData, IRequestHandler} from "./types.js";
3
-
4
- /**
5
- * Base implementation for creating a request handler.
6
- *
7
- * Provides a base implementation of the `IRequestHandler` base class for
8
- * use with the Wonder Blocks Data framework.
9
- */
10
- export default class RequestHandler<TOptions, TData: ValidData>
11
- implements IRequestHandler<TOptions, TData>
12
- {
13
- _type: string;
14
- _hydrate: boolean;
15
-
16
- constructor(type: string, hydrate?: boolean = true) {
17
- this._type = type;
18
- this._hydrate = !!hydrate;
19
- }
20
-
21
- get type(): string {
22
- return this._type;
23
- }
24
-
25
- get hydrate(): boolean {
26
- return this._hydrate;
27
- }
28
-
29
- getKey(options: TOptions): string {
30
- try {
31
- return options === undefined
32
- ? "undefined"
33
- : (JSON.stringify(options): any);
34
- } catch (e) {
35
- throw new Error(`Failed to auto-generate key: ${e}`);
36
- }
37
- }
38
-
39
- fulfillRequest(options: TOptions): Promise<TData> {
40
- throw new Error("Not implemented");
41
- }
42
- }
@@ -1,51 +0,0 @@
1
- This class implements the `IRequestHandler` interface. It is to be used as a
2
- base class to implement your own request handler.
3
-
4
- ```js static
5
- interface IRequestHandler<TOptions, TData> {
6
- /**
7
- * Fulfill a given request.
8
- */
9
- fulfillRequest(options: TOptions): Promise<TData>;
10
-
11
- /**
12
- * The handler type; this is used to uniquely identify this handler from
13
- * any other handler.
14
- */
15
- get type(): string;
16
-
17
- /**
18
- * When true, server-side results are cached and hydrated in the client.
19
- * When false, the server-side cache is not used and results are not
20
- * hydrated.
21
- * This should only be set to false if something is ensuring that the
22
- * hydrated client result will match the server result.
23
- */
24
- get hydrate(): boolean;
25
-
26
- /**
27
- * Get the key to use for a given request. This should be idempotent for a
28
- * given options set if you want caching to work across requests.
29
- */
30
- getKey(options: TOptions): string;
31
- }
32
- ```
33
-
34
- The constructor requires a `type` to identify your handler. This should be unique
35
- among the handlers that are used across your application, otherwise, requests
36
- may be fulfilled by the wrong handler.
37
-
38
- The `fulfillRequest` method of this class is not implemented and will throw if
39
- called. Subclasses will need to implement this method.
40
-
41
- A default implementation of `getKey` is provided that serializes the options of
42
- a request to a string and uses that as the cache key. You may want to override
43
- this behavior to simplify the key or to omit some values from the key.
44
-
45
- The `hydrate` property indicates if the data that is fulfilled for the handler
46
- during SSR should be provided for hydration. This should be `true` in most
47
- cases. When `false`, React hydration will fail unless some other aspect of your
48
- SSR process is tracking the data for hydration. An example of setting this to
49
- false might be when you are using Apollo Client. In that scenario, you may use
50
- Apollo Cache to store and hydrate the data, while using Wonder Blocks Data to
51
- track and fulfill any query requests made via Apollo Client.
@@ -1,213 +0,0 @@
1
- // @flow
2
- import {Server} from "@khanacademy/wonder-blocks-core";
3
- import MemoryCache from "./memory-cache.js";
4
-
5
- import type {
6
- ValidData,
7
- CacheEntry,
8
- Cache,
9
- IRequestHandler,
10
- ResponseCache as ResCache,
11
- } from "./types.js";
12
-
13
- /**
14
- * The default instance is stored here.
15
- * It's created below in the Default() static property.
16
- */
17
- let _default: ResponseCache;
18
-
19
- /**
20
- * Implements the response cache.
21
- *
22
- * INTERNAL USE ONLY
23
- */
24
- export class ResponseCache {
25
- static get Default(): ResponseCache {
26
- if (!_default) {
27
- _default = new ResponseCache();
28
- }
29
- return _default;
30
- }
31
-
32
- _hydrationCache: MemoryCache<any, any>;
33
- _ssrOnlyCache: ?MemoryCache<any, any>;
34
-
35
- constructor(
36
- hydrationCache: ?MemoryCache<any, any> = null,
37
- ssrOnlyCache: ?MemoryCache<any, any> = null,
38
- ) {
39
- this._ssrOnlyCache = Server.isServerSide()
40
- ? ssrOnlyCache || new MemoryCache()
41
- : undefined;
42
- this._hydrationCache = hydrationCache || new MemoryCache();
43
- }
44
-
45
- _setCacheEntry<TOptions, TData: ValidData>(
46
- handler: IRequestHandler<TOptions, TData>,
47
- options: TOptions,
48
- entry: CacheEntry<TData>,
49
- ): CacheEntry<TData> {
50
- const frozenEntry = Object.freeze(entry);
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
- }
59
- }
60
- return frozenEntry;
61
- }
62
-
63
- /**
64
- * Initialize the cache from a given cache state.
65
- *
66
- * This can only be called if the cache is not already in use.
67
- */
68
- initialize: (source: ResCache) => void = (source) => {
69
- if (this._hydrationCache.inUse) {
70
- throw new Error(
71
- "Cannot initialize data response cache more than once",
72
- );
73
- }
74
-
75
- try {
76
- this._hydrationCache = new MemoryCache(source);
77
- } catch (e) {
78
- throw new Error(
79
- `An error occurred trying to initialize the data response cache: ${e}`,
80
- );
81
- }
82
- };
83
-
84
- /**
85
- * Cache data for a specific response.
86
- *
87
- * This is a noop when client-side.
88
- */
89
- cacheData: <TOptions, TData: ValidData>(
90
- handler: IRequestHandler<TOptions, TData>,
91
- options: TOptions,
92
- data: TData,
93
- ) => CacheEntry<TData> = <TOptions, TData: ValidData>(
94
- handler: IRequestHandler<TOptions, TData>,
95
- options: TOptions,
96
- data: TData,
97
- ): CacheEntry<TData> => this._setCacheEntry(handler, options, {data});
98
-
99
- /**
100
- * Cache an error for a specific response.
101
- *
102
- * This is a noop when client-side.
103
- */
104
- cacheError: <TOptions, TData: ValidData>(
105
- handler: IRequestHandler<TOptions, TData>,
106
- options: TOptions,
107
- error: Error | string,
108
- ) => CacheEntry<TData> = <TOptions, TData: ValidData>(
109
- handler: IRequestHandler<TOptions, TData>,
110
- options: TOptions,
111
- error: Error | string,
112
- ): CacheEntry<TData> => {
113
- const errorMessage = typeof error === "string" ? error : error.message;
114
- return this._setCacheEntry(handler, options, {error: errorMessage});
115
- };
116
-
117
- /**
118
- * Retrieve data from our cache.
119
- */
120
- getEntry: <TOptions, TData: ValidData>(
121
- handler: IRequestHandler<TOptions, TData>,
122
- options: TOptions,
123
- ) => ?$ReadOnly<CacheEntry<TData>> = <TOptions, TData: ValidData>(
124
- handler: IRequestHandler<TOptions, TData>,
125
- options: TOptions,
126
- ): ?$ReadOnly<CacheEntry<TData>> => {
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);
137
-
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.
142
- // This does mean that if another handler of the same type but
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);
148
- }
149
- return internalEntry;
150
- };
151
-
152
- /**
153
- * Remove from cache, the entry matching the given handler and options.
154
- *
155
- * This will, if present therein, remove the value from the custom cache
156
- * associated with the handler and the framework in-memory cache.
157
- *
158
- * Returns true if something was removed from any cache; otherwise, false.
159
- */
160
- remove: <TOptions, TData: ValidData>(
161
- handler: IRequestHandler<TOptions, TData>,
162
- options: TOptions,
163
- ) => boolean = <TOptions, TData: ValidData>(
164
- handler: IRequestHandler<TOptions, TData>,
165
- options: TOptions,
166
- ): boolean => {
167
- // NOTE(somewhatabstract): We could invoke removeAll with a predicate
168
- // to match the key of the entry we're removing, but that's an
169
- // inefficient way to remove a single item, so let's not do that.
170
-
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;
175
- };
176
-
177
- /**
178
- * Remove from cache, any entries matching the given handler and predicate.
179
- *
180
- * This will, if present therein, remove matching values from the framework
181
- * in-memory cache.
182
- *
183
- * It returns a count of all records removed.
184
- */
185
- removeAll: <TOptions, TData: ValidData>(
186
- handler: IRequestHandler<TOptions, TData>,
187
- predicate?: (
188
- key: string,
189
- cachedEntry: $ReadOnly<CacheEntry<TData>>,
190
- ) => boolean,
191
- ) => number = <TOptions, TData: ValidData>(
192
- handler: IRequestHandler<TOptions, TData>,
193
- predicate?: (
194
- key: string,
195
- cachedEntry: $ReadOnly<CacheEntry<TData>>,
196
- ) => boolean,
197
- ): number => {
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;
202
- };
203
-
204
- /**
205
- * Deep clone the hydration cache.
206
- *
207
- * By design, this only clones the data that is to be used for hydration.
208
- */
209
- cloneHydratableData: () => $ReadOnly<Cache> = (): $ReadOnly<Cache> => {
210
- // We return our hydration cache only.
211
- return this._hydrationCache.cloneData();
212
- };
213
- }