@khanacademy/wonder-blocks-data 5.0.1 → 6.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 (85) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/es/index.js +771 -372
  3. package/dist/index.js +1191 -550
  4. package/legacy-docs.md +3 -0
  5. package/package.json +2 -2
  6. package/src/__docs__/_overview_.stories.mdx +18 -0
  7. package/src/__docs__/_overview_graphql.stories.mdx +35 -0
  8. package/src/__docs__/_overview_ssr_.stories.mdx +185 -0
  9. package/src/__docs__/_overview_testing_.stories.mdx +123 -0
  10. package/src/__docs__/exports.clear-shared-cache.stories.mdx +20 -0
  11. package/src/__docs__/exports.data-error.stories.mdx +23 -0
  12. package/src/__docs__/exports.data-errors.stories.mdx +23 -0
  13. package/src/{components/data.md → __docs__/exports.data.stories.mdx} +15 -18
  14. package/src/__docs__/exports.fulfill-all-data-requests.stories.mdx +24 -0
  15. package/src/__docs__/exports.gql-error.stories.mdx +23 -0
  16. package/src/__docs__/exports.gql-errors.stories.mdx +20 -0
  17. package/src/__docs__/exports.gql-router.stories.mdx +29 -0
  18. package/src/__docs__/exports.has-unfulfilled-requests.stories.mdx +20 -0
  19. package/src/{components/intercept-requests.md → __docs__/exports.intercept-requests.stories.mdx} +16 -1
  20. package/src/__docs__/exports.intialize-cache.stories.mdx +29 -0
  21. package/src/__docs__/exports.remove-all-from-cache.stories.mdx +24 -0
  22. package/src/__docs__/exports.remove-from-cache.stories.mdx +25 -0
  23. package/src/__docs__/exports.request-fulfillment.stories.mdx +36 -0
  24. package/src/__docs__/exports.scoped-in-memory-cache.stories.mdx +92 -0
  25. package/src/__docs__/exports.serializable-in-memory-cache.stories.mdx +112 -0
  26. package/src/__docs__/exports.status.stories.mdx +31 -0
  27. package/src/{components/track-data.md → __docs__/exports.track-data.stories.mdx} +15 -0
  28. package/src/__docs__/exports.use-cached-effect.stories.mdx +41 -0
  29. package/src/__docs__/exports.use-gql.stories.mdx +73 -0
  30. package/src/__docs__/exports.use-hydratable-effect.stories.mdx +43 -0
  31. package/src/__docs__/exports.use-server-effect.stories.mdx +38 -0
  32. package/src/__docs__/exports.use-shared-cache.stories.mdx +30 -0
  33. package/src/__docs__/exports.when-client-side.stories.mdx +33 -0
  34. package/src/__docs__/types.cached-response.stories.mdx +29 -0
  35. package/src/__docs__/types.error-options.stories.mdx +21 -0
  36. package/src/__docs__/types.gql-context.stories.mdx +20 -0
  37. package/src/__docs__/types.gql-fetch-fn.stories.mdx +24 -0
  38. package/src/__docs__/types.gql-fetch-options.stories.mdx +24 -0
  39. package/src/__docs__/types.gql-operation-type.stories.mdx +24 -0
  40. package/src/__docs__/types.gql-operation.stories.mdx +67 -0
  41. package/src/__docs__/types.response-cache.stories.mdx +33 -0
  42. package/src/__docs__/types.result.stories.mdx +39 -0
  43. package/src/__docs__/types.scoped-cache.stories.mdx +27 -0
  44. package/src/__docs__/types.valid-cache-data.stories.mdx +23 -0
  45. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -80
  46. package/src/__tests__/generated-snapshot.test.js +0 -24
  47. package/src/components/__tests__/data.test.js +149 -128
  48. package/src/components/data.js +22 -112
  49. package/src/components/intercept-requests.js +1 -1
  50. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
  51. package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
  52. package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
  53. package/src/hooks/__tests__/use-gql.test.js +1 -30
  54. package/src/hooks/__tests__/use-hydratable-effect.test.js +708 -0
  55. package/src/hooks/__tests__/use-server-effect.test.js +39 -11
  56. package/src/hooks/use-cached-effect.js +225 -0
  57. package/src/hooks/use-gql-router-context.js +50 -0
  58. package/src/hooks/use-gql.js +22 -52
  59. package/src/hooks/use-hydratable-effect.js +206 -0
  60. package/src/hooks/use-request-interception.js +20 -23
  61. package/src/hooks/use-server-effect.js +12 -5
  62. package/src/hooks/use-shared-cache.js +13 -11
  63. package/src/index.js +53 -3
  64. package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
  65. package/src/util/__tests__/merge-gql-context.test.js +74 -0
  66. package/src/util/__tests__/request-fulfillment.test.js +23 -42
  67. package/src/util/__tests__/request-tracking.test.js +26 -7
  68. package/src/util/__tests__/result-from-cache-response.test.js +19 -5
  69. package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
  70. package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
  71. package/src/util/__tests__/ssr-cache.test.js +52 -52
  72. package/src/util/abort-error.js +15 -0
  73. package/src/util/data-error.js +58 -0
  74. package/src/util/get-gql-data-from-response.js +3 -2
  75. package/src/util/gql-error.js +19 -11
  76. package/src/util/merge-gql-context.js +34 -0
  77. package/src/util/request-fulfillment.js +49 -46
  78. package/src/util/request-tracking.js +69 -15
  79. package/src/util/result-from-cache-response.js +12 -16
  80. package/src/util/scoped-in-memory-cache.js +24 -47
  81. package/src/util/serializable-in-memory-cache.js +49 -0
  82. package/src/util/ssr-cache.js +9 -8
  83. package/src/util/status.js +30 -0
  84. package/src/util/types.js +18 -1
  85. package/docs.md +0 -122
