@khanacademy/wonder-blocks-data 2.3.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/es/index.js +212 -446
  3. package/dist/index.js +230 -478
  4. package/docs.md +19 -13
  5. package/package.json +2 -3
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +40 -160
  7. package/src/__tests__/generated-snapshot.test.js +15 -195
  8. package/src/components/__tests__/data.test.js +159 -965
  9. package/src/components/__tests__/intercept-data.test.js +9 -66
  10. package/src/components/__tests__/track-data.test.js +6 -5
  11. package/src/components/data.js +9 -117
  12. package/src/components/data.md +38 -60
  13. package/src/components/intercept-data.js +2 -34
  14. package/src/components/intercept-data.md +7 -105
  15. package/src/hooks/__tests__/use-data.test.js +790 -0
  16. package/src/hooks/use-data.js +138 -0
  17. package/src/index.js +1 -3
  18. package/src/util/__tests__/memory-cache.test.js +134 -35
  19. package/src/util/__tests__/request-fulfillment.test.js +21 -36
  20. package/src/util/__tests__/request-handler.test.js +30 -30
  21. package/src/util/__tests__/request-tracking.test.js +29 -30
  22. package/src/util/__tests__/response-cache.test.js +521 -561
  23. package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
  24. package/src/util/memory-cache.js +18 -14
  25. package/src/util/request-fulfillment.js +4 -0
  26. package/src/util/request-handler.js +2 -27
  27. package/src/util/request-handler.md +0 -32
  28. package/src/util/response-cache.js +50 -110
  29. package/src/util/result-from-cache-entry.js +38 -0
  30. package/src/util/types.js +14 -35
  31. package/LICENSE +0 -21
  32. package/src/components/__tests__/intercept-cache.test.js +0 -124
  33. package/src/components/__tests__/internal-data.test.js +0 -1030
  34. package/src/components/intercept-cache.js +0 -79
  35. package/src/components/intercept-cache.md +0 -103
  36. package/src/components/internal-data.js +0 -219
  37. package/src/util/__tests__/no-cache.test.js +0 -112
  38. package/src/util/no-cache.js +0 -67
  39. package/src/util/no-cache.md +0 -66
