@khanacademy/wonder-blocks-data 11.0.16 → 13.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 (70) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/components/data.d.ts +2 -2
  3. package/dist/es/index.js +24 -18
  4. package/dist/hooks/use-gql.d.ts +5 -1
  5. package/dist/hooks/use-hydratable-effect.d.ts +6 -6
  6. package/dist/index.js +24 -18
  7. package/dist/util/status.d.ts +4 -3
  8. package/dist/util/types.d.ts +8 -6
  9. package/package.json +3 -3
  10. package/src/components/__tests__/data.test.tsx +6 -13
  11. package/src/components/data.ts +2 -4
  12. package/src/hooks/__tests__/use-cached-effect.test.tsx +79 -40
  13. package/src/hooks/__tests__/use-gql-router-context.test.tsx +1 -2
  14. package/src/hooks/__tests__/use-hydratable-effect.test.ts +1 -2
  15. package/src/hooks/__tests__/use-request-interception.test.tsx +2 -5
  16. package/src/hooks/__tests__/use-server-effect.test.ts +3 -6
  17. package/src/hooks/__tests__/use-shared-cache.test.ts +17 -13
  18. package/src/hooks/use-cached-effect.ts +24 -20
  19. package/src/hooks/use-gql.ts +12 -9
  20. package/src/hooks/use-hydratable-effect.ts +6 -7
  21. package/src/hooks/use-request-interception.ts +13 -11
  22. package/src/hooks/use-shared-cache.ts +4 -2
  23. package/src/util/__tests__/request-api.test.ts +2 -1
  24. package/src/util/__tests__/request-tracking.test.tsx +5 -9
  25. package/src/util/__tests__/result-from-cache-response.test.ts +2 -2
  26. package/src/util/__tests__/serializable-in-memory-cache.test.ts +1 -2
  27. package/src/util/__tests__/ssr-cache.test.ts +2 -4
  28. package/src/util/__tests__/to-gql-operation.test.ts +2 -4
  29. package/src/util/graphql-document-node-parser.ts +6 -6
  30. package/src/util/merge-gql-context.ts +2 -1
  31. package/src/util/request-tracking.ts +6 -2
  32. package/src/util/ssr-cache.ts +11 -8
  33. package/src/util/status.ts +6 -0
  34. package/src/util/types.ts +9 -7
  35. package/tsconfig-build.tsbuildinfo +1 -1
  36. package/dist/components/data.js.flow +0 -63
  37. package/dist/components/gql-router.js.flow +0 -33
  38. package/dist/components/intercept-context.js.flow +0 -18
  39. package/dist/components/intercept-requests.js.flow +0 -51
  40. package/dist/components/track-data.js.flow +0 -16
  41. package/dist/hooks/use-cached-effect.js.flow +0 -83
  42. package/dist/hooks/use-gql-router-context.js.flow +0 -14
  43. package/dist/hooks/use-gql.js.flow +0 -28
  44. package/dist/hooks/use-hydratable-effect.js.flow +0 -122
  45. package/dist/hooks/use-request-interception.js.flow +0 -24
  46. package/dist/hooks/use-server-effect.js.flow +0 -49
  47. package/dist/hooks/use-shared-cache.js.flow +0 -42
  48. package/dist/index.js.flow +0 -47
  49. package/dist/util/data-error.js.flow +0 -62
  50. package/dist/util/get-gql-data-from-response.js.flow +0 -12
  51. package/dist/util/get-gql-request-id.js.flow +0 -16
  52. package/dist/util/gql-error.js.flow +0 -41
  53. package/dist/util/gql-router-context.js.flow +0 -10
  54. package/dist/util/gql-types.js.flow +0 -50
  55. package/dist/util/graphql-document-node-parser.js.flow +0 -29
  56. package/dist/util/graphql-types.js.flow +0 -30
  57. package/dist/util/hydration-cache-api.js.flow +0 -29
  58. package/dist/util/merge-gql-context.js.flow +0 -18
  59. package/dist/util/purge-caches.js.flow +0 -14
  60. package/dist/util/request-api.js.flow +0 -33
  61. package/dist/util/request-fulfillment.js.flow +0 -48
  62. package/dist/util/request-tracking.js.flow +0 -81
  63. package/dist/util/result-from-cache-response.js.flow +0 -14
  64. package/dist/util/scoped-in-memory-cache.js.flow +0 -56
  65. package/dist/util/serializable-in-memory-cache.js.flow +0 -25
  66. package/dist/util/ssr-cache.js.flow +0 -86
  67. package/dist/util/status.js.flow +0 -17
  68. package/dist/util/to-gql-operation.js.flow +0 -41
  69. package/dist/util/types.js.flow +0 -142
  70. package/src/util/graphql-types.js.flow +0 -30
