@khanacademy/wonder-blocks-data 7.0.1 → 8.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.
- package/CHANGELOG.md +20 -0
- package/dist/es/index.js +284 -100
- package/dist/index.js +1180 -800
- package/package.json +1 -1
- package/src/__docs__/_overview_ssr_.stories.mdx +13 -13
- package/src/__docs__/exports.abort-inflight-requests.stories.mdx +20 -0
- package/src/__docs__/exports.data.stories.mdx +3 -3
- package/src/__docs__/{exports.fulfill-all-data-requests.stories.mdx → exports.fetch-tracked-requests.stories.mdx} +5 -5
- package/src/__docs__/exports.get-gql-request-id.stories.mdx +24 -0
- package/src/__docs__/{exports.has-unfulfilled-requests.stories.mdx → exports.has-tracked-requests-to-be-fetched.stories.mdx} +4 -4
- package/src/__docs__/exports.intialize-hydration-cache.stories.mdx +29 -0
- package/src/__docs__/exports.purge-caches.stories.mdx +23 -0
- package/src/__docs__/{exports.remove-all-from-cache.stories.mdx → exports.purge-hydration-cache.stories.mdx} +4 -4
- package/src/__docs__/{exports.clear-shared-cache.stories.mdx → exports.purge-shared-cache.stories.mdx} +4 -4
- package/src/__docs__/exports.track-data.stories.mdx +4 -4
- package/src/__docs__/exports.use-cached-effect.stories.mdx +7 -4
- package/src/__docs__/exports.use-gql.stories.mdx +1 -33
- package/src/__docs__/exports.use-server-effect.stories.mdx +1 -1
- package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
- package/src/__docs__/types.fetch-policy.stories.mdx +44 -0
- package/src/__docs__/types.response-cache.stories.mdx +1 -1
- package/src/__tests__/generated-snapshot.test.js +5 -5
- package/src/components/__tests__/data.test.js +2 -6
- package/src/hooks/__tests__/use-cached-effect.test.js +341 -100
- package/src/hooks/__tests__/use-hydratable-effect.test.js +15 -9
- package/src/hooks/__tests__/use-shared-cache.test.js +6 -6
- package/src/hooks/use-cached-effect.js +169 -93
- package/src/hooks/use-hydratable-effect.js +8 -1
- package/src/hooks/use-shared-cache.js +2 -2
- package/src/index.js +14 -78
- package/src/util/__tests__/get-gql-request-id.test.js +74 -0
- package/src/util/__tests__/graphql-document-node-parser.test.js +542 -0
- package/src/util/__tests__/hydration-cache-api.test.js +35 -0
- package/src/util/__tests__/purge-caches.test.js +29 -0
- package/src/util/__tests__/request-api.test.js +188 -0
- package/src/util/__tests__/request-fulfillment.test.js +42 -0
- package/src/util/__tests__/ssr-cache.test.js +10 -60
- package/src/util/__tests__/to-gql-operation.test.js +42 -0
- package/src/util/data-error.js +6 -0
- package/src/util/get-gql-request-id.js +50 -0
- package/src/util/graphql-document-node-parser.js +133 -0
- package/src/util/graphql-types.js +30 -0
- package/src/util/hydration-cache-api.js +28 -0
- package/src/util/purge-caches.js +15 -0
- package/src/util/request-api.js +66 -0
- package/src/util/request-fulfillment.js +32 -12
- package/src/util/request-tracking.js +1 -1
- package/src/util/ssr-cache.js +1 -21
- package/src/util/to-gql-operation.js +44 -0
- package/src/util/types.js +31 -0
- package/src/__docs__/exports.intialize-cache.stories.mdx +0 -29
- package/src/__docs__/exports.remove-from-cache.stories.mdx +0 -25
- package/src/__docs__/exports.request-fulfillment.stories.mdx +0 -36
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
import {RequestTracker} from "./request-tracking.js";
|
|
4
|
+
import {RequestFulfillment} from "./request-fulfillment.js";
|
|
5
|
+
import {DataError, DataErrors} from "./data-error.js";
|
|
6
|
+
|
|
7
|
+
import type {ResponseCache} from "./types.js";
|
|
8
|
+
|
|
9
|
+
const SSRCheck = () => {
|
|
10
|
+
if (Server.isServerSide()) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (process.env.NODE_ENV === "production") {
|
|
15
|
+
return new DataError("No CSR tracking", DataErrors.NotAllowed);
|
|
16
|
+
} else {
|
|
17
|
+
return new DataError(
|
|
18
|
+
"Data requests are not tracked for fulfillment when when client-side",
|
|
19
|
+
DataErrors.NotAllowed,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetches all tracked data requests.
|
|
26
|
+
*
|
|
27
|
+
* This is for use with the `TrackData` component during server-side rendering.
|
|
28
|
+
*
|
|
29
|
+
* @throws {Error} If executed outside of server-side rendering.
|
|
30
|
+
* @returns {Promise<void>} A promise that resolves when all tracked requests
|
|
31
|
+
* have been fetched.
|
|
32
|
+
*/
|
|
33
|
+
export const fetchTrackedRequests = (): Promise<ResponseCache> => {
|
|
34
|
+
const ssrCheck = SSRCheck();
|
|
35
|
+
if (ssrCheck != null) {
|
|
36
|
+
return Promise.reject(ssrCheck);
|
|
37
|
+
}
|
|
38
|
+
return RequestTracker.Default.fulfillTrackedRequests();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Indicate if there are tracked requests waiting to be fetched.
|
|
43
|
+
*
|
|
44
|
+
* This is used in conjunction with `TrackData`.
|
|
45
|
+
*
|
|
46
|
+
* @throws {Error} If executed outside of server-side rendering.
|
|
47
|
+
* @returns {boolean} `true` if there are unfetched tracked requests;
|
|
48
|
+
* otherwise, `false`.
|
|
49
|
+
*/
|
|
50
|
+
export const hasTrackedRequestsToBeFetched = (): boolean => {
|
|
51
|
+
const ssrCheck = SSRCheck();
|
|
52
|
+
if (ssrCheck != null) {
|
|
53
|
+
throw ssrCheck;
|
|
54
|
+
}
|
|
55
|
+
return RequestTracker.Default.hasUnfulfilledRequests;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Abort all in-flight requests.
|
|
60
|
+
*
|
|
61
|
+
* This aborts all requests currently inflight via our default request
|
|
62
|
+
* fulfillment.
|
|
63
|
+
*/
|
|
64
|
+
export const abortInflightRequests = (): void => {
|
|
65
|
+
RequestFulfillment.Default.abortAll();
|
|
66
|
+
};
|
|
@@ -5,9 +5,13 @@ import {DataError, DataErrors} from "./data-error.js";
|
|
|
5
5
|
|
|
6
6
|
type RequestCache = {
|
|
7
7
|
[id: string]: Promise<Result<any>>,
|
|
8
|
-
...
|
|
9
8
|
};
|
|
10
9
|
|
|
10
|
+
type FulfillOptions<TData: ValidCacheData> = {|
|
|
11
|
+
handler: () => Promise<TData>,
|
|
12
|
+
hydrate?: boolean,
|
|
13
|
+
|};
|
|
14
|
+
|
|
11
15
|
let _default: RequestFulfillment;
|
|
12
16
|
|
|
13
17
|
/**
|
|
@@ -31,19 +35,10 @@ export class RequestFulfillment {
|
|
|
31
35
|
*/
|
|
32
36
|
fulfill: <TData: ValidCacheData>(
|
|
33
37
|
id: string,
|
|
34
|
-
options:
|
|
35
|
-
handler: () => Promise<TData>,
|
|
36
|
-
hydrate?: boolean,
|
|
37
|
-
|},
|
|
38
|
+
options: FulfillOptions<TData>,
|
|
38
39
|
) => Promise<Result<TData>> = <TData: ValidCacheData>(
|
|
39
40
|
id: string,
|
|
40
|
-
{
|
|
41
|
-
handler,
|
|
42
|
-
hydrate = true,
|
|
43
|
-
}: {|
|
|
44
|
-
handler: () => Promise<TData>,
|
|
45
|
-
hydrate?: boolean,
|
|
46
|
-
|},
|
|
41
|
+
{handler, hydrate = true}: FulfillOptions<TData>,
|
|
47
42
|
): Promise<Result<TData>> => {
|
|
48
43
|
/**
|
|
49
44
|
* If we have an inflight request, we'll provide that.
|
|
@@ -97,4 +92,29 @@ export class RequestFulfillment {
|
|
|
97
92
|
|
|
98
93
|
return request;
|
|
99
94
|
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Abort an inflight request.
|
|
98
|
+
*
|
|
99
|
+
* NOTE: Currently, this does not perform an actual abort. It merely
|
|
100
|
+
* removes the request from being tracked.
|
|
101
|
+
*/
|
|
102
|
+
abort: (id: string) => void = (id) => {
|
|
103
|
+
// TODO(somewhatabstract, FEI-4276): Add first class abort
|
|
104
|
+
// support to the handler API.
|
|
105
|
+
// For now, we will just clear the request out of the list.
|
|
106
|
+
// When abort is implemented, the `finally` in the `fulfill` method
|
|
107
|
+
// would handle the deletion.
|
|
108
|
+
delete this._requests[id];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Abort all inflight requests.
|
|
113
|
+
*
|
|
114
|
+
* NOTE: Currently, this does not perform actual aborts. It merely
|
|
115
|
+
* removes the requests from our tracking.
|
|
116
|
+
*/
|
|
117
|
+
abortAll: () => void = (): void => {
|
|
118
|
+
Object.keys(this._requests).forEach((id) => this.abort(id));
|
|
119
|
+
};
|
|
100
120
|
}
|
package/src/util/ssr-cache.js
CHANGED
|
@@ -142,26 +142,6 @@ export class SsrCache {
|
|
|
142
142
|
return internalEntry;
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
-
/**
|
|
146
|
-
* Remove from cache, the entry matching the given handler and options.
|
|
147
|
-
*
|
|
148
|
-
* This will, if present therein, remove the value from the custom cache
|
|
149
|
-
* associated with the handler and the framework in-memory cache.
|
|
150
|
-
*
|
|
151
|
-
* Returns true if something was removed from any cache; otherwise, false.
|
|
152
|
-
*/
|
|
153
|
-
remove: (id: string) => boolean = (id: string): boolean => {
|
|
154
|
-
// NOTE(somewhatabstract): We could invoke removeAll with a predicate
|
|
155
|
-
// to match the key of the entry we're removing, but that's an
|
|
156
|
-
// inefficient way to remove a single item, so let's not do that.
|
|
157
|
-
|
|
158
|
-
// Delete the entry from the appropriate cache.
|
|
159
|
-
return (
|
|
160
|
-
this._hydrationCache.purge(DefaultScope, id) ||
|
|
161
|
-
(this._ssrOnlyCache?.purge(DefaultScope, id) ?? false)
|
|
162
|
-
);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
145
|
/**
|
|
166
146
|
* Remove from cache, any entries matching the given handler and predicate.
|
|
167
147
|
*
|
|
@@ -170,7 +150,7 @@ export class SsrCache {
|
|
|
170
150
|
*
|
|
171
151
|
* It returns a count of all records removed.
|
|
172
152
|
*/
|
|
173
|
-
|
|
153
|
+
purgeData: (
|
|
174
154
|
predicate?: (
|
|
175
155
|
key: string,
|
|
176
156
|
cachedEntry: $ReadOnly<CachedResponse<ValidCacheData>>,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {graphQLDocumentNodeParser} from "./graphql-document-node-parser.js";
|
|
3
|
+
import type {GqlOperation} from "./gql-types.js";
|
|
4
|
+
import type {DocumentNode} from "./graphql-types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert a GraphQL DocumentNode to a base Wonder Blocks Data GqlOperation.
|
|
8
|
+
*
|
|
9
|
+
* If you want to include the query/mutation body, extend the result of this
|
|
10
|
+
* method and use the `graphql/language/printer` like:
|
|
11
|
+
*
|
|
12
|
+
* ```js
|
|
13
|
+
* import {print} from "graphql/language/printer";
|
|
14
|
+
*
|
|
15
|
+
* const gqlOpWithBody = {
|
|
16
|
+
* ...toGqlOperation(documentNode),
|
|
17
|
+
* query: print(documentNode),
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* If you want to enforce inclusion of __typename properties, then you can use
|
|
22
|
+
* `apollo-utilities` first to modify the document:
|
|
23
|
+
*
|
|
24
|
+
* ```js
|
|
25
|
+
* import {print} from "graphql/language/printer";
|
|
26
|
+
* import {addTypenameToDocument} from "apollo-utilities";
|
|
27
|
+
*
|
|
28
|
+
* const documentWithTypenames = addTypenameToDocument(documentNode);
|
|
29
|
+
* const gqlOpWithBody = {
|
|
30
|
+
* ...toGqlOperation(documentWithTypenames),
|
|
31
|
+
* query: print(documentWithTypenames),
|
|
32
|
+
* };
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const toGqlOperation = <TData, TVariables: {...}>(
|
|
36
|
+
documentNode: DocumentNode,
|
|
37
|
+
): GqlOperation<TData, TVariables> => {
|
|
38
|
+
const definition = graphQLDocumentNodeParser(documentNode);
|
|
39
|
+
const wbDataOperation: GqlOperation<TData, TVariables> = {
|
|
40
|
+
id: definition.name,
|
|
41
|
+
type: definition.type,
|
|
42
|
+
};
|
|
43
|
+
return wbDataOperation;
|
|
44
|
+
};
|
package/src/util/types.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import type {Metadata} from "@khanacademy/wonder-stuff-core";
|
|
3
3
|
|
|
4
|
+
// TODO(somewhatabstract, FEI-4172): Update eslint-plugin-flowtype when
|
|
5
|
+
// they've fixed https://github.com/gajus/eslint-plugin-flowtype/issues/502
|
|
6
|
+
/* eslint-disable no-undef */
|
|
7
|
+
/**
|
|
8
|
+
* Defines the various fetch policies that can be applied to requests.
|
|
9
|
+
*/
|
|
10
|
+
export enum FetchPolicy {
|
|
11
|
+
/**
|
|
12
|
+
* If the data is in the cache, return that; otherwise, fetch from the
|
|
13
|
+
* server.
|
|
14
|
+
*/
|
|
15
|
+
CacheBeforeNetwork,
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* If the data is in the cache, return that; always fetch from the server
|
|
19
|
+
* regardless of cache.
|
|
20
|
+
*/
|
|
21
|
+
CacheAndNetwork,
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* If the data is in the cache, return that; otherwise, do nothing.
|
|
25
|
+
*/
|
|
26
|
+
CacheOnly,
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ignore any existing cached result; always fetch from the server.
|
|
30
|
+
*/
|
|
31
|
+
NetworkOnly,
|
|
32
|
+
}
|
|
33
|
+
/* eslint-enable no-undef */
|
|
34
|
+
|
|
4
35
|
/**
|
|
5
36
|
* Define what can be cached.
|
|
6
37
|
*
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import {Meta} from "@storybook/addon-docs";
|
|
2
|
-
|
|
3
|
-
<Meta
|
|
4
|
-
title="Data / Exports / initializeCache()"
|
|
5
|
-
parameters={{
|
|
6
|
-
chromatic: {
|
|
7
|
-
disableSnapshot: true,
|
|
8
|
-
},
|
|
9
|
-
}}
|
|
10
|
-
/>
|
|
11
|
-
|
|
12
|
-
# initializeCache()
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
initializeCache(sourceCache: ResponseCache): void;
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
| Argument | Flow Type | Default | Description |
|
|
19
|
-
| --- | --- | --- | --- |
|
|
20
|
-
| `sourceData` | `ResponseCache` | _Required_ | The source cache that will be used to initialize the response cache. |
|
|
21
|
-
|
|
22
|
-
Wonder Blocks Data caches data in its response cache for hydration. This cache can be initialized with data using the `initializeCache` method.
|
|
23
|
-
The `initializeCache` method can only be called when the hydration cache is empty.
|
|
24
|
-
|
|
25
|
-
Usually, the data to be passed to `initializeCache` will be obtained by calling [`fulfillAllDataRequests`](/docs/data-exports-fulfillalldatarequests--page) after tracking data requests (see [`TrackData`](/docs/data-exports-trackdata--page)) during server-side rendering.
|
|
26
|
-
|
|
27
|
-
Combine with [`removeFromCache`](/docs/data-exports-removefromcache--page) or [`removeAllFromCache`](/docs/data-exports-removeallfromcache--page) to support your testing needs.
|
|
28
|
-
|
|
29
|
-
More details about server-side rendering with Wonder Blocks Data can be found in the [relevant overview section](/docs/data-server-side-rendering-and-hydration--page).
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import {Meta} from "@storybook/addon-docs";
|
|
2
|
-
|
|
3
|
-
<Meta
|
|
4
|
-
title="Data / Exports / removeFromCache()"
|
|
5
|
-
parameters={{
|
|
6
|
-
chromatic: {
|
|
7
|
-
disableSnapshot: true,
|
|
8
|
-
},
|
|
9
|
-
}}
|
|
10
|
-
/>
|
|
11
|
-
|
|
12
|
-
# removeFromCache()
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
removeFromCache(id: string): void;
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
| Argument | Flow Type | Default | Description |
|
|
19
|
-
| --- | --- | --- | --- |
|
|
20
|
-
| `id` | `string` | _Required_ | The id of the item to be removed. |
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Removes an entry from the cache.
|
|
24
|
-
|
|
25
|
-
This can be used after [`initializeCache`](/docs/data-exports-initializecache--page) to manipulate the cache prior to hydration. This can be useful during testing.
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import {Meta} from "@storybook/addon-docs";
|
|
2
|
-
|
|
3
|
-
<Meta
|
|
4
|
-
title="Data / Exports / RequestFulfillment"
|
|
5
|
-
parameters={{
|
|
6
|
-
chromatic: {
|
|
7
|
-
disableSnapshot: true,
|
|
8
|
-
},
|
|
9
|
-
}}
|
|
10
|
-
/>
|
|
11
|
-
|
|
12
|
-
# RequestFulfillment
|
|
13
|
-
|
|
14
|
-
The `RequestFulfillment` class encapsulates tracking of inflight asynchronous actions keyed by an identifier. Using this API, callers can request the result of the same asynchronous action multiple times without invoking the underlying action more than is necessary. Instead, any pending request will be shared by all requests for the same identifier.
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
fulfill: <TData: ValidCacheData>(
|
|
20
|
-
id: string,
|
|
21
|
-
options: {|
|
|
22
|
-
handler: () => Promise<TData>,
|
|
23
|
-
hydrate?: boolean,
|
|
24
|
-
|},
|
|
25
|
-
) => Promise<Result<TData>>;
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
There is a single function on the `RequestFulfillment` class, called `fulfill`.
|
|
29
|
-
|
|
30
|
-
The `fulfill` method takes the request identifier (used to deduplicate requests) and an options object. The options object contains the following properties:
|
|
31
|
-
|
|
32
|
-
* `handler`: A function that returns a promise resolving to the result of the request. This is the asynchronous work that will be tracked by the given identifier.
|
|
33
|
-
* `hydrate`: A boolean indicating whether the data should be hydrated. This is used during server-side rendering to determine if the response data should be included in the hydration cache. This defaults to `true` and should only be set to `false` if you are performing server-side rendering of the request and you know that the data will not be needed for hydration to succeed.
|
|
34
|
-
|
|
35
|
-
## RequestFulfillment.Default
|
|
36
|
-
The `RequestFulfillment` class provides a static instance, `RequestFulfillment.Default`, which is used by the Wonder Blocks Data framework. However, a custom instance can be constructed should your specific use case need to be isolated from others.
|