@khanacademy/wonder-blocks-data 8.0.5 → 9.1.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 +19 -0
- package/dist/es/index.js +22 -15
- package/dist/index.js +59 -22
- package/package.json +3 -3
- package/src/__docs__/_overview_ssr_.stories.mdx +2 -2
- package/src/__docs__/exports.purge-caches.stories.mdx +1 -1
- package/src/__docs__/exports.scoped-in-memory-cache.stories.mdx +3 -3
- package/src/__docs__/exports.serializable-in-memory-cache.stories.mdx +2 -2
- package/src/__docs__/exports.shared-cache.stories.mdx +16 -0
- package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
- package/src/__docs__/types.raw-scoped-cache.stories.mdx +27 -0
- package/src/__docs__/types.scoped-cache.stories.mdx +96 -9
- package/src/components/__tests__/data.test.js +2 -2
- package/src/hooks/__tests__/use-shared-cache.test.js +2 -50
- package/src/hooks/use-shared-cache.js +6 -14
- package/src/index.js +3 -2
- package/src/util/__tests__/get-gql-request-id.test.js +33 -0
- package/src/util/__tests__/graphql-document-node-parser.test.js +5 -5
- package/src/util/__tests__/purge-caches.test.js +2 -2
- package/src/util/get-gql-request-id.js +50 -6
- package/src/util/purge-caches.js +2 -2
- package/src/util/scoped-in-memory-cache.js +5 -9
- package/src/util/serializable-in-memory-cache.js +4 -8
- package/src/util/types.js +38 -1
- package/src/__docs__/exports.purge-shared-cache.stories.mdx +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-data
|
|
2
2
|
|
|
3
|
+
## 9.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- eb59ce34: Update to latest Wonder Stuff Core
|
|
8
|
+
|
|
9
|
+
## 9.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 944c3071: Make sure objects and arrays in variables are sorted and readable in generated request identifiers
|
|
14
|
+
|
|
15
|
+
## 9.0.0
|
|
16
|
+
|
|
17
|
+
### Major Changes
|
|
18
|
+
|
|
19
|
+
- 778f8e43: `SharedCache` export added for interacting with the shared in-memory cache. `purgeSharedCache` method has been removed.
|
|
20
|
+
- 778f8e43: Rename `ScopedCache` type to `RawScopedCache`
|
|
21
|
+
|
|
3
22
|
## 8.0.5
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/dist/es/index.js
CHANGED
|
@@ -390,13 +390,7 @@ const abortInflightRequests = () => {
|
|
|
390
390
|
};
|
|
391
391
|
|
|
392
392
|
const cache$1 = new ScopedInMemoryCache();
|
|
393
|
-
const
|
|
394
|
-
if (scope && typeof scope === "string") {
|
|
395
|
-
cache$1.purgeScope(scope);
|
|
396
|
-
} else {
|
|
397
|
-
cache$1.purgeAll();
|
|
398
|
-
}
|
|
399
|
-
};
|
|
393
|
+
const SharedCache = cache$1;
|
|
400
394
|
const useSharedCache = (id, scope, initialValue) => {
|
|
401
395
|
if (!id || typeof id !== "string") {
|
|
402
396
|
throw new DataError("id must be a non-empty string", DataErrors.InvalidInput);
|
|
@@ -422,7 +416,7 @@ const useSharedCache = (id, scope, initialValue) => {
|
|
|
422
416
|
};
|
|
423
417
|
|
|
424
418
|
const purgeCaches = () => {
|
|
425
|
-
|
|
419
|
+
SharedCache.purgeAll();
|
|
426
420
|
purgeHydrationCache();
|
|
427
421
|
};
|
|
428
422
|
|
|
@@ -689,14 +683,27 @@ const InterceptRequests = ({
|
|
|
689
683
|
}, children);
|
|
690
684
|
};
|
|
691
685
|
|
|
692
|
-
const toString =
|
|
686
|
+
const toString = value => {
|
|
693
687
|
var _JSON$stringify;
|
|
694
688
|
|
|
695
|
-
if (typeof
|
|
696
|
-
return
|
|
689
|
+
if (typeof value === "string") {
|
|
690
|
+
return value;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return (_JSON$stringify = JSON.stringify(value)) != null ? _JSON$stringify : "";
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const toStringifiedVariables = (acc, key, value) => {
|
|
697
|
+
if (typeof value === "object" && value !== null) {
|
|
698
|
+
return Object.entries(value).reduce((innerAcc, [i, v]) => {
|
|
699
|
+
const subKey = `${key}.${i}`;
|
|
700
|
+
return toStringifiedVariables(innerAcc, subKey, v);
|
|
701
|
+
}, acc);
|
|
702
|
+
} else {
|
|
703
|
+
acc[key] = toString(value);
|
|
697
704
|
}
|
|
698
705
|
|
|
699
|
-
return
|
|
706
|
+
return acc;
|
|
700
707
|
};
|
|
701
708
|
|
|
702
709
|
const getGqlRequestId = (operation, variables, context) => {
|
|
@@ -708,8 +715,8 @@ const getGqlRequestId = (operation, variables, context) => {
|
|
|
708
715
|
|
|
709
716
|
if (variables != null) {
|
|
710
717
|
const stringifiedVariables = Object.keys(variables).reduce((acc, key) => {
|
|
711
|
-
|
|
712
|
-
return acc;
|
|
718
|
+
const value = variables[key];
|
|
719
|
+
return toStringifiedVariables(acc, key, value);
|
|
713
720
|
}, {});
|
|
714
721
|
const sortableVariables = new URLSearchParams(stringifiedVariables);
|
|
715
722
|
sortableVariables.sort();
|
|
@@ -934,4 +941,4 @@ const useGql = (context = {}) => {
|
|
|
934
941
|
return gqlFetch;
|
|
935
942
|
};
|
|
936
943
|
|
|
937
|
-
export { Data, DataError, DataErrors, FetchPolicy, GqlError, GqlErrors, GqlRouter, InterceptRequests, ScopedInMemoryCache, SerializableInMemoryCache, Status, TrackData, WhenClientSide, abortInflightRequests, fetchTrackedRequests, getGqlDataFromResponse, getGqlRequestId, graphQLDocumentNodeParser, hasTrackedRequestsToBeFetched, initializeHydrationCache, purgeCaches, purgeHydrationCache,
|
|
944
|
+
export { Data, DataError, DataErrors, FetchPolicy, GqlError, GqlErrors, GqlRouter, InterceptRequests, ScopedInMemoryCache, SerializableInMemoryCache, SharedCache, Status, TrackData, WhenClientSide, abortInflightRequests, fetchTrackedRequests, getGqlDataFromResponse, getGqlRequestId, graphQLDocumentNodeParser, hasTrackedRequestsToBeFetched, initializeHydrationCache, purgeCaches, purgeHydrationCache, toGqlOperation, useCachedEffect, useGql, useHydratableEffect, useServerEffect, useSharedCache };
|
package/dist/index.js
CHANGED
|
@@ -242,7 +242,7 @@ class GqlError extends _khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0
|
|
|
242
242
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
243
243
|
|
|
244
244
|
"use strict";
|
|
245
|
-
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return
|
|
245
|
+
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return SharedCache; });
|
|
246
246
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return useSharedCache; });
|
|
247
247
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
|
|
248
248
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
@@ -259,18 +259,13 @@ class GqlError extends _khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0
|
|
|
259
259
|
*/
|
|
260
260
|
const cache = new _util_scoped_in_memory_cache_js__WEBPACK_IMPORTED_MODULE_2__[/* ScopedInMemoryCache */ "a"]();
|
|
261
261
|
/**
|
|
262
|
-
*
|
|
262
|
+
* Access to the shared in-memory cache.
|
|
263
|
+
*
|
|
264
|
+
* This is the cache used by `useSharedCache` and related hooks and
|
|
265
|
+
* components.
|
|
263
266
|
*/
|
|
264
267
|
|
|
265
|
-
const
|
|
266
|
-
// If we have a valid scope (empty string is falsy), then clear that scope.
|
|
267
|
-
if (scope && typeof scope === "string") {
|
|
268
|
-
cache.purgeScope(scope);
|
|
269
|
-
} else {
|
|
270
|
-
// Just reset the object. This should be sufficient.
|
|
271
|
-
cache.purgeAll();
|
|
272
|
-
}
|
|
273
|
-
};
|
|
268
|
+
const SharedCache = cache;
|
|
274
269
|
/**
|
|
275
270
|
* Hook to retrieve data from and store data in an in-memory cache.
|
|
276
271
|
*
|
|
@@ -279,9 +274,6 @@ const purgeSharedCache = (scope = "") => {
|
|
|
279
274
|
* function to set the cache entry (passing null or undefined to this function
|
|
280
275
|
* will delete the entry).
|
|
281
276
|
*
|
|
282
|
-
* To clear a single scope within the cache or the entire cache,
|
|
283
|
-
* the `clearScopedCache` export is available.
|
|
284
|
-
*
|
|
285
277
|
* NOTE: Unlike useState or useReducer, we don't automatically update folks
|
|
286
278
|
* if the value they reference changes. We might add it later (if we need to),
|
|
287
279
|
* but the likelihood here is that things won't be changing in this cache in a
|
|
@@ -1737,7 +1729,7 @@ const abortInflightRequests = () => {
|
|
|
1737
1729
|
*/
|
|
1738
1730
|
|
|
1739
1731
|
const purgeCaches = () => {
|
|
1740
|
-
|
|
1732
|
+
_hooks_use_shared_cache_js__WEBPACK_IMPORTED_MODULE_0__[/* SharedCache */ "a"].purgeAll();
|
|
1741
1733
|
Object(_hydration_cache_api_js__WEBPACK_IMPORTED_MODULE_1__[/* purgeHydrationCache */ "b"])();
|
|
1742
1734
|
};
|
|
1743
1735
|
|
|
@@ -1851,14 +1843,32 @@ const InterceptRequests = ({
|
|
|
1851
1843
|
|
|
1852
1844
|
"use strict";
|
|
1853
1845
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return getGqlRequestId; });
|
|
1854
|
-
const toString =
|
|
1846
|
+
const toString = value => {
|
|
1855
1847
|
var _JSON$stringify;
|
|
1856
1848
|
|
|
1857
|
-
if (typeof
|
|
1858
|
-
return
|
|
1849
|
+
if (typeof value === "string") {
|
|
1850
|
+
return value;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
return (_JSON$stringify = JSON.stringify(value)) != null ? _JSON$stringify : "";
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
const toStringifiedVariables = (acc, key, value) => {
|
|
1857
|
+
if (typeof value === "object" && value !== null) {
|
|
1858
|
+
// If we have an object or array, we build sub-variables so that
|
|
1859
|
+
// the ID is easily human-readable rather than having lots of
|
|
1860
|
+
// extra %-encodings. This means that an object or array variable
|
|
1861
|
+
// turns into x variables, where x is the field or element count of
|
|
1862
|
+
// variable. See below for example.
|
|
1863
|
+
return Object.entries(value).reduce((innerAcc, [i, v]) => {
|
|
1864
|
+
const subKey = `${key}.${i}`;
|
|
1865
|
+
return toStringifiedVariables(innerAcc, subKey, v);
|
|
1866
|
+
}, acc);
|
|
1867
|
+
} else {
|
|
1868
|
+
acc[key] = toString(value);
|
|
1859
1869
|
}
|
|
1860
1870
|
|
|
1861
|
-
return
|
|
1871
|
+
return acc;
|
|
1862
1872
|
};
|
|
1863
1873
|
/**
|
|
1864
1874
|
* Get an identifier for a given request.
|
|
@@ -1879,9 +1889,36 @@ const getGqlRequestId = (operation, variables, context) => {
|
|
|
1879
1889
|
|
|
1880
1890
|
if (variables != null) {
|
|
1881
1891
|
// We need to turn each variable into a string.
|
|
1892
|
+
// We also need to ensure we sort any sub-object keys.
|
|
1893
|
+
// `toStringifiedVariables` helps us with this by hoisting nested
|
|
1894
|
+
// data to individual variables for the purposes of ID generation.
|
|
1895
|
+
//
|
|
1896
|
+
// For example, consider variables:
|
|
1897
|
+
// {x: [1,2,3], y: {a: 1, b: 2, c: 3}, z: 123}
|
|
1898
|
+
//
|
|
1899
|
+
// Each variable, x, y and z, would be stringified into
|
|
1900
|
+
// stringifiedVariables as follows:
|
|
1901
|
+
// x becomes {"x.0": "1", "x.1": "2", "x.2": "3"}
|
|
1902
|
+
// y becomes {"y.a": "1", "y.b": "2", "y.c": "3"}
|
|
1903
|
+
// z becomes {"z": "123"}
|
|
1904
|
+
//
|
|
1905
|
+
// This then leads to stringifiedVariables being:
|
|
1906
|
+
// {
|
|
1907
|
+
// "x.0": "1",
|
|
1908
|
+
// "x.1": "2",
|
|
1909
|
+
// "x.2": "3",
|
|
1910
|
+
// "y.a": "1",
|
|
1911
|
+
// "y.b": "2",
|
|
1912
|
+
// "y.c": "3",
|
|
1913
|
+
// "z": "123",
|
|
1914
|
+
// }
|
|
1915
|
+
//
|
|
1916
|
+
// Thus allowing our use of URLSearchParams to both sort and easily
|
|
1917
|
+
// encode the variables into an idempotent identifier for those
|
|
1918
|
+
// variable values that is also human-readable.
|
|
1882
1919
|
const stringifiedVariables = Object.keys(variables).reduce((acc, key) => {
|
|
1883
|
-
|
|
1884
|
-
return acc;
|
|
1920
|
+
const value = variables[key];
|
|
1921
|
+
return toStringifiedVariables(acc, key, value);
|
|
1885
1922
|
}, {}); // We use the same mechanism as context to sort and arrange the
|
|
1886
1923
|
// variables.
|
|
1887
1924
|
|
|
@@ -2174,7 +2211,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
2174
2211
|
/* harmony import */ var _hooks_use_shared_cache_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(5);
|
|
2175
2212
|
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "useSharedCache", function() { return _hooks_use_shared_cache_js__WEBPACK_IMPORTED_MODULE_10__["b"]; });
|
|
2176
2213
|
|
|
2177
|
-
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "
|
|
2214
|
+
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "SharedCache", function() { return _hooks_use_shared_cache_js__WEBPACK_IMPORTED_MODULE_10__["a"]; });
|
|
2178
2215
|
|
|
2179
2216
|
/* harmony import */ var _hooks_use_hydratable_effect_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(12);
|
|
2180
2217
|
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "useHydratableEffect", function() { return _hooks_use_hydratable_effect_js__WEBPACK_IMPORTED_MODULE_11__["b"]; });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-data",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.1.1",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@khanacademy/wonder-blocks-core": "^4.3.2"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@khanacademy/wonder-stuff-core": "^0.1
|
|
20
|
+
"@khanacademy/wonder-stuff-core": "^1.0.1",
|
|
21
21
|
"flow-enums-runtime": "^0.0.6",
|
|
22
22
|
"react": "16.14.0"
|
|
23
23
|
},
|
|
@@ -26,4 +26,4 @@
|
|
|
26
26
|
},
|
|
27
27
|
"author": "",
|
|
28
28
|
"license": "MIT"
|
|
29
|
-
}
|
|
29
|
+
}
|
|
@@ -86,7 +86,7 @@ import {
|
|
|
86
86
|
TrackData,
|
|
87
87
|
hasTrackedRequestsToBeFetched,
|
|
88
88
|
fetchTrackedRequests,
|
|
89
|
-
|
|
89
|
+
SharedCache,
|
|
90
90
|
} from "@khanacademy/wonder-blocks-data";
|
|
91
91
|
|
|
92
92
|
// Don't forget to import your app!
|
|
@@ -113,7 +113,7 @@ async function renderApp(): Promise<string> {
|
|
|
113
113
|
* shared cache used by the `useSharedCache` hook as this is transient
|
|
114
114
|
* cache that does not itself get directly hydrated.
|
|
115
115
|
*/
|
|
116
|
-
|
|
116
|
+
SharedCache.purgeAll();
|
|
117
117
|
|
|
118
118
|
// Render the tracked component.
|
|
119
119
|
renderedComponent = renderToString(trackedElement);
|
|
@@ -20,4 +20,4 @@ The `purgeCaches` method will purge the following caches managed by Wonder Block
|
|
|
20
20
|
- Shared in-memory cache as used by [`useSharedCache`](/docs/data-exports-usesharedcache--page) and other hooks
|
|
21
21
|
- Hydration cache as used during server-side rendering
|
|
22
22
|
|
|
23
|
-
This is equivalent to calling both `
|
|
23
|
+
This is equivalent to calling both `SharedCache.purgeAll()` and `purgeHydrationCache()`, and is especially useful when writing tests or setting up a test environment.
|
|
@@ -11,12 +11,12 @@ import {Meta} from "@storybook/addon-docs";
|
|
|
11
11
|
|
|
12
12
|
# ScopedInMemoryCache
|
|
13
13
|
|
|
14
|
-
This class implements an in-memory cache that can contain different scopes of cached data. This allows for quick removal of entire classes of data as identified by their scopes without having to iterate each cached item to find them.
|
|
14
|
+
This class implements an in-memory cache that can contain different scopes of cached data. This allows for quick removal of entire classes of data as identified by their scopes without having to iterate each cached item to find them. It implements the [`ScopedCache`](/docs/data-types-scopedcache--page) interface.
|
|
15
15
|
|
|
16
16
|
## constructor()
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
|
-
new ScopedInMemoryCache(initialCache?:
|
|
19
|
+
new ScopedInMemoryCache(initialCache?: RawScopedCache)
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Creates a new instance. An initial state for the cache can be provided.
|
|
@@ -34,7 +34,7 @@ Is `true` if the cache contains any data; otherwise, `false`.
|
|
|
34
34
|
## set()
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
|
-
set
|
|
37
|
+
set(
|
|
38
38
|
scope: string,
|
|
39
39
|
id: string,
|
|
40
40
|
value: TValue,
|
|
@@ -16,7 +16,7 @@ This class is a specialization of [`ScopedInMemoryCache`](/docs/data-exports-sco
|
|
|
16
16
|
## constructor()
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
|
-
new SerializableInMemoryCache(initialCache?:
|
|
19
|
+
new SerializableInMemoryCache(initialCache?: RawScopedCache)
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Creates a new instance. The `initialCache`, if provided, will be cloned and used as the initial state of the cache.
|
|
@@ -67,7 +67,7 @@ Gets a value from the cache. If a value with the given identifier (`id`) is not
|
|
|
67
67
|
## clone()
|
|
68
68
|
|
|
69
69
|
```ts
|
|
70
|
-
clone():
|
|
70
|
+
clone(): RawScopedCache;
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
Returns a clone of the current cache.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / SharedCache"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# SharedCache
|
|
13
|
+
|
|
14
|
+
The `SharedCache` export can be used to view and modify the in-memory cache used by [`useSharedCache`](/docs/data-exports-usesharedcache--page) hook and the hooks and components that relate to it.
|
|
15
|
+
|
|
16
|
+
The `SharedCache` export implements the [`ScopedCache` interface type](/docs/data-types-scopedcache--page).
|
|
@@ -19,12 +19,12 @@ function useSharedCache<TValue: ValidCacheData>(
|
|
|
19
19
|
): [?TValue, CacheValueFn<TValue>];
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
The `useSharedCache` hook provides access to a shared in-memory cache. This cache is not part of the cache hydrated by Wonder Blocks Data, so [`
|
|
22
|
+
The `useSharedCache` hook provides access to a shared in-memory cache. This cache is not part of the cache hydrated by Wonder Blocks Data, so [`SharedCache.purgeAll()`](/docs/data-exports-sharedcache--page) must be called between server-side render cycles.
|
|
23
23
|
|
|
24
24
|
The hook returns a tuple of the currently cached value, or `null` if none is cached, and a function that can be used to set the cached value.
|
|
25
25
|
|
|
26
26
|
The shared cache is passive and as such does not notify of changes to its contents.
|
|
27
27
|
|
|
28
|
-
Each cached item is identified by an id and a scope. The scope is used to group items. Whole scopes can be cleared by specifying the specific scope when calling [`
|
|
28
|
+
Each cached item is identified by an id and a scope. The scope is used to group items. Whole scopes can be cleared by specifying the specific scope when calling [`SharedCache.purgeScope()`](/docs/data-exports-sharedcache--page).
|
|
29
29
|
|
|
30
30
|
An optional argument, `initialValue` can be given. This can be either the value to be cached itself or a function that returns the value to be cached (functions themselves are not valid cachable values). This allows for expensive initialization to only occur when it is necessary.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Types / RawScopedCache"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# RawScopedCache
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
type RawScopedCache = {
|
|
16
|
+
[scope: string]: {
|
|
17
|
+
[id: string]: ValidCacheData,
|
|
18
|
+
...
|
|
19
|
+
},
|
|
20
|
+
...
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`RawScopedCache` describes a cache that has distinct scoped sections in its raw object form. This is the representation of the caches used internally by Wonder Blocks Data to support the scoping of requests when using hooks such as [`useSharedCache`](/docs/data-exports-use-shared-cache--page), [`useCachedEffect`](/docs/data-exports-use-cached-effect--page), and [`useHydratableEffect`](/docs/data-exports-use-hydratable-effect--page).
|
|
26
|
+
|
|
27
|
+
See the section on [server-side rendering](/docs/data-server-side-rendering-and-hydration--page) for more information.
|
|
@@ -12,16 +12,103 @@ import {Meta} from "@storybook/addon-docs";
|
|
|
12
12
|
# ScopedCache
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
[id: string]: ValidCacheData,
|
|
18
|
-
...
|
|
19
|
-
},
|
|
20
|
-
...
|
|
21
|
-
};
|
|
15
|
+
interface ScopedCache {
|
|
16
|
+
set(scope: string, id: string, value: ValidCacheData): void;
|
|
22
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Retrieve a value from the cache.
|
|
20
|
+
*/
|
|
21
|
+
get(scope: string, id: string): ?ValidCacheData;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Purge an item from the cache.
|
|
25
|
+
*/
|
|
26
|
+
purge(scope: string, id: string): void;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Purge a scope of items that match the given predicate.
|
|
30
|
+
*
|
|
31
|
+
* If the predicate is omitted, then all items in the scope are purged.
|
|
32
|
+
*/
|
|
33
|
+
purgeScope(
|
|
34
|
+
scope: string,
|
|
35
|
+
predicate?: (id: string, value: ValidCacheData) => boolean,
|
|
36
|
+
): void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Purge all items from the cache that match the given predicate.
|
|
40
|
+
*
|
|
41
|
+
* If the predicate is omitted, then all items in the cache are purged.
|
|
42
|
+
*/
|
|
43
|
+
purgeAll(
|
|
44
|
+
predicate?: (
|
|
45
|
+
scope: string,
|
|
46
|
+
id: string,
|
|
47
|
+
value: ValidCacheData,
|
|
48
|
+
) => boolean,
|
|
49
|
+
): void;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This interface defines how to interact with a scoped cache, such as [`ScopedInMemoryCache`](/docs/data-exports-scopedinmemorycache--page).
|
|
54
|
+
|
|
55
|
+
## set()
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
set(
|
|
59
|
+
scope: string,
|
|
60
|
+
id: string,
|
|
61
|
+
value: TValue,
|
|
62
|
+
): void;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Sets a value in the cache within a given scope.
|
|
66
|
+
|
|
67
|
+
### Throws
|
|
68
|
+
|
|
69
|
+
| Error Type | Error Name | Reason |
|
|
70
|
+
| ------ | ------ | ------ |
|
|
71
|
+
| [`DataError`](/docs/data-exports-dataerror--page) | `InvalidInputDataError` | `id` and `scope` must be non-empty strings |
|
|
72
|
+
| [`DataError`](/docs/data-exports-dataerror--page) | `InvalidInputDataError` | `value` must be a non-function value |
|
|
73
|
+
|
|
74
|
+
## get()
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
get(scope: string, id: string): ?ValidCacheData;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Gets a value from the cache. If a value with the given identifier (`id`) is not found within the given scope (`scope`) of the cache, `null` is returned.
|
|
81
|
+
|
|
82
|
+
## purge()
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
purge(scope: string, id: string): void;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Purges the value from the cache. If a value with the given identifier (`id`) is not found within the given scope (`scope`) of the cache, nothing happens.
|
|
89
|
+
|
|
90
|
+
## purgeScope()
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
purgeScope(
|
|
94
|
+
scope: string,
|
|
95
|
+
predicate?: (id: string, value: ValidCacheData) => boolean,
|
|
96
|
+
): void;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Purges items within a given scope (`scope`) of the cache from that scope. If a predicate is provided, only items for which the predicate returns `true` will be purged; otherwise, the entire scope will be purged.
|
|
100
|
+
|
|
101
|
+
## purgeAll()
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
purgeAll(
|
|
105
|
+
predicate?: (
|
|
106
|
+
scope: string,
|
|
107
|
+
id: string,
|
|
108
|
+
value: ValidCacheData,
|
|
109
|
+
) => boolean,
|
|
110
|
+
): void;
|
|
23
111
|
```
|
|
24
112
|
|
|
25
|
-
|
|
113
|
+
Purges all items from the cache. If a predicate is provided, only items for which the predicate returns `true` will be purged; otherwise, the entire cache will be purged.
|
|
26
114
|
|
|
27
|
-
See the section on [server-side rendering](/docs/data-server-side-rendering-and-hydration--page) for more information.
|
|
@@ -7,7 +7,7 @@ import {render, act} from "@testing-library/react";
|
|
|
7
7
|
import * as ReactDOMServer from "react-dom/server";
|
|
8
8
|
import {Server, View} from "@khanacademy/wonder-blocks-core";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {SharedCache} from "../../hooks/use-shared-cache.js";
|
|
11
11
|
import TrackData from "../track-data.js";
|
|
12
12
|
import {RequestFulfillment} from "../../util/request-fulfillment.js";
|
|
13
13
|
import {SsrCache} from "../../util/ssr-cache.js";
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
describe("Data", () => {
|
|
26
26
|
beforeEach(() => {
|
|
27
|
-
|
|
27
|
+
SharedCache.purgeAll();
|
|
28
28
|
|
|
29
29
|
const responseCache = new SsrCache();
|
|
30
30
|
jest.spyOn(SsrCache, "Default", "get").mockReturnValue(responseCache);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
|
|
3
3
|
|
|
4
|
-
import {useSharedCache,
|
|
4
|
+
import {useSharedCache, SharedCache} from "../use-shared-cache.js";
|
|
5
5
|
|
|
6
6
|
describe("#useSharedCache", () => {
|
|
7
7
|
beforeEach(() => {
|
|
8
|
-
|
|
8
|
+
SharedCache.purgeAll();
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it.each`
|
|
@@ -257,51 +257,3 @@ describe("#useSharedCache", () => {
|
|
|
257
257
|
expect(result).toBeNull();
|
|
258
258
|
});
|
|
259
259
|
});
|
|
260
|
-
|
|
261
|
-
describe("#purgeSharedCache", () => {
|
|
262
|
-
beforeEach(() => {
|
|
263
|
-
purgeSharedCache();
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("should clear the entire cache if no scope given", () => {
|
|
267
|
-
// Arrange
|
|
268
|
-
const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
|
|
269
|
-
const hook2 = clientRenderHook(() => useSharedCache("id2", "scope2"));
|
|
270
|
-
hook1.result.current[1]("VALUE_1");
|
|
271
|
-
hook2.result.current[1]("VALUE_2");
|
|
272
|
-
// Make sure both hook results include the updated value.
|
|
273
|
-
hook1.rerender();
|
|
274
|
-
hook2.rerender();
|
|
275
|
-
|
|
276
|
-
// Act
|
|
277
|
-
purgeSharedCache();
|
|
278
|
-
// Make sure we refresh the hook results.
|
|
279
|
-
hook1.rerender();
|
|
280
|
-
hook2.rerender();
|
|
281
|
-
|
|
282
|
-
// Assert
|
|
283
|
-
expect(hook1.result.current[0]).toBeNull();
|
|
284
|
-
expect(hook2.result.current[0]).toBeNull();
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it("should clear the given scope only", () => {
|
|
288
|
-
// Arrange
|
|
289
|
-
const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
|
|
290
|
-
const hook2 = clientRenderHook(() => useSharedCache("id2", "scope2"));
|
|
291
|
-
hook1.result.current[1]("VALUE_1");
|
|
292
|
-
hook2.result.current[1]("VALUE_2");
|
|
293
|
-
// Make sure both hook results include the updated value.
|
|
294
|
-
hook1.rerender();
|
|
295
|
-
hook2.rerender();
|
|
296
|
-
|
|
297
|
-
// Act
|
|
298
|
-
purgeSharedCache("scope2");
|
|
299
|
-
// Make sure we refresh the hook results.
|
|
300
|
-
hook1.rerender();
|
|
301
|
-
hook2.rerender();
|
|
302
|
-
|
|
303
|
-
// Assert
|
|
304
|
-
expect(hook1.result.current[0]).toBe("VALUE_1");
|
|
305
|
-
expect(hook2.result.current[0]).toBeNull();
|
|
306
|
-
});
|
|
307
|
-
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {DataError, DataErrors} from "../util/data-error.js";
|
|
4
4
|
import {ScopedInMemoryCache} from "../util/scoped-in-memory-cache.js";
|
|
5
|
-
import type {ValidCacheData} from "../util/types.js";
|
|
5
|
+
import type {ValidCacheData, ScopedCache} from "../util/types.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A function for inserting a value into the cache or clearing it.
|
|
@@ -17,17 +17,12 @@ type CacheValueFn<TValue: ValidCacheData> = (value: ?TValue) => void;
|
|
|
17
17
|
const cache = new ScopedInMemoryCache();
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Access to the shared in-memory cache.
|
|
21
|
+
*
|
|
22
|
+
* This is the cache used by `useSharedCache` and related hooks and
|
|
23
|
+
* components.
|
|
21
24
|
*/
|
|
22
|
-
export const
|
|
23
|
-
// If we have a valid scope (empty string is falsy), then clear that scope.
|
|
24
|
-
if (scope && typeof scope === "string") {
|
|
25
|
-
cache.purgeScope(scope);
|
|
26
|
-
} else {
|
|
27
|
-
// Just reset the object. This should be sufficient.
|
|
28
|
-
cache.purgeAll();
|
|
29
|
-
}
|
|
30
|
-
};
|
|
25
|
+
export const SharedCache: ScopedCache = cache;
|
|
31
26
|
|
|
32
27
|
/**
|
|
33
28
|
* Hook to retrieve data from and store data in an in-memory cache.
|
|
@@ -37,9 +32,6 @@ export const purgeSharedCache = (scope: string = "") => {
|
|
|
37
32
|
* function to set the cache entry (passing null or undefined to this function
|
|
38
33
|
* will delete the entry).
|
|
39
34
|
*
|
|
40
|
-
* To clear a single scope within the cache or the entire cache,
|
|
41
|
-
* the `clearScopedCache` export is available.
|
|
42
|
-
*
|
|
43
35
|
* NOTE: Unlike useState or useReducer, we don't automatically update folks
|
|
44
36
|
* if the value they reference changes. We might add it later (if we need to),
|
|
45
37
|
* but the likelihood here is that things won't be changing in this cache in a
|
package/src/index.js
CHANGED
|
@@ -9,8 +9,9 @@ export type {
|
|
|
9
9
|
ResponseCache,
|
|
10
10
|
CachedResponse,
|
|
11
11
|
Result,
|
|
12
|
-
|
|
12
|
+
RawScopedCache,
|
|
13
13
|
ValidCacheData,
|
|
14
|
+
ScopedCache,
|
|
14
15
|
} from "./util/types.js";
|
|
15
16
|
|
|
16
17
|
export * from "./util/hydration-cache-api.js";
|
|
@@ -22,7 +23,7 @@ export {default as InterceptRequests} from "./components/intercept-requests.js";
|
|
|
22
23
|
export {DataError, DataErrors} from "./util/data-error.js";
|
|
23
24
|
export {useServerEffect} from "./hooks/use-server-effect.js";
|
|
24
25
|
export {useCachedEffect} from "./hooks/use-cached-effect.js";
|
|
25
|
-
export {useSharedCache,
|
|
26
|
+
export {useSharedCache, SharedCache} from "./hooks/use-shared-cache.js";
|
|
26
27
|
export {
|
|
27
28
|
useHydratableEffect,
|
|
28
29
|
// TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
|
|
@@ -71,4 +71,37 @@ describe("#getGqlRequestId", () => {
|
|
|
71
71
|
`variable1=value1&variable2=42&variable3=&variable4=null&variable5=true`,
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
it("should sort nested variable properties", () => {
|
|
76
|
+
// Arrange
|
|
77
|
+
const operation = {
|
|
78
|
+
type: "query",
|
|
79
|
+
id: "myQuery",
|
|
80
|
+
};
|
|
81
|
+
const variables = {
|
|
82
|
+
variable4: null,
|
|
83
|
+
variable2: 42,
|
|
84
|
+
variable1: "value1",
|
|
85
|
+
variable5: true,
|
|
86
|
+
variable3: undefined,
|
|
87
|
+
variable6: {
|
|
88
|
+
nested2: "nested2",
|
|
89
|
+
nested1: "nested1",
|
|
90
|
+
},
|
|
91
|
+
variable7: [1, 2, 3],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
const requestId = getGqlRequestId(operation, variables, {
|
|
96
|
+
module: "MODULE",
|
|
97
|
+
curriculum: "CURRICULUM",
|
|
98
|
+
targetLocale: "LOCALE",
|
|
99
|
+
});
|
|
100
|
+
const result = new Set(requestId.split("|"));
|
|
101
|
+
|
|
102
|
+
// Assert
|
|
103
|
+
expect(result).toContain(
|
|
104
|
+
`variable1=value1&variable2=42&variable3=&variable4=null&variable5=true&variable6.nested1=nested1&variable6.nested2=nested2&variable7.0=1&variable7.1=2&variable7.2=3`,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
74
107
|
});
|
|
@@ -18,7 +18,7 @@ describe("#graphQLDocumentNodeParser", () => {
|
|
|
18
18
|
|
|
19
19
|
it("should throw if the document lacks the kind property", () => {
|
|
20
20
|
// Arrange
|
|
21
|
-
const documentNode =
|
|
21
|
+
const documentNode: any = {};
|
|
22
22
|
|
|
23
23
|
// Act
|
|
24
24
|
const underTest = () => graphQLDocumentNodeParser(documentNode);
|
|
@@ -283,7 +283,7 @@ describe("#graphQLDocumentNodeParser", () => {
|
|
|
283
283
|
|
|
284
284
|
// Assert
|
|
285
285
|
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
286
|
-
`"We do not support subscriptions. {
|
|
286
|
+
`"We do not support subscriptions. {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","variableDefinitions":[],"name":{"kind":"Name","value":"subscription"}}]} had 1 subscriptions"`,
|
|
287
287
|
);
|
|
288
288
|
});
|
|
289
289
|
|
|
@@ -318,7 +318,7 @@ describe("#graphQLDocumentNodeParser", () => {
|
|
|
318
318
|
|
|
319
319
|
// Assert
|
|
320
320
|
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
321
|
-
`"We only support one query or mutation per component. {
|
|
321
|
+
`"We only support one query or mutation per component. {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","variableDefinitions":[],"name":{"kind":"Name","value":"query"}},{"kind":"OperationDefinition","operation":"query","variableDefinitions":[],"name":{"kind":"Name","value":"query"}}]} had 2 queries and 0 mutations. "`,
|
|
322
322
|
);
|
|
323
323
|
});
|
|
324
324
|
|
|
@@ -353,7 +353,7 @@ describe("#graphQLDocumentNodeParser", () => {
|
|
|
353
353
|
|
|
354
354
|
// Assert
|
|
355
355
|
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
356
|
-
`"We only support one query or mutation per component. {
|
|
356
|
+
`"We only support one query or mutation per component. {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","variableDefinitions":[],"name":{"kind":"Name","value":"mutation"}},{"kind":"OperationDefinition","operation":"mutation","variableDefinitions":[],"name":{"kind":"Name","value":"mutation"}}]} had 0 queries and 2 mutations. "`,
|
|
357
357
|
);
|
|
358
358
|
});
|
|
359
359
|
|
|
@@ -388,7 +388,7 @@ describe("#graphQLDocumentNodeParser", () => {
|
|
|
388
388
|
|
|
389
389
|
// Assert
|
|
390
390
|
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
391
|
-
`"We only support one query or mutation per component. {
|
|
391
|
+
`"We only support one query or mutation per component. {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","variableDefinitions":[],"name":{"kind":"Name","value":"query"}},{"kind":"OperationDefinition","operation":"mutation","variableDefinitions":[],"name":{"kind":"Name","value":"mutation"}}]} had 1 queries and 1 mutations. "`,
|
|
392
392
|
);
|
|
393
393
|
});
|
|
394
394
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import
|
|
2
|
+
import {SharedCache} from "../../hooks/use-shared-cache.js";
|
|
3
3
|
import * as HydrationCacheApi from "../hydration-cache-api.js";
|
|
4
4
|
|
|
5
5
|
import {purgeCaches} from "../purge-caches.js";
|
|
@@ -7,7 +7,7 @@ import {purgeCaches} from "../purge-caches.js";
|
|
|
7
7
|
describe("#purgeCaches", () => {
|
|
8
8
|
it("should purge the shared cache", () => {
|
|
9
9
|
// Arrange
|
|
10
|
-
const spy = jest.spyOn(
|
|
10
|
+
const spy = jest.spyOn(SharedCache, "purgeAll");
|
|
11
11
|
|
|
12
12
|
// Act
|
|
13
13
|
purgeCaches();
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import type {GqlOperation, GqlContext} from "./gql-types.js";
|
|
3
3
|
|
|
4
|
-
const toString = (
|
|
5
|
-
if (typeof
|
|
6
|
-
return
|
|
4
|
+
const toString = (value: mixed): string => {
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
return value;
|
|
7
7
|
}
|
|
8
|
-
return JSON.stringify(
|
|
8
|
+
return JSON.stringify(value) ?? "";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const toStringifiedVariables = (acc: any, key: string, value: mixed): any => {
|
|
12
|
+
if (typeof value === "object" && value !== null) {
|
|
13
|
+
// If we have an object or array, we build sub-variables so that
|
|
14
|
+
// the ID is easily human-readable rather than having lots of
|
|
15
|
+
// extra %-encodings. This means that an object or array variable
|
|
16
|
+
// turns into x variables, where x is the field or element count of
|
|
17
|
+
// variable. See below for example.
|
|
18
|
+
return Object.entries(value).reduce((innerAcc, [i, v]) => {
|
|
19
|
+
const subKey = `${key}.${i}`;
|
|
20
|
+
return toStringifiedVariables(innerAcc, subKey, v);
|
|
21
|
+
}, acc);
|
|
22
|
+
} else {
|
|
23
|
+
acc[key] = toString(value);
|
|
24
|
+
}
|
|
25
|
+
return acc;
|
|
9
26
|
};
|
|
10
27
|
|
|
11
28
|
/**
|
|
@@ -32,10 +49,37 @@ export const getGqlRequestId = <TData, TVariables: {...}>(
|
|
|
32
49
|
// Finally, if we have variables, we add those too.
|
|
33
50
|
if (variables != null) {
|
|
34
51
|
// We need to turn each variable into a string.
|
|
52
|
+
// We also need to ensure we sort any sub-object keys.
|
|
53
|
+
// `toStringifiedVariables` helps us with this by hoisting nested
|
|
54
|
+
// data to individual variables for the purposes of ID generation.
|
|
55
|
+
//
|
|
56
|
+
// For example, consider variables:
|
|
57
|
+
// {x: [1,2,3], y: {a: 1, b: 2, c: 3}, z: 123}
|
|
58
|
+
//
|
|
59
|
+
// Each variable, x, y and z, would be stringified into
|
|
60
|
+
// stringifiedVariables as follows:
|
|
61
|
+
// x becomes {"x.0": "1", "x.1": "2", "x.2": "3"}
|
|
62
|
+
// y becomes {"y.a": "1", "y.b": "2", "y.c": "3"}
|
|
63
|
+
// z becomes {"z": "123"}
|
|
64
|
+
//
|
|
65
|
+
// This then leads to stringifiedVariables being:
|
|
66
|
+
// {
|
|
67
|
+
// "x.0": "1",
|
|
68
|
+
// "x.1": "2",
|
|
69
|
+
// "x.2": "3",
|
|
70
|
+
// "y.a": "1",
|
|
71
|
+
// "y.b": "2",
|
|
72
|
+
// "y.c": "3",
|
|
73
|
+
// "z": "123",
|
|
74
|
+
// }
|
|
75
|
+
//
|
|
76
|
+
// Thus allowing our use of URLSearchParams to both sort and easily
|
|
77
|
+
// encode the variables into an idempotent identifier for those
|
|
78
|
+
// variable values that is also human-readable.
|
|
35
79
|
const stringifiedVariables = Object.keys(variables).reduce(
|
|
36
80
|
(acc, key) => {
|
|
37
|
-
|
|
38
|
-
return acc;
|
|
81
|
+
const value = variables[key];
|
|
82
|
+
return toStringifiedVariables(acc, key, value);
|
|
39
83
|
},
|
|
40
84
|
{},
|
|
41
85
|
);
|
package/src/util/purge-caches.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import {
|
|
2
|
+
import {SharedCache} from "../hooks/use-shared-cache.js";
|
|
3
3
|
import {purgeHydrationCache} from "./hydration-cache-api.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,6 +10,6 @@ import {purgeHydrationCache} from "./hydration-cache-api.js";
|
|
|
10
10
|
* which caches may have been used during a given test run.
|
|
11
11
|
*/
|
|
12
12
|
export const purgeCaches = () => {
|
|
13
|
-
|
|
13
|
+
SharedCache.purgeAll();
|
|
14
14
|
purgeHydrationCache();
|
|
15
15
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import {DataError, DataErrors} from "./data-error.js";
|
|
3
|
-
import type {ScopedCache, ValidCacheData} from "./types.js";
|
|
3
|
+
import type {ScopedCache, RawScopedCache, ValidCacheData} from "./types.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Describe an in-memory cache.
|
|
7
7
|
*/
|
|
8
|
-
export class ScopedInMemoryCache {
|
|
9
|
-
_cache:
|
|
8
|
+
export class ScopedInMemoryCache implements ScopedCache {
|
|
9
|
+
_cache: RawScopedCache;
|
|
10
10
|
|
|
11
|
-
constructor(initialCache:
|
|
11
|
+
constructor(initialCache: RawScopedCache = {}) {
|
|
12
12
|
this._cache = initialCache;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -24,11 +24,7 @@ export class ScopedInMemoryCache {
|
|
|
24
24
|
/**
|
|
25
25
|
* Set a value in the cache.
|
|
26
26
|
*/
|
|
27
|
-
set
|
|
28
|
-
scope: string,
|
|
29
|
-
id: string,
|
|
30
|
-
value: TValue,
|
|
31
|
-
): void {
|
|
27
|
+
set(scope: string, id: string, value: ValidCacheData): void {
|
|
32
28
|
if (!id || typeof id !== "string") {
|
|
33
29
|
throw new DataError(
|
|
34
30
|
"id must be non-empty string",
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import {clone} from "@khanacademy/wonder-stuff-core";
|
|
3
3
|
import {DataError, DataErrors} from "./data-error.js";
|
|
4
4
|
import {ScopedInMemoryCache} from "./scoped-in-memory-cache.js";
|
|
5
|
-
import type {ValidCacheData,
|
|
5
|
+
import type {ValidCacheData, RawScopedCache} from "./types.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Describe a serializable in-memory cache.
|
|
9
9
|
*/
|
|
10
10
|
export class SerializableInMemoryCache extends ScopedInMemoryCache {
|
|
11
|
-
constructor(initialCache:
|
|
11
|
+
constructor(initialCache: RawScopedCache = {}) {
|
|
12
12
|
try {
|
|
13
13
|
super(clone(initialCache));
|
|
14
14
|
} catch (e) {
|
|
@@ -22,18 +22,14 @@ export class SerializableInMemoryCache extends ScopedInMemoryCache {
|
|
|
22
22
|
/**
|
|
23
23
|
* Set a value in the cache.
|
|
24
24
|
*/
|
|
25
|
-
set
|
|
26
|
-
scope: string,
|
|
27
|
-
id: string,
|
|
28
|
-
value: TValue,
|
|
29
|
-
): void {
|
|
25
|
+
set(scope: string, id: string, value: ValidCacheData): void {
|
|
30
26
|
super.set(scope, id, Object.freeze(clone(value)));
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
/**
|
|
34
30
|
* Clone the cache.
|
|
35
31
|
*/
|
|
36
|
-
clone():
|
|
32
|
+
clone(): RawScopedCache {
|
|
37
33
|
try {
|
|
38
34
|
return clone(this._cache);
|
|
39
35
|
} catch (e) {
|
package/src/util/types.js
CHANGED
|
@@ -84,7 +84,7 @@ export type ResponseCache = {
|
|
|
84
84
|
/**
|
|
85
85
|
* A cache with scoped sections.
|
|
86
86
|
*/
|
|
87
|
-
export type
|
|
87
|
+
export type RawScopedCache = {
|
|
88
88
|
/**
|
|
89
89
|
* The cache is scoped to allow easier clearing of different types of usage.
|
|
90
90
|
*/
|
|
@@ -112,3 +112,40 @@ export type ErrorOptions = {|
|
|
|
112
112
|
*/
|
|
113
113
|
cause?: ?Error,
|
|
114
114
|
|};
|
|
115
|
+
|
|
116
|
+
export interface ScopedCache {
|
|
117
|
+
set(scope: string, id: string, value: ValidCacheData): void;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Retrieve a value from the cache.
|
|
121
|
+
*/
|
|
122
|
+
get(scope: string, id: string): ?ValidCacheData;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Purge an item from the cache.
|
|
126
|
+
*/
|
|
127
|
+
purge(scope: string, id: string): void;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Purge a scope of items that match the given predicate.
|
|
131
|
+
*
|
|
132
|
+
* If the predicate is omitted, then all items in the scope are purged.
|
|
133
|
+
*/
|
|
134
|
+
purgeScope(
|
|
135
|
+
scope: string,
|
|
136
|
+
predicate?: (id: string, value: ValidCacheData) => boolean,
|
|
137
|
+
): void;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Purge all items from the cache that match the given predicate.
|
|
141
|
+
*
|
|
142
|
+
* If the predicate is omitted, then all items in the cache are purged.
|
|
143
|
+
*/
|
|
144
|
+
purgeAll(
|
|
145
|
+
predicate?: (
|
|
146
|
+
scope: string,
|
|
147
|
+
id: string,
|
|
148
|
+
value: ValidCacheData,
|
|
149
|
+
) => boolean,
|
|
150
|
+
): void;
|
|
151
|
+
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import {Meta} from "@storybook/addon-docs";
|
|
2
|
-
|
|
3
|
-
<Meta
|
|
4
|
-
title="Data / Exports / purgeSharedCache()"
|
|
5
|
-
parameters={{
|
|
6
|
-
chromatic: {
|
|
7
|
-
disableSnapshot: true,
|
|
8
|
-
},
|
|
9
|
-
}}
|
|
10
|
-
/>
|
|
11
|
-
|
|
12
|
-
# purgeSharedCache()
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
purgeSharedCache(scope?: string): void;
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
The `purgeSharedCache` method can be used to clear the shared in-memory cache used by the [`useSharedCache`](/docs/data-exports-usesharedcache--page) hook. Either a single scope or all scopes can be cleared.
|
|
19
|
-
|
|
20
|
-
Common uses for calling this method are during [server-side rendering](/docs/data-server-side-rendering-and-hydration--page) to ensure each render cycle remains isolated, or during testing in a `beforeEach` to cover for where previous test cases may have changed the shared cache.
|