@@ -10,8 +10,7 @@ import type {Result, ValidCacheData} from "../util/types";
10
10
  /**
11
11
  * Policies to define how a hydratable effect should behave client-side.
12
12
  */
13
- // TODO(FEI-5000): Convert to TS enum after all codebases have been migrated
14
- export const WhenClientSide = {
13
+ export enum WhenClientSide {
15
14
  /**
16
15
  * The result from executing the effect server-side will not be hydrated.
17
16
  * The effect will always be executed client-side.
@@ -20,7 +19,7 @@ export const WhenClientSide = {
20
19
  * for properly hydrating this component (for example, the action invokes
21
20
  * Apollo which manages its own cache to ensure things render properly).
22
21
  */
23
- DoNotHydrate: "DoNotHydrate" as const,
22
+ DoNotHydrate = "DoNotHydrate",
24
23
 
25
24
  /**
26
25
  * The result from executing the effect server-side will be hydrated.
@@ -28,7 +27,7 @@ export const WhenClientSide = {
28
27
  * be hydrated (i.e. both error and success hydration results prevent the
29
28
  * effect running client-side).
30
29
  */
31
- ExecuteWhenNoResult: "ExecuteWhenNoResult" as const,
30
+ ExecuteWhenNoResult = "ExecuteWhenNoResult",
32
31
 
33
32
  /**
34
33
  * The result from executing the effect server-side will be hydrated.
@@ -37,15 +36,15 @@ export const WhenClientSide = {
37
36
  * If the hydrated result was not a success result, or there was no
38
37
  * hydrated result, the effect will not be executed.
39
38
  */
40
- ExecuteWhenNoSuccessResult: "ExecuteWhenNoSuccessResult" as const,
39
+ ExecuteWhenNoSuccessResult = "ExecuteWhenNoSuccessResult",
41
40
 
42
41
  /**
43
42
  * The result from executing the effect server-side will be hydrated.
44
43
  * The effect will always be executed client-side, regardless of the
45
44
  * hydrated result status.
46
45
  */
47
- AlwaysExecute: "AlwaysExecute" as const,
48
- } as const;
46
+ AlwaysExecute = "AlwaysExecute",
47
+ }
49
48
 