@@ -1,79 +0,0 @@
1
- // @flow
2
- import * as React from "react";
3
-
4
- import InterceptContext from "./intercept-context.js";
5
-
6
- import type {
7
- ValidData,
8
- IRequestHandler,
9
- InterceptCacheFn,
10
- } from "../util/types.js";
11
-
12
- type Props<TOptions, TData> = {|
13
- /**
14
- * A handler of the type to be intercepted.
15
- */
16
- handler: IRequestHandler<TOptions, TData>,
17
-
18
- /**
19
- * The children to render within this component. Any cache lookups by `Data`
20
- * components that use a handler of the same type as the handler for this
21
- * component that are rendered within these children will be intercepted by
22
- * this component (unless another `InterceptData` component overrides this
23
- * one).
24
- */
25
- children: React.Node,
26
-
27
- /**
28
- * Called to retrieve a cache entry.
29
- *
30
- * The method takes a key, the options for a request, and the existing
31
- * cached entry if one exists.
32
- *
33
- * If this returns null, the default behavior occurs.
34
- */
35
- getEntry: InterceptCacheFn<TOptions, TData>,
36
- |};
37
-
38
- /**
39
- * This component provides a mechanism to intercept cache lookups for the
40
- * type of a given handler and provide alternative values. This is mostly
41
- * useful for testing.
42
- *
43
- * This does not modify the cache in any way. If you want to intercept
44
- * requests and cache based on the intercept, then use `InterceptData`.
45
- *
46
- * This component is generally not suitable for use in production code as it
47
- * can prevent predictable functioning of the Wonder Blocks Data framework.
48
- *
49
- * These components do not chain. If a different `InterceptCache` instance is
50
- * rendered within this one that intercepts the same handler type, then that
51
- * new instance will replace this interceptor for its children.
52
- */
53
- export default class InterceptCache<
54
- TOptions,
55
- TData: ValidData,
56
- > extends React.Component<Props<TOptions, TData>> {
57
- render(): React.Node {
58
- return (
59
- <InterceptContext.Consumer>
60
- {(value) => {
61
- const handlerType = this.props.handler.type;
62
- const interceptor = {
63
- ...value[handlerType],
64
- getEntry: this.props.getEntry,
65
- };
66
- const newValue = {
67
- ...value,
68
- [handlerType]: interceptor,
69
- };
70
- return (
71
- <InterceptContext.Provider value={newValue}>
72
- {this.props.children}
73
- </InterceptContext.Provider>
74
- );
75
- }}
76
- </InterceptContext.Consumer>
77
- );
78
- }
79
- }
@@ -1,103 +0,0 @@
1
- Although it is possible to use the `initializeCache` method to setup test cases
2
- when working with the `Data` component, that can be a little cumbersome since
3
- it can only be called once and it requires knowledge of the cache structure.
4
-
5
- Instead of jumping through those hoops, you can use the `InterceptCache`
6
- component and provide an override method that will get called when looking up
7
- cached values. This component takes three props:
8
-
9
- - children to be rendered
10
- - the handler for data caches that are to be intercepted
11
- - a function for processing the intercept
12
-
13
- The intercept function has the form:
14
-
15
- ```js static
16
- (options: TOptions, cacheEntry: ?$ReadOnly<CacheEntry<TData>>) => ?$ReadOnly<CacheEntry<TData>>
17
- ```
18
-
19
- `options` are the options that would configure a data request that leads to the
20
- cached result.
21
- `cacheEntry` is the existing entry in the data cache.
22
-
23
- If the method returns `null`, the default behavior occurs. This means that if
24
- `cacheEntry` has a value, that will be used; otherwise, a request will be
25
- made for data via the relevant handler assigned to the `Data` component being
26
- intercepted.
27
-
28
- ```jsx
29
- import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
30
- import {View} from "@khanacademy/wonder-blocks-core";
31
- import {InterceptCache, Data, RequestHandler} from "@khanacademy/wonder-blocks-data";
32
- import {Strut} from "@khanacademy/wonder-blocks-layout";
33
- import Color from "@khanacademy/wonder-blocks-color";
34
- import Spacing from "@khanacademy/wonder-blocks-spacing";
35
-
36
- class MyHandler extends RequestHandler {
37
- constructor() {
38
- super("INTERCEPT_CACHE_HANDLER");
39
- }
40
-
41
- /**
42
- * fulfillRequest should not get called as we already have data cached.
43
- */
44
- fulfillRequest(options) {
45
- throw new Error(
46
- "If you're seeing this error, the examples are broken.",
47
- );
48
- }
49
-
50
- shouldRefreshCache(options, cachedEntry) {
51
- /**
52
- * For our purposes, the cache never needs a refresh.
53
- */
54
- return false;
55
- }
56
- }
57
-
58
- const handler = new MyHandler();
59
- const getEntryIntercept = function(options) {
60
- if (options === "ERROR") {
61
- return {error: "I'm an ERROR from the cache interceptor"};
62
- }
63
-
64
- return {data: "I'm DATA from the cache interceptor"};
65
- };
66
-
67
- <InterceptCache handler={handler} getEntry={getEntryIntercept}>
68
- <View>
69
- <Body>This intercepted cache has data!</Body>
70
- <Data handler={handler} options={"DATA"}>
71
- {({loading, data}) => {
72
- if (loading) {
73
- return "If you see this, the example is broken!";
74
- }
75
-
76
- return (
77
- <BodyMonospace>{data}</BodyMonospace>
78
- );
79
- }}
80
- </Data>
81
- </View>
82
- <Strut size={Spacing.small_12} />
83
- <View>
84
- <Body>This intercepted cache has error!</Body>
85
- <Data handler={handler} options={"ERROR"}>
86
- {({loading, error}) => {
87
- if (loading) {
88
- return "If you see this, the example is broken!";
89
- }
90
-
91
- return (
92
- <BodyMonospace style={{color: Color.red}}>ERROR: {error}</BodyMonospace>
93
- );
94
- }}
95
- </Data>
96
- </View>
97
- </InterceptCache>
98
- ```
99
-
100
- This component can also be useful if you want to avoid a data request because
101
- a parent component already has the data you need. Using this component, you
102
- can map already cached data of one request (or a portion thereof) to another
103
- without affecting the cache itself.
@@ -1,219 +0,0 @@
1
- // @flow
2
- import * as React from "react";
3
- import {Server} from "@khanacademy/wonder-blocks-core";
4
-
5
- import {RequestFulfillment} from "../util/request-fulfillment.js";
6
- import {TrackerContext} from "../util/request-tracking.js";
7
-
8
- import type {
9
- ValidData,
10
- CacheEntry,
11
- Result,
12
- IRequestHandler,
13
- } from "../util/types.js";
14
-
15
- type Props<TOptions, TData> = {|
16
- handler: IRequestHandler<TOptions, TData>,
17
- options: TOptions,
18
- getEntry: (
19
- handler: IRequestHandler<TOptions, TData>,
20
- options: TOptions,
21
- ) => ?$ReadOnly<CacheEntry<TData>>,
22
- children: (result: Result<TData>) => React.Node,
23
- |};
24
-
25
- type State<TData> = {|
26
- loading: boolean,
27
- data: ?TData,
28
- error: ?string,
29
- |};
30
-
31
- /**
32
- * This component is responsible for actually handling the data request.
33
- * It is wrapped by Data in order to support intercepts and be exported for use.
34
- *
35
- * INTERNAL USE ONLY
36
- */
37
- export default class InternalData<
38
- TOptions,
39
- TData: ValidData,
40
- > extends React.Component<Props<TOptions, TData>, State<TData>> {
41
- _mounted: boolean;
42
-
43
- constructor(props: Props<TOptions, TData>) {
44
- super(props);
45
-
46
- this.state = this._buildStateAndfulfillNeeds(props);
47
- }
48
-
49
- componentDidMount() {
50
- this._mounted = true;
51
- }
52
-
53
- shouldComponentUpdate(
54
- nextProps: $ReadOnly<Props<TOptions, TData>>,
55
- nextState: $ReadOnly<State<TData>>,
56
- ): boolean {
57
- /**
58
- * We only bother updating if our state changed.
59
- *
60
- * And we only update the state if props changed
61
- * or we got new data/error.
62
- */
63
- if (!this._propsMatch(nextProps)) {
64
- const newState = this._buildStateAndfulfillNeeds(nextProps);
65
- this.setState(newState);
66
- }
67
-
68
- return (
69
- this.state.loading !== nextState.loading ||
70
- this.state.data !== nextState.data ||
71
- this.state.error !== nextState.error
72
- );
73
- }
74
-
75
- componentWillUnmount() {
76
- this._mounted = false;
77
- }
78
-
79
- _propsMatch(otherProps: $Shape<Props<TOptions, TData>>): boolean {
80
- const {handler, options} = this.props;
81
- const {handler: prevHandler, options: prevOptions} = otherProps;
82
- return (
83
- handler === prevHandler &&
84
- handler.getKey(options) === prevHandler.getKey(prevOptions)
85
- );
86
- }
87
-
88
- _buildStateAndfulfillNeeds(
89
- propsAtFulfillment: $ReadOnly<Props<TOptions, TData>>,
90
- ): State<TData> {
91
- const {getEntry, handler, options} = propsAtFulfillment;
92
- const cachedData = getEntry(handler, options);
93
- if (
94
- !Server.isServerSide() &&
95
- (cachedData == null ||
96
- handler.shouldRefreshCache(options, cachedData))
97
- ) {
98
- /**
99
- * We're not on the server, the cache missed, or our handler says
100
- * we should refresh the cache.
101
- *
102
- * Therefore, we need to request data.
103
- *
104
- * We have to do this here from the constructor so that this
105
- * data request is tracked when performing server-side rendering.
106
- */
107
- RequestFulfillment.Default.fulfill(handler, options)
108
- .then((cacheEntry) => {
109
- /**
110
- * We get here, we should have updated the cache.
111
- * However, we need to update the component, but we
112
- * should only do that if the props are the same as they
113
- * were when this was called.
114
- */
115
- if (this._mounted && this._propsMatch(propsAtFulfillment)) {
116
- this.setState({
117
- loading: false,
118
- data: cacheEntry.data,
119
- error: cacheEntry.error,
120
- });
121
- }
122
- return null;
123
- })
124
- .catch((e) => {
125
- /**
126
- * We should never get here, but if we do.
127
- */
128
- // eslint-disable-next-line no-console
129
- console.error(
130
- `Unexpected error occurred during data fulfillment: ${e}`,
131
- );
132
- if (this._mounted && this._propsMatch(propsAtFulfillment)) {
133
- this.setState({
134
- loading: false,
135
- data: null,
136
- error: typeof e === "string" ? e : e.message,
137
- });
138
- }
139
- return null;
140
- });
141
- }
142
-
143
- /**
144
- * This is the default response for the server and for the initial
145
- * client-side render if we have cachedData.
146
- *
147
- * This ensures we don't make promises we don't want when doing
148
- * server-side rendering. Instead, we either have data from the cache
149
- * or we don't.
150
- */
151
- return {
152
- loading: cachedData == null,
153
- data: cachedData && cachedData.data,
154
- error: cachedData && cachedData.error,
155
- };
156
- }
157
-
158
- _resultFromState(): Result<TData> {
159
- const {loading, data, error} = this.state;
160
-
161
- if (loading) {
162
- return {
163
- loading: true,
164
- };
165
- }
166
-
167
- if (data != null) {
168
- return {
169
- loading: false,
170
- data,
171
- };
172
- }
173
-
174
- if (error == null) {
175
- // We should never get here ever.
176
- throw new Error(
177
- "Loaded result has invalid state where data and error are missing",
178
- );
179
- }
180
-
181
- return {
182
- loading: false,
183
- error,
184
- };
185
- }
186
-
187
- _renderContent(result: Result<TData>): React.Node {
188
- const {children} = this.props;
189
- return children(result);
190
- }
191
-
192
- _renderWithTrackingContext(result: Result<TData>): React.Node {
193
- return (
194
- <TrackerContext.Consumer>
195
- {(track) => {
196
- /**
197
- * If data tracking wasn't enabled, don't do it.
198
- */
199
- if (track != null) {
200
- track(this.props.handler, this.props.options);
201
- }
202
- return this._renderContent(result);
203
- }}
204
- </TrackerContext.Consumer>
205
- );
206
- }
207
-
208
- render(): React.Node {
209
- const result = this._resultFromState();
210
- // We only track data requests when we are server-side and we don't
211
- // already have a result. The existence of a result is indicated by the
212
- // loading flag being false.
213
- if (result.loading && Server.isServerSide()) {
214
- return this._renderWithTrackingContext(result);
215
- }
216
-
217
- return this._renderContent(result);
218
- }
219
- }
@@ -1,112 +0,0 @@
1
- // @flow
2
- import NoCache from "../no-cache.js";
3
-
4
- import type {IRequestHandler} from "../types.js";
5
-
6
- describe("NoCache", () => {
7
- describe("#store", () => {
8
- it("should not throw", () => {
9
- // Arrange
10
- const cache = new NoCache();
11
- const fakeHandler: IRequestHandler<string, string> = {
12
- getKey: () => "MY_KEY",
13
- type: "MY_HANDLER",
14
- shouldRefreshCache: () => false,
15
- fulfillRequest: jest.fn(),
16
- cache: null,
17
- hydrate: true,
18
- };
19
-
20
- // Act
21
- const underTest = () =>
22
- cache.store<string, string>(fakeHandler, "options", {
23
- data: "data",
24
- });
25
-
26
- // Assert
27
- expect(underTest).not.toThrow();
28
- });
29
- });
30
-
31
- describe("#retrieve", () => {
32
- it("should return null", () => {
33
- // Arrange
34
- const cache = new NoCache();
35
- const fakeHandler: IRequestHandler<string, string> = {
36
- getKey: () => "MY_KEY",
37
- type: "MY_HANDLER",
38
- shouldRefreshCache: () => false,
39
- fulfillRequest: jest.fn(),
40
- cache: null,
41
- hydrate: true,
42
- };
43
-
44
- // Act
45
- const result = cache.retrieve(fakeHandler, "options");
46
-
47
- // Assert
48
- expect(result).toBeNull();
49
- });
50
- });
51
-
52
- describe("#remove", () => {
53
- it("should return false", () => {
54
- // Arrange
55
- const cache = new NoCache();
56
- const fakeHandler: IRequestHandler<string, string> = {
57
- getKey: () => "MY_KEY",
58
- type: "MY_HANDLER",
59
- shouldRefreshCache: () => false,
60
- fulfillRequest: jest.fn(),
61
- cache: null,
62
- hydrate: true,
63
- };
64
-
65
- // Act
66
- const result = cache.remove(fakeHandler, "options");
67
-
68
- // Assert
69
- expect(result).toBeFalsy();
70
- });
71
- });
72
-
73
- describe("#removeAll", () => {
74
- it("should return 0 without predicate", () => {
75
- // Arrange
76
- const cache = new NoCache();
77
- const fakeHandler: IRequestHandler<string, string> = {
78
- getKey: () => "MY_KEY",
79
- type: "MY_HANDLER",
80
- shouldRefreshCache: () => false,
81
- fulfillRequest: jest.fn(),
82
- cache: null,
83
- hydrate: true,
84
- };
85
-
86
- // Act
87
- const result = cache.removeAll(fakeHandler);
88
-
89
- // Assert
90
- expect(result).toBe(0);
91
- });
92
-
93
- it("should return 0 with predicate", () => {
94
- // Arrange
95
- const cache = new NoCache();
96
- const fakeHandler: IRequestHandler<string, string> = {
97
- getKey: () => "MY_KEY",
98
- type: "MY_HANDLER",
99
- shouldRefreshCache: () => false,
100
- fulfillRequest: jest.fn(),
101
- cache: null,
102
- hydrate: true,
103
- };
104
-
105
- // Act
106
- const result = cache.removeAll(fakeHandler, () => true);
107
-
108
- // Assert
109
- expect(result).toBe(0);
110
- });
111
- });
112
- });
@@ -1,67 +0,0 @@
1
- // @flow
2
- import type {ValidData, ICache, CacheEntry, IRequestHandler} from "./types.js";
3
-
4
- let defaultInstance: ?ICache<any, any> = null;
5
- /**
6
- * This is a cache implementation to use when no caching is wanted.
7
- *
8
- * Use this with your request handler if you want to support server-side
9
- * rendering of your data requests, but want to ensure data is never cached
10
- * on the client-side.
11
- *
12
- * This is better than having `shouldRefreshCache` always return `true` in the
13
- * handler as this ensures that cache space and memory are never used for the
14
- * requested data after hydration has finished.
15
- */
16
- export default class NoCache<TOptions, TData: ValidData>
17
- implements ICache<TOptions, TData>
18
- {
19
- static get Default(): ICache<TOptions, TData> {
20
- if (defaultInstance == null) {
21
- defaultInstance = new NoCache<TOptions, TData>();
22
- }
23
- return defaultInstance;
24
- }
25
-
26
- store: <TOptions, TData: ValidData>(
27
- handler: IRequestHandler<TOptions, TData>,
28
- options: TOptions,
29
- entry: CacheEntry<TData>,
30
- ) => void = <TOptions, TData: ValidData>(
31
- handler: IRequestHandler<TOptions, TData>,
32
- options: TOptions,
33
- entry: CacheEntry<TData>,
34
- ): void => {
35
- /* empty */
36
- };
37
-
38
- retrieve: <TOptions, TData: ValidData>(
39
- handler: IRequestHandler<TOptions, TData>,
40
- options: TOptions,
41
- ) => ?CacheEntry<TData> = <TOptions, TData: ValidData>(
42
- handler: IRequestHandler<TOptions, TData>,
43
- options: TOptions,
44
- ): ?CacheEntry<TData> => null;
45
-
46
- remove: <TOptions, TData: ValidData>(
47
- handler: IRequestHandler<TOptions, TData>,
48
- options: TOptions,
49
- ) => boolean = <TOptions, TData: ValidData>(
50
- handler: IRequestHandler<TOptions, TData>,
51
- options: TOptions,
52
- ): boolean => false;
53
-
54
- removeAll: <TOptions, TData: ValidData>(
55
- handler: IRequestHandler<TOptions, TData>,
56
- predicate?: (
57
- key: string,
58
- cachedEntry: $ReadOnly<CacheEntry<TData>>,
59
- ) => boolean,
60
- ) => number = <TOptions, TData: ValidData>(
61
- handler: IRequestHandler<TOptions, TData>,
62
- predicate?: (
63
- key: string,
64
- cachedEntry: $ReadOnly<CacheEntry<TData>>,
65
- ) => boolean,
66
- ): number => 0;
67
- }
@@ -1,66 +0,0 @@
1
-
2
- `NoCache` is a cache implementation to use when no caching is wanted.
3
-
4
- Use this with your request handler if you want to support server-side
5
- rendering of your data requests, but want to ensure data is never cached
6
- on the client-side.
7
-
8
- This is better than having `shouldRefreshCache` always return `true` in the
9
- handler as this ensures that cache space and memory are never used for the
10
- requested data after hydration has finished.
11
-
12
- The `ICache` interface is included below for reference in case you would like
13
- to implement your own caching strategy.
14
-
15
- ```js static
16
- interface ICache<TOptions, TData: ValidData> {
17
- /**
18
- * Stores a value in the cache for the given handler and options.
19
- */
20
- store(
21
- handler: IRequestHandler<TOptions, TData>,
22
- options: TOptions,
23
- entry: CacheEntry<TData>,
24
- ): void;
25
-
26
- /**
27
- * Retrieves a value from the cache for the given handler and options.
28
- */
29
- retrieve(
30
- handler: IRequestHandler<TOptions, TData>,
31
- options: TOptions,
32
- ): ?$ReadOnly<CacheEntry<TData>>;
33
-
34
- /**
35
- * Remove the cached entry for the given handler and options.
36
- *
37
- * If the item exists in the cache, the cached entry is deleted and true
38
- * is returned. Otherwise, this returns false.
39
- */
40
- remove(
41
- handler: IRequestHandler<TOptions, TData>,
42
- options: TOptions,
43
- ): boolean;
44
-
45
- /**
46
- * Remove all cached entries for the given handler that, optionally, match
47
- * a given predicate.
48
- *
49
- * Returns the number of entries that were cleared from the cache.
50
- */
51
- removeAll(
52
- handler: IRequestHandler<TOptions, TData>,
53
- predicate?: (
54
- key: string,
55
- cachedEntry: $ReadOnly<CacheEntry<TData>>,
56
- ) => boolean,
57
- ): number;
58
- ```
59
-
60
- Use `NoCache` with your request handler if you want to support server-side
61
- rendering of your data requests, but also want to ensure data is never cached
62
- on the client-side.
63
-
64
- This is better than having `shouldRefreshCache` always return `true` in the
65
- handler as this ensures that cache space and memory are never used for the
66
- requested data after hydration has finished.