@@ -0,0 +1,206 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import {AbortError} from "../util/abort-error.js";
5
+
6
+ import {useServerEffect} from "./use-server-effect.js";
7
+ import {useSharedCache} from "./use-shared-cache.js";
8
+ import {useCachedEffect} from "./use-cached-effect.js";
9
+
10
+ import type {Result, ValidCacheData} from "../util/types.js";
11
+
12
+ /**
13
+ * Policies to define how a hydratable effect should behave client-side.
14
+ */
15
+ export enum WhenClientSide {
16
+ // TODO(somewhatabstract, FEI-4172): Update eslint-plugin-flowtype when
17
+ // they've fixed https://github.com/gajus/eslint-plugin-flowtype/issues/502
18
+ /* eslint-disable no-undef */
19
+ /**
20
+ * The result from executing the effect server-side will not be hydrated.
21
+ * The effect will always be executed client-side.
22
+ *
23
+ * This should only be used if there is something else that is responsible
24
+ * for properly hydrating this component (for example, the action invokes
25
+ * Apollo which manages its own cache to ensure things render properly).
26
+ */
27
+ DoNotHydrate,
28
+
29
+ /**
30
+ * The result from executing the effect server-side will be hydrated.
31
+ * The effect will only execute client-side if there was no result to
32
+ * be hydrated (i.e. both error and success hydration results prevent the
33
+ * effect running client-side).
34
+ */
35
+ ExecuteWhenNoResult,
36
+
37
+ /**
38
+ * The result from executing the effect server-side will be hydrated.
39
+ * If the hydrated result is a success result, the effect will not be
40
+ * executed client-side.
41
+ * If the hydrated result was not a success result, or there was no
42
+ * hydrated result, the effect will not be executed.
43
+ */
44
+ ExecuteWhenNoSuccessResult,
45
+
46
+ /**
47
+ * The result from executing the effect server-side will be hydrated.
48
+ * The effect will always be executed client-side, regardless of the
49
+ * hydrated result status.
50
+ */
51
+ AlwaysExecute,
52
+ /* eslint-enable no-undef */
53
+ }
54
+
55
+ type HydratableEffectOptions<TData: ValidCacheData> = {|
56
+ /**
57
+ * How the hook should behave when rendering client-side for the first time.
58
+ *
59
+ * This controls how the hook hydrates and executes when client-side.
60
+ *
61
+ * Default is `WhenClientSide.ExecuteWhenNoSuccessResult`.
62
+ *
63
+ * Changing this value after the first call is irrelevant as it only
64
+ * affects the initial render behavior.
65
+ */
66
+ clientBehavior?: WhenClientSide,
67
+
68
+ /**
69
+ * When `true`, the effect will not be executed; otherwise, the effect will
70
+ * be executed.
71
+ *
72
+ * If this is set to `true` while the effect is still pending, the pending
73
+ * effect will be cancelled.
74
+ *
75
+ * Default is `false`.
76
+ */
77
+ skip?: boolean,
78
+
79
+ /**
80
+ * When `true`, the effect will not reset the result to the loading status
81
+ * while executing if the requestId changes, instead, returning
82
+ * the existing result from before the change; otherwise, the result will
83
+ * be set to loading status.
84
+ *
85
+ * If the status is loading when the changes are made, it will remain as
86
+ * loading; old pending effects are discarded on changes and as such this
87
+ * value has no effect in that case.
88
+ */
89
+ retainResultOnChange?: boolean,
90
+
91
+ /**
92
+ * Callback that is invoked if the result for the given hook has changed.
93
+ *
94
+ * When defined, the hook will invoke this callback whenever it has reason
95
+ * to change the result and will not otherwise affect component rendering
96
+ * directly.
97
+ *
98
+ * When not defined, the hook will ensure the component re-renders to pick
99
+ * up the latest result.
100
+ */
101
+ onResultChanged?: (result: Result<TData>) => void,
102
+
103
+ /**
104
+ * Scope to use with the shared cache.
105
+ *
106
+ * When specified, the given scope will be used to isolate this hook's
107
+ * cached results. Otherwise, a shared default scope will be used.
108
+ *
109
+ * Changing this value after the first call is not supported.
110
+ */
111
+ scope?: string,
112
+ |};
113
+
114
+ const DefaultScope = "useHydratableEffect";
115
+
116
+ /**
117
+ * Hook to execute an async operation on server and client.
118
+ *
119
+ * This hook executes the given handler on the server and on the client,
120
+ * and, depending on the given options, can hydrate the server-side result.
121
+ *
122
+ * Results are cached on the client so they can be shared between equivalent
123
+ * invocations. Cache changes from one hook instance do not trigger renders
124
+ * in components that use the same requestID.
125
+ */
126
+ export const useHydratableEffect = <TData: ValidCacheData>(
127
+ requestId: string,
128
+ handler: () => Promise<TData>,
129
+ options: HydratableEffectOptions<TData> = ({}: $Shape<
130
+ HydratableEffectOptions<TData>,
131
+ >),
132
+ ): Result<TData> => {
133
+ const {
134
+ clientBehavior = WhenClientSide.ExecuteWhenNoSuccessResult,
135
+ skip = false,
136
+ retainResultOnChange = false,
137
+ onResultChanged,
138
+ scope = DefaultScope,
139
+ } = options;
140
+
141
+ // Now we instruct the server to perform the operation.
142
+ // When client-side, this will look up any response for hydration; it does
143
+ // not invoke the handler.
144
+ const serverResult = useServerEffect(
145
+ requestId,
146
+
147
+ // If we're skipped (unlikely in server worlds, but maybe),
148
+ // just give an aborted response.
149
+ skip ? () => Promise.reject(new AbortError("skipped")) : handler,
150
+
151
+ // Only hydrate if our behavior isn't telling us not to.
152
+ clientBehavior !== WhenClientSide.DoNotHydrate,
153
+ );
154
+
155
+ const getDefaultCacheValue: () => ?Result<TData> = React.useCallback(() => {
156
+ // If we don't have a requestId, it's our first render, the one
157
+ // where we hydrated. So defer to our clientBehavior value.
158
+ switch (clientBehavior) {
159
+ case WhenClientSide.DoNotHydrate:
160
+ case WhenClientSide.AlwaysExecute:
161
+ // Either we weren't hydrating at all, or we don't care
162
+ // if we hydrated something or not, either way, we're
163
+ // doing a request.
164
+ return null;
165
+
166
+ case WhenClientSide.ExecuteWhenNoResult:
167
+ // We only execute if we didn't hydrate something.
168
+ // So, returning the hydration result as default for our
169
+ // cache, will then prevent the cached effect running.
170
+ return serverResult;
171
+
172
+ case WhenClientSide.ExecuteWhenNoSuccessResult:
173
+ // We only execute if we didn't hydrate a success result.
174
+ if (serverResult?.status === "success") {
175
+ // So, returning the hydration result as default for our
176
+ // cache, will then prevent the cached effect running.
177
+ return serverResult;
178
+ }
179
+ return null;
180
+ }
181
+ // There is no reason for this to change after the first render.
182
+ // eslint-disable-next-line react-hooks/exhaustive-deps
183
+ }, []);
184
+
185
+ // Instead of using state, which would be local to just this hook instance,
186
+ // we use a shared in-memory cache.
187
+ useSharedCache<Result<TData>>(
188
+ requestId, // The key of the cached item
189
+ scope, // The scope of the cached items
190
+ getDefaultCacheValue,
191
+ );
192
+
193
+ // When we're client-side, we ultimately want the result from this call.
194
+ const clientResult = useCachedEffect(requestId, handler, {
195
+ skip,
196
+ onResultChanged,
197
+ retainResultOnChange,
198
+ scope,
199
+ });
200
+
201
+ // OK, now which result do we return.
202
+ // Well, we return the serverResult on our very first call and then
203
+ // the clientResult thereafter. The great thing is that after the very
204
+ // first call, the serverResult is going to be `null` anyway.
205
+ return serverResult ?? clientResult;
206
+ };
@@ -18,8 +18,8 @@ import type {ValidCacheData} from "../util/types.js";
18
18
  */