50
49
  type HydratableEffectOptions<TData extends ValidCacheData> = {
51
50
  /**
@@ -30,20 +30,22 @@ export const useRequestInterception = <TData extends ValidCacheData>(
30
30
  const interceptedHandler = React.useCallback((): Promise<TData> => {
31
31
  // Call the interceptors from closest to furthest.
32
32
  // If one returns a non-null result, then we keep that.
33
- const interceptResponse = interceptors.reduceRight(
34
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
35
- (prev, interceptor) => {
36
- if (prev != null) {
37
- return prev;
38
- }
39
- return interceptor(requestId);
40
- },
41
- null,
42
- );
33
+ const interceptResponse: Promise<TData> | null | undefined =
34
+ interceptors.reduceRight(
35
+ (prev: Promise<TData> | null | undefined, interceptor) => {
36
+ if (prev != null) {
37
+ return prev;
38
+ }
39
+ return interceptor(requestId) as
40
+ | Promise<TData>
41
+ | null
42
+ | undefined;
43
+ },
44
+ null,
45
+ );
43
46
  // If nothing intercepted this request, invoke the original handler.
44
47
  // NOTE: We can't guarantee all interceptors return the same type
45
48
  // as our handler, so how can TypeScript know? Let's just suppress that.
46
- // @ts-expect-error [FEI-5019] - TS2739 - Type '(requestId: string) => Promise<ValidCacheData | null | undefined> | null | undefined' is missing the following properties from type 'Promise<TData>': then, catch, finally, [Symbol.toStringTag]
47
49
  return interceptResponse ?? handler();
48
50
  }, [handler, interceptors, requestId]);
49
51
 
@@ -81,8 +81,10 @@ export const useSharedCache = <TValue extends ValidCacheData>(
81
81
  // since our last run through. Also, our cache does not know what type it
82
82
  // stores, so we have to cast it to the type we're exporting. This is a
83
83
  // dev time courtesy, rather than a runtime thing.
84
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'ValidCacheData | null | undefined' is not assignable to type 'TValue | null | undefined'.
85
- let currentValue: TValue | null | undefined = cache.get(scope, id);
84
+ let currentValue: TValue | null | undefined = cache.get(scope, id) as
85
+ | TValue
86
+ | null
87
+ | undefined;
86
88
 
87
89
  // If we have an initial value, we need to add it to the cache
88
90
  // and use it as our current value.
@@ -1,3 +1,5 @@
1
+ // eslint-disable-next-line import/no-unassigned-import
2
+ import "jest-extended";
1
3
  import {Server} from "@khanacademy/wonder-blocks-core";
2
4
  import {RequestFulfillment} from "../request-fulfillment";
3
5
  import {RequestTracker} from "../request-tracking";
@@ -123,7 +125,6 @@ describe("#hasTrackedRequestsToBeFetched", () => {
123
125
  const result = hasTrackedRequestsToBeFetched();
124
126
 
125
127
  // Assert
126
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'toBeTrue' does not exist on type 'JestMatchers<boolean>'.
127
128
  expect(result).toBeTrue();
128
129
  });
129
130
  });
@@ -162,13 +162,12 @@ describe("../request-tracking.js", () => {
162
162
  it("should cache errors occurring in promises", async () => {
163
163
  // Arrange
164
164
  const requestTracker = createRequestTracker();
165
- const fakeBadRequestHandler = () =>
165
+ const fakeBadRequestHandler = (): Promise<any> =>
166
166
  new Promise((resolve: any, reject: any) =>
167
167
  reject("OH NO!"),
168
168
  );
169
169
  requestTracker.trackDataRequest(
170
170
  "ID",
171
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => Promise<unknown>' is not assignable to parameter of type '() => Promise<ValidCacheData>'.
172
171
  fakeBadRequestHandler,
173
172
  true,
174
173
  );
@@ -192,23 +191,22 @@ describe("../request-tracking.js", () => {
192
191
  * - Handlers that reject the promise
193
192
  * - Handlers that resolve
194
193
  */
195
- const fakeBadRequestHandler = () =>
194
+ const fakeBadRequestHandler = (): Promise<any> =>
196
195
  new Promise((resolve: any, reject: any) =>
197
196
  reject("OH NO!"),
198
197
  );
199
- const fakeBadHandler = () => {
198
+ const fakeBadHandler = (): Promise<any> => {
200
199
  throw new Error("OH NO!");
201
200
  };
202
- const fakeValidHandler = (() => {
201
+ const fakeValidHandler = ((): (() => Promise<any>) => {
203
202
  let counter = 0;
204
- return (o: any) => {
203
+ return () => {
205
204
  counter++;
206
205
  return Promise.resolve(`DATA:${counter}`);
207
206
  };
208
207
  })();
209
208
  requestTracker.trackDataRequest(
210
209
  "BAD_REQUEST",
211
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => Promise<unknown>' is not assignable to parameter of type '() => Promise<ValidCacheData>'.
212
210
  fakeBadRequestHandler,
213
211
  true,
214
212
  );
@@ -219,13 +217,11 @@ describe("../request-tracking.js", () => {
219
217
  );
220
218
  requestTracker.trackDataRequest(
221
219
  "VALID_HANDLER1",
222
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '(o: any) => Promise<string>' is not assignable to parameter of type '() => Promise<string>'.
223
220
  fakeValidHandler,
224
221
  true,
225
222
  );
226
223
  requestTracker.trackDataRequest(
227
224
  "VALID_HANDLER2",
228
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '(o: any) => Promise<string>' is not assignable to parameter of type '() => Promise<string>'.
229
225
  fakeValidHandler,
230
226
  true,
231
227
  );
@@ -70,8 +70,8 @@ describe("#resultFromCachedResponse", () => {
70
70
  };
71
71
 
72
72
  // Act
73
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'error' does not exist on type 'Result<ValidCacheData> | null | undefined'.
74
- const {error} = resultFromCachedResponse(cacheEntry);
73
+ const cacheResult = resultFromCachedResponse(cacheEntry);
74
+ const error = cacheResult?.status === "error" && cacheResult.error;
75
75
 
76
76
  // Assert
77
77
  expect(error).toMatchInlineSnapshot(`[HydratedDataError: ERROR]`);
@@ -9,12 +9,11 @@ describe("SerializableInMemoryCache", () => {
9
9
  scope: {
10
10
  key: "value",
11
11
  },
12
- } as const;
12
+ };
13
13
 
14
14
  // Act
15
15
  const cache = new SerializableInMemoryCache(sourceData);
16
16
  // Try to mutate the cache.
17
- // @ts-expect-error [FEI-5019] - TS2540 - Cannot assign to 'scope' because it is a read-only property.
18
17
  sourceData["scope"] = {key: "SOME_NEW_DATA"};
19
18
  const result = cache.get("scope", "key");
20
19
 
@@ -125,12 +125,11 @@ describe("../ssr-cache.js", () => {
125
125
  const cache = new SsrCache();
126
126
  const sourceData = {
127
127
  MY_KEY: {data: "THE_DATA"},
128
- } as const;
128
+ };
129
129
 
130
130
  // Act
131
131
  cache.initialize(sourceData);
132
132
  // Try to mutate the cache.
133
- // @ts-expect-error [FEI-5019] - TS2540 - Cannot assign to 'MY_KEY' because it is a read-only property.
134
133
  sourceData["MY_KEY"] = {data: "SOME_NEW_DATA"};
135
134
  const result = cache.getEntry("MY_KEY");
136
135
 
@@ -448,8 +447,7 @@ describe("../ssr-cache.js", () => {
448
447
  const cloneSpy = jest
449
448
  .spyOn(hydrationCache, "clone")
450
449
  .mockReturnValue({
451
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'string' is not assignable to type '{ [id: string]: ValidCacheData; }'.
452
- default: "CLONE!",
450
+ default: "CLONE!" as any,
453
451
  });
454
452
  const cache = new SsrCache(hydrationCache);
455
453
  // Let's add to the initialized state to check that everything
@@ -9,11 +9,10 @@ describe("#toGqlOperation", () => {
9
9
  const documentNode: any = {};
10
10
  const parserSpy = jest
11
11
  .spyOn(GDNP, "graphQLDocumentNodeParser")
12
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '{ name: string; type: string; }' is not assignable to parameter of type 'IDocumentDefinition'.
13
12
  .mockReturnValue({
14
13
  name: "operationName",
15
14
  type: "query",
16
- });
15
+ } as any);
17
16
 
18
17
  // Act
19
18
  toGqlOperation(documentNode);
@@ -25,11 +24,10 @@ describe("#toGqlOperation", () => {
25
24
  it("should return the Wonder Blocks Data representation of the given document node", () => {
26
25
  // Arrange
27
26
  const documentNode: any = {};
28
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '{ name: string; type: string; }' is not assignable to parameter of type 'IDocumentDefinition'.
29
27
  jest.spyOn(GDNP, "graphQLDocumentNodeParser").mockReturnValue({
30
28
  name: "operationName",
31
29
  type: "mutation",
32
- });
30
+ } as any);
33
31
 
34
32
  // Act
35
33
  const result = toGqlOperation(documentNode);
@@ -59,20 +59,20 @@ export function graphQLDocumentNodeParser(
59
59
 
60
60
  const queries = document.definitions.filter(
61
61
  (x: DefinitionNode) =>
62
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'operation' does not exist on type 'DefinitionNode'.
63
- x.kind === "OperationDefinition" && x.operation === "query",
62
+ x.kind === "OperationDefinition" &&
63
+ (x as OperationDefinitionNode).operation === "query",
64
64
  );
65
65
 
66
66
  const mutations = document.definitions.filter(
67
67
  (x: DefinitionNode) =>
68
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'operation' does not exist on type 'DefinitionNode'.
69
- x.kind === "OperationDefinition" && x.operation === "mutation",
68
+ x.kind === "OperationDefinition" &&
69
+ (x as OperationDefinitionNode).operation === "mutation",
70
70
  );
71
71
 
72
72
  const subscriptions = document.definitions.filter(
73
73
  (x: DefinitionNode) =>
74
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'operation' does not exist on type 'DefinitionNode'.
75
- x.kind === "OperationDefinition" && x.operation === "subscription",
74
+ x.kind === "OperationDefinition" &&
75
+ (x as OperationDefinitionNode).operation === "subscription",
76
76
  );
77
77
 
78
78
  if (fragments.length && !queries.length && !mutations.length) {
@@ -23,7 +23,8 @@ export const mergeGqlContext = <TContext extends GqlContext>(
23
23
  delete acc[key];
24
24
  } else {
25
25
  // Otherwise, we set it.
26
- // @ts-expect-error [FEI-5019] - TS2536 - Type 'string' cannot be used to index type 'TContext'.
26
+ // @ts-expect-error TypeScript doesn't seem to see that
27
+ // TContext can have string keys.
27
28
  acc[key] = overrides[key];
28
29
  }
29
30
  }
@@ -53,8 +53,7 @@ export class RequestTracker {
53
53
  _responseCache: SsrCache;
54
54
  _requestFulfillment: RequestFulfillment;
55
55
 
56
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'undefined' is not assignable to type 'SsrCache | null'.
57
- constructor(responseCache: SsrCache | null = undefined) {
56
+ constructor(responseCache?: SsrCache | null) {
58
57
  this._responseCache = responseCache || SsrCache.Default;
59
58
  this._requestFulfillment = new RequestFulfillment();
60
59
  }
@@ -160,6 +159,11 @@ export class RequestTracker {
160
159
  // the code wrong. Rather than bloat
161
160
  // code with useless error, just ignore.
162
161
 
162
+ // For status === "no-data":
163
+ // Could never get here unless we wrote
164
+ // the code wrong. Rather than bloat
165
+ // code with useless error, just ignore.
166
+
163
167
  // For status === "aborted":
164
168
  // We won't cache this.
165
169
  // We don't hydrate aborted requests,
@@ -124,7 +124,7 @@ export class SsrCache {
124
124
  : null;
125
125
 
126
126
  // Now we defer to the SSR value, and fallback to the hydration cache.
127
- const internalEntry =
127
+ const internalEntry: ValidCacheData | null | undefined =
128
128
  ssrEntry ?? this._hydrationCache.get(DefaultScope, id);
129
129
 
130
130
  // If we are not server-side and we hydrated something, let's clear
@@ -140,8 +140,10 @@ export class SsrCache {
140
140
  }
141
141
  // Getting the typing right between the in-memory cache and this
142
142
  // is hard. Just telling TypeScript it's OK.
143
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'string | number | boolean | Record<any, any> | null | undefined' is not assignable to type 'Readonly<CachedResponse<TData>> | null | undefined'.
144
- return internalEntry;
143
+ return internalEntry as
144
+ | Readonly<CachedResponse<TData>>
145
+ | null
146
+ | undefined;
145
147
  };
146
148
 
147
149
  /**
@@ -161,9 +163,11 @@ export class SsrCache {
161
163
  const realPredicate = predicate
162
164
  ? // We know what we're putting into the cache so let's assume it
163
165
  // conforms.
164
- // @ts-expect-error [FEI-5019] - TS7006 - Parameter 'cachedEntry' implicitly has an 'any' type.
165
- (_: string, key: string, cachedEntry) =>
166
- predicate(key, cachedEntry)
166
+ (_: string, key: string, cachedEntry: ValidCacheData) =>
167
+ predicate(
168
+ key,
169
+ cachedEntry as Readonly<CachedResponse<ValidCacheData>>,
170
+ )
167
171
  : undefined;
168
172
 
169
173
  // Apply the predicate to what we have in our caches.
@@ -184,7 +188,6 @@ export class SsrCache {
184
188
  // to an empty object.
185
189
  // We only need the default scope out of our scoped in-memory cache.
186
190
  // We know that it conforms to our expectations.
187
- // @ts-expect-error [FEI-5019] - TS2322 - Type '{ [id: string]: ValidCacheData; }' is not assignable to type 'ResponseCache'.
188
- return cache[DefaultScope] ?? {};
191
+ return (cache[DefaultScope] as ResponseCache) ?? {};
189
192
  };
190
193
  }
@@ -4,6 +4,10 @@ const loadingStatus = Object.freeze({
4
4
  status: "loading",
5
5
  });
6
6
 
7
+ const noDataStatus = Object.freeze({
8
+ status: "no-data",
9
+ });
10
+
7
11
  const abortedStatus = Object.freeze({
8
12
  status: "aborted",
9
13
  });
@@ -14,6 +18,8 @@ const abortedStatus = Object.freeze({
14
18
  export const Status = Object.freeze({
15
19
  loading: <TData extends ValidCacheData = ValidCacheData>(): Result<TData> =>
16
20
  loadingStatus,
21
+ noData: <TData extends ValidCacheData = ValidCacheData>(): Result<TData> =>
22
+ noDataStatus,
17
23
  aborted: <TData extends ValidCacheData = ValidCacheData>(): Result<TData> =>
18
24
  abortedStatus,
19
25
  success: <TData extends ValidCacheData>(data: TData): Result<TData> => ({
package/src/util/types.ts CHANGED
@@ -3,30 +3,29 @@ import type {Metadata} from "@khanacademy/wonder-stuff-core";
3
3
  /**
4
4
  * Defines the various fetch policies that can be applied to requests.
5
5
  */
6
- // TODO(FEI-5000): Convert to TS enum after all codebases have been migrated
7
- export const FetchPolicy = {
6
+ export enum FetchPolicy {
8
7
  /**
9
8
  * If the data is in the cache, return that; otherwise, fetch from the
10
9
  * server.
11
10
  */
12
- CacheBeforeNetwork: "CacheBeforeNetwork" as const,
11
+ CacheBeforeNetwork = "CacheBeforeNetwork",
13
12
 
14
13
  /**
15
14
  * If the data is in the cache, return that; always fetch from the server
16
15
  * regardless of cache.
17
16
  */
18
- CacheAndNetwork: "CacheAndNetwork" as const,
17
+ CacheAndNetwork = "CacheAndNetwork",
19
18
 
20
19
  /**
21
20
  * If the data is in the cache, return that; otherwise, do nothing.
22
21
  */
23
- CacheOnly: "CacheOnly" as const,
22
+ CacheOnly = "CacheOnly",
24
23
 
25
24
  /**
26
25
  * Ignore any existing cached result; always fetch from the server.
27
26
  */
28
- NetworkOnly: "NetworkOnly" as const,
29
- } as const;
27
+ NetworkOnly = "NetworkOnly",
28
+ }
30
29
 
31
30
  /**
32
31
  * Define what can be cached.
@@ -48,6 +47,9 @@ export type Result<TData extends ValidCacheData> =
48
47
  | {
49
48
  status: "loading";
50
49
  }
50
+ | {
51
+ status: "no-data";
52
+ }
51
53
  | {
52
54
  status: "success";
53
55
  data: TData;