19
19
  export const useRequestInterception = <TData: ValidCacheData>(
20
20
  requestId: string,
21
- handler: () => Promise<?TData>,
22
- ): (() => Promise<?TData>) => {
21
+ handler: () => Promise<TData>,
22
+ ): (() => Promise<TData>) => {
23
23
  // Get the interceptors that have been registered.
24
24
  const interceptors = React.useContext(InterceptContext);
25
25
 
@@ -28,27 +28,24 @@ export const useRequestInterception = <TData: ValidCacheData>(
28
28
  // if nothing intercepted it.
29
29
  // We memoize this so that it only changes if something related to it
30
30
  // changes.
31
- const interceptedHandler = React.useMemo(
32
- () => (): Promise<?TData> => {
33
- // Call the interceptors from closest to furthest.
34
- // If one returns a non-null result, then we keep that.
35
- const interceptResponse = interceptors.reduceRight(
36
- (prev, interceptor) => {
37
- if (prev != null) {
38
- return prev;
39
- }
40
- return interceptor(requestId);
41
- },
42
- null,
43
- );
44
- // If nothing intercepted this request, invoke the original handler.
45
- // NOTE: We can't guarantee all interceptors return the same type
46
- // as our handler, so how can flow know? Let's just suppress that.
47
- // $FlowFixMe[incompatible-return]
48
- return interceptResponse ?? handler();
49
- },
50
- [handler, interceptors, requestId],
51
- );
31
+ const interceptedHandler = React.useCallback((): Promise<TData> => {
32
+ // Call the interceptors from closest to furthest.
33
+ // If one returns a non-null result, then we keep that.
34
+ const interceptResponse = interceptors.reduceRight(
35
+ (prev, interceptor) => {
36
+ if (prev != null) {
37
+ return prev;
38
+ }
39
+ return interceptor(requestId);
40
+ },
41
+ null,
42
+ );
43
+ // If nothing intercepted this request, invoke the original handler.
44
+ // NOTE: We can't guarantee all interceptors return the same type
45
+ // as our handler, so how can flow know? Let's just suppress that.
46
+ // $FlowFixMe[incompatible-return]
47
+ return interceptResponse ?? handler();
48
+ }, [handler, interceptors, requestId]);
52
49
 
53
50
  return interceptedHandler;
54
51
  };
@@ -3,8 +3,10 @@ import {Server} from "@khanacademy/wonder-blocks-core";
3
3
  import {useContext} from "react";
4
4
  import {TrackerContext} from "../util/request-tracking.js";
5
5
  import {SsrCache} from "../util/ssr-cache.js";
6
+ import {resultFromCachedResponse} from "../util/result-from-cache-response.js";
7
+ import {useRequestInterception} from "./use-request-interception.js";
6
8
 
7
- import type {CachedResponse, ValidCacheData} from "../util/types.js";
9
+ import type {Result, ValidCacheData} from "../util/types.js";
8
10
 
9
11
  /**
10
12
  * Hook to perform an asynchronous action during server-side rendering.
@@ -23,9 +25,13 @@ import type {CachedResponse, ValidCacheData} from "../util/types.js";
23
25
  */
24
26
  export const useServerEffect = <TData: ValidCacheData>(
25
27
  requestId: string,
26
- handler: () => Promise<?TData>,
28
+ handler: () => Promise<TData>,
27
29
  hydrate: boolean = true,
28
- ): ?CachedResponse<TData> => {
30
+ ): ?Result<TData> => {
31
+ // Plug in to the request interception framework for code that wants
32
+ // to use that.
33
+ const interceptedHandler = useRequestInterception(requestId, handler);
34
+
29
35
  // If we're server-side or hydrating, we'll have a cached entry to use.
30
36
  // So we get that and use it to initialize our state.
31
37
  // This works in both hydration and SSR because the very first call to
@@ -38,8 +44,9 @@ export const useServerEffect = <TData: ValidCacheData>(
38
44
  // initial value for the result state).
39
45
  const maybeTrack = useContext(TrackerContext);
40
46
  if (cachedResult == null && Server.isServerSide()) {
41
- maybeTrack?.(requestId, handler, hydrate);
47
+ maybeTrack?.(requestId, interceptedHandler, hydrate);
42
48
  }
43
49
 
44
- return cachedResult;
50
+ // A null result means there was no result to hydrate.
51
+ return cachedResult == null ? null : resultFromCachedResponse(cachedResult);
45
52
  };
@@ -1,6 +1,6 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
- import {KindError, Errors} from "@khanacademy/wonder-stuff-core";
3
+ import {DataError, DataErrors} from "../util/data-error.js";
4
4
  import {ScopedInMemoryCache} from "../util/scoped-in-memory-cache.js";
5
5
  import type {ValidCacheData} from "../util/types.js";
6
6
 
@@ -57,23 +57,23 @@ export const useSharedCache = <TValue: ValidCacheData>(
57
57
  ): [?TValue, CacheValueFn<TValue>] => {
58
58
  // Verify arguments.
59
59
  if (!id || typeof id !== "string") {
60
- throw new KindError(
60
+ throw new DataError(
61
61
  "id must be a non-empty string",
62
- Errors.InvalidInput,
62
+ DataErrors.InvalidInput,
63
63
  );
64
64
  }
65
65
 
66
66
  if (!scope || typeof scope !== "string") {
67
- throw new KindError(
67
+ throw new DataError(
68
68
  "scope must be a non-empty string",
69
- Errors.InvalidInput,
69
+ DataErrors.InvalidInput,
70
70
  );
71
71
  }
72
72
 
73
73
  // Memoize our APIs.
74
74
  // This one allows callers to set or replace the cached value.
75
- const cacheValue = React.useMemo(
76
- () => (value: ?TValue) =>
75
+ const cacheValue = React.useCallback(
76
+ (value: ?TValue) =>
77
77
  value == null
78
78
  ? cache.purge(scope, id)
79
79
  : cache.set(scope, id, value),
@@ -94,11 +94,13 @@ export const useSharedCache = <TValue: ValidCacheData>(
94
94
  const value =
95
95
  typeof initialValue === "function" ? initialValue() : initialValue;
96
96
 
97
- // Update the cache.
98
- cacheValue(value);
97
+ if (value != null) {
98
+ // Update the cache.
99
+ cacheValue(value);
99
100
 
100
- // Make sure we return this value as our current value.
101
- currentValue = value;
101
+ // Make sure we return this value as our current value.
102
+ currentValue = value;
103
+ }
102
104
  }
103
105
 
104
106
  // Now we have everything, let's return it.
package/src/index.js CHANGED
@@ -10,15 +10,33 @@ import type {
10
10
  } from "./util/types.js";
11
11
 
12
12
  export type {
13
+ ErrorOptions,
13
14
  ResponseCache,
14
15
  CachedResponse,
15
16
  Result,
16
17
  ScopedCache,
18
+ ValidCacheData,
17
19
  } from "./util/types.js";
18
20
 
21
+ /**
22
+ * Initialize the hydration cache.
23
+ *
24
+ * @param {ResponseCache} source The cache content to use for initializing the
25
+ * cache.
26
+ * @throws {Error} If the cache is already initialized.
27
+ */
19
28
  export const initializeCache = (source: ResponseCache): void =>
20
29
  SsrCache.Default.initialize(source);
21
30
 
31
+ /**
32
+ * Fulfill all tracked data requests.
33
+ *
34
+ * This is for use with the `TrackData` component during server-side rendering.
35
+ *
36
+ * @throws {Error} If executed outside of server-side rendering.
37
+ * @returns {Promise<void>} A promise that resolves when all tracked requests
38
+ * have been fulfilled.
39
+ */
22
40
  export const fulfillAllDataRequests = (): Promise<ResponseCache> => {
23
41
  if (!Server.isServerSide()) {
24
42
  return Promise.reject(
@@ -28,6 +46,15 @@ export const fulfillAllDataRequests = (): Promise<ResponseCache> => {
28
46
  return RequestTracker.Default.fulfillTrackedRequests();
29
47
  };
30
48
 
49
+ /**
50
+ * Indicate if there are unfulfilled tracked requests.
51
+ *
52
+ * This is used in conjunction with `TrackData`.
53
+ *
54
+ * @throws {Error} If executed outside of server-side rendering.
55
+ * @returns {boolean} `true` if there are unfulfilled tracked requests;
56
+ * otherwise, `false`.
57
+ */
31
58
  export const hasUnfulfilledRequests = (): boolean => {
32
59
  if (!Server.isServerSide()) {
33
60
  throw new Error("Data requests are not tracked when client-side");
@@ -35,9 +62,21 @@ export const hasUnfulfilledRequests = (): boolean => {
35
62
  return RequestTracker.Default.hasUnfulfilledRequests;
36
63
  };
37
64
 
65
+ /**
66
+ * Remove the request identified from the cached hydration responses.
67
+ *
68
+ * @param {string} id The request ID of the response to remove from the cache.
69
+ */
38
70
  export const removeFromCache = (id: string): boolean =>
39
71
  SsrCache.Default.remove(id);
40
72
 
73
+ /**
74
+ * Remove all cached hydration responses that match the given predicate.
75
+ *
76
+ * @param {(id: string) => boolean} [predicate] The predicate to match against
77
+ * the cached hydration responses. If no predicate is provided, all cached
78
+ * hydration responses will be removed.
79
+ */
41
80
  export const removeAllFromCache = (
42
81
  predicate?: (
43
82
  key: string,
@@ -48,16 +87,27 @@ export const removeAllFromCache = (
48
87
  export {default as TrackData} from "./components/track-data.js";
49
88
  export {default as Data} from "./components/data.js";
50
89
  export {default as InterceptRequests} from "./components/intercept-requests.js";
90
+ export {DataError, DataErrors} from "./util/data-error.js";
51
91
  export {useServerEffect} from "./hooks/use-server-effect.js";
52
- export {useRequestInterception} from "./hooks/use-request-interception.js";
92
+ export {useCachedEffect} from "./hooks/use-cached-effect.js";
53
93
  export {useSharedCache, clearSharedCache} from "./hooks/use-shared-cache.js";
94
+ export {
95
+ useHydratableEffect,
96
+ // TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
97
+ // have fixed:
98
+ // https://github.com/import-js/eslint-plugin-import/issues/2073
99
+ // eslint-disable-next-line import/named
100
+ WhenClientSide,
101
+ } from "./hooks/use-hydratable-effect.js";
54
102
  export {ScopedInMemoryCache} from "./util/scoped-in-memory-cache.js";
55
- export {resultFromCachedResponse} from "./util/result-from-cache-response.js";
103
+ export {SerializableInMemoryCache} from "./util/serializable-in-memory-cache.js";
104
+ export {RequestFulfillment} from "./util/request-fulfillment.js";
105
+ export {Status} from "./util/status.js";
56
106
 
57
107
  // GraphQL
58
108
  export {GqlRouter} from "./components/gql-router.js";
59
109
  export {useGql} from "./hooks/use-gql.js";
60
- export * from "./util/gql-error.js";
110
+ export {GqlError, GqlErrors} from "./util/gql-error.js";
61
111
  export type {
62
112
  GqlContext,
63
113
  GqlOperation,
@@ -0,0 +1,19 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`SerializableInMemoryCache #set should throw if the id is 1`] = `"id must be non-empty string"`;
4
+
5
+ exports[`SerializableInMemoryCache #set should throw if the id is [Function anonymous] 1`] = `"id must be non-empty string"`;
6
+
7
+ exports[`SerializableInMemoryCache #set should throw if the id is 5 1`] = `"id must be non-empty string"`;
8
+
9
+ exports[`SerializableInMemoryCache #set should throw if the id is null 1`] = `"id must be non-empty string"`;
10
+
11
+ exports[`SerializableInMemoryCache #set should throw if the scope is 1`] = `"scope must be non-empty string"`;
12
+
13
+ exports[`SerializableInMemoryCache #set should throw if the scope is [Function anonymous] 1`] = `"scope must be non-empty string"`;
14
+
15
+ exports[`SerializableInMemoryCache #set should throw if the scope is 5 1`] = `"scope must be non-empty string"`;
16
+
17
+ exports[`SerializableInMemoryCache #set should throw if the scope is null 1`] = `"scope must be non-empty string"`;
18
+
19
+ exports[`SerializableInMemoryCache #set should throw if the value is a function 1`] = `"value must be a non-function value"`;
@@ -0,0 +1,74 @@
1
+ // @flow
2
+ import {mergeGqlContext} from "../merge-gql-context.js";
3
+
4
+ describe("#mergeGqlContext", () => {
5
+ it("should combine the default context with the given overrides", () => {
6
+ // Arrange
7
+ const baseContext = {
8
+ foo: "bar",
9
+ };
10
+
11
+ // Act
12
+ const result = mergeGqlContext<any>(baseContext, {
13
+ fiz: "baz",
14
+ });
15
+
16
+ // Assert
17
+ expect(result).toStrictEqual({
18
+ foo: "bar",
19
+ fiz: "baz",
20
+ });
21
+ });
22
+
23
+ it("should overwrite values in the default context with the given overrides", () => {
24
+ // Arrange
25
+ const baseContext = {
26
+ foo: "bar",
27
+ };
28
+
29
+ // Act
30
+ const result = mergeGqlContext<any>(baseContext, {
31
+ foo: "boo",
32
+ });
33
+
34
+ // Assert
35
+ expect(result).toStrictEqual({
36
+ foo: "boo",
37
+ });
38
+ });
39
+
40
+ it("should not overwrite values in the default context with undefined values in the given overrides", () => {
41
+ // Arrange
42
+ const baseContext = {
43
+ foo: "bar",
44
+ };
45
+
46
+ // Act
47
+ const result = mergeGqlContext<any>(baseContext, {
48
+ foo: undefined,
49
+ });
50
+
51
+ // Assert
52
+ expect(result).toStrictEqual({
53
+ foo: "bar",
54
+ });
55
+ });
56
+
57
+ it("should delete values in the default context when the value is null in the given overrides", () => {
58
+ // Arrange
59
+ const baseContext = {
60
+ foo: "bar",
61
+ fiz: "baz",
62
+ };
63
+
64
+ // Act
65
+ const result = mergeGqlContext<any>(baseContext, {
66
+ fiz: null,
67
+ });
68
+
69
+ // Assert
70
+ expect(result).toStrictEqual({
71
+ foo: "bar",
72
+ });
73
+ });
74
+ });
@@ -1,6 +1,6 @@
1
1
  // @flow
2
- import {SsrCache} from "../ssr-cache.js";
3
2
  import {RequestFulfillment} from "../request-fulfillment.js";
3
+ import {DataError} from "../data-error.js";
4
4
 
5
5
  describe("RequestFulfillment", () => {
6
6
  it("should provide static default instance", () => {
@@ -15,62 +15,44 @@ describe("RequestFulfillment", () => {
15
15
  });
16
16
 
17
17
  describe("#fulfill", () => {
18
- it("should attempt to cache errors caused directly by handlers", async () => {
18
+ it("should resolve to an error result", async () => {
19
19
  // Arrange
20
- const responseCache = new SsrCache();
21
- const requestFulfillment = new RequestFulfillment(responseCache);
22
- const error = new Error("OH NO!");
23
- const fakeBadHandler = () => {
24
- throw error;
25
- };
26
- const cacheErrorSpy = jest.spyOn(responseCache, "cacheError");
20
+ const requestFulfillment = new RequestFulfillment();
21
+ const fakeBadRequestHandler = () => Promise.reject("OH NO!");
27
22
 
28
23
  // Act
29
- await requestFulfillment.fulfill("ID", {
30
- handler: fakeBadHandler,
24
+ const result = await requestFulfillment.fulfill("ID", {
25
+ handler: fakeBadRequestHandler,
31
26
  });
32
27
 
33
28
  // Assert
34
- expect(cacheErrorSpy).toHaveBeenCalledWith("ID", error, true);
29
+ expect(result).toStrictEqual({
30
+ status: "error",
31
+ error: expect.any(DataError),
32
+ });
35
33
  });
36
34
 
37
- it("should cache errors occurring in promises", async () => {
35
+ it("should resolve to an aborted result", async () => {
38
36
  // Arrange
39
- const responseCache = new SsrCache();
40
- const requestFulfillment = new RequestFulfillment(responseCache);
41
- const fakeBadRequestHandler = () =>
42
- new Promise((resolve, reject) => reject("OH NO!"));
43
- const cacheErrorSpy = jest.spyOn(responseCache, "cacheError");
37
+ const requestFulfillment = new RequestFulfillment();
38
+ const abortError = new Error("abort abort abort, awoooga");
39
+ abortError.name = "AbortError";
40
+ const fakeBadRequestHandler = () => Promise.reject(abortError);
44
41
 
45
42
  // Act
46
- await requestFulfillment.fulfill("ID", {
43
+ const result = await requestFulfillment.fulfill("ID", {
47
44
  handler: fakeBadRequestHandler,
48
45
  });
49
46
 
50
47
  // Assert
51
- expect(cacheErrorSpy).toHaveBeenCalledWith("ID", "OH NO!", true);
52
- });
53
-
54
- it("should cache data from requests", async () => {
55
- // Arrange
56
- const responseCache = new SsrCache();
57
- const requestFulfillment = new RequestFulfillment(responseCache);
58
- const fakeRequestHandler = () => Promise.resolve("DATA!");
59
- const cacheDataSpy = jest.spyOn(responseCache, "cacheData");
60
-
61
- // Act
62
- await requestFulfillment.fulfill("ID", {
63
- handler: fakeRequestHandler,
48
+ expect(result).toStrictEqual({
49
+ status: "aborted",
64
50
  });
65
-
66
- // Assert
67
- expect(cacheDataSpy).toHaveBeenCalledWith("ID", "DATA!", true);
68
51
  });
69
52
 
70
- it("should return a promise of the result", async () => {
53
+ it("should resolve to a data result", async () => {
71
54
  // Arrange
72
- const responseCache = new SsrCache();
73
- const requestFulfillment = new RequestFulfillment(responseCache);
55
+ const requestFulfillment = new RequestFulfillment();
74
56
  const fakeRequestHandler = () => Promise.resolve("DATA!");
75
57
 
76
58
  // Act
@@ -80,14 +62,14 @@ describe("RequestFulfillment", () => {
80
62
 
81
63
  // Assert
82
64
  expect(result).toStrictEqual({
65
+ status: "success",
83
66
  data: "DATA!",
84
67
  });
85
68
  });
86
69
 
87
70
  it("should reuse inflight requests", () => {
88
71
  // Arrange
89
- const responseCache = new SsrCache();
90
- const requestFulfillment = new RequestFulfillment(responseCache);
72
+ const requestFulfillment = new RequestFulfillment();
91
73
  const fakeRequestHandler = () => Promise.resolve("DATA!");
92
74
 
93
75
  // Act
@@ -104,8 +86,7 @@ describe("RequestFulfillment", () => {
104
86
 
105
87
  it("should remove inflight requests upon completion", async () => {
106
88
  // Arrange
107
- const responseCache = new SsrCache();
108
- const requestFulfillment = new RequestFulfillment(responseCache);
89
+ const requestFulfillment = new RequestFulfillment();
109
90
  const fakeRequestHandler = () => Promise.resolve("DATA!");
110
91
 
111
92
  // Act