@khanacademy/wonder-blocks-data 13.0.11 → 14.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 (66) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +5 -5
  3. package/src/components/__tests__/data.test.tsx +0 -832
  4. package/src/components/__tests__/gql-router.test.tsx +0 -63
  5. package/src/components/__tests__/intercept-requests.test.tsx +0 -57
  6. package/src/components/__tests__/track-data.test.tsx +0 -56
  7. package/src/components/data.ts +0 -73
  8. package/src/components/gql-router.tsx +0 -63
  9. package/src/components/intercept-context.ts +0 -19
  10. package/src/components/intercept-requests.tsx +0 -67
  11. package/src/components/track-data.tsx +0 -28
  12. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.ts.snap +0 -17
  13. package/src/hooks/__tests__/use-cached-effect.test.tsx +0 -789
  14. package/src/hooks/__tests__/use-gql-router-context.test.tsx +0 -132
  15. package/src/hooks/__tests__/use-gql.test.tsx +0 -204
  16. package/src/hooks/__tests__/use-hydratable-effect.test.ts +0 -708
  17. package/src/hooks/__tests__/use-request-interception.test.tsx +0 -254
  18. package/src/hooks/__tests__/use-server-effect.test.ts +0 -293
  19. package/src/hooks/__tests__/use-shared-cache.test.ts +0 -263
  20. package/src/hooks/use-cached-effect.ts +0 -297
  21. package/src/hooks/use-gql-router-context.ts +0 -49
  22. package/src/hooks/use-gql.ts +0 -58
  23. package/src/hooks/use-hydratable-effect.ts +0 -201
  24. package/src/hooks/use-request-interception.ts +0 -53
  25. package/src/hooks/use-server-effect.ts +0 -75
  26. package/src/hooks/use-shared-cache.ts +0 -107
  27. package/src/index.ts +0 -46
  28. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.ts.snap +0 -19
  29. package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.ts.snap +0 -19
  30. package/src/util/__tests__/get-gql-data-from-response.test.ts +0 -186
  31. package/src/util/__tests__/get-gql-request-id.test.ts +0 -132
  32. package/src/util/__tests__/graphql-document-node-parser.test.ts +0 -535
  33. package/src/util/__tests__/hydration-cache-api.test.ts +0 -34
  34. package/src/util/__tests__/merge-gql-context.test.ts +0 -73
  35. package/src/util/__tests__/purge-caches.test.ts +0 -28
  36. package/src/util/__tests__/request-api.test.ts +0 -176
  37. package/src/util/__tests__/request-fulfillment.test.ts +0 -146
  38. package/src/util/__tests__/request-tracking.test.tsx +0 -321
  39. package/src/util/__tests__/result-from-cache-response.test.ts +0 -79
  40. package/src/util/__tests__/scoped-in-memory-cache.test.ts +0 -316
  41. package/src/util/__tests__/serializable-in-memory-cache.test.ts +0 -397
  42. package/src/util/__tests__/ssr-cache.test.ts +0 -636
  43. package/src/util/__tests__/to-gql-operation.test.ts +0 -41
  44. package/src/util/data-error.ts +0 -63
  45. package/src/util/get-gql-data-from-response.ts +0 -65
  46. package/src/util/get-gql-request-id.ts +0 -106
  47. package/src/util/gql-error.ts +0 -43
  48. package/src/util/gql-router-context.ts +0 -9
  49. package/src/util/gql-types.ts +0 -64
  50. package/src/util/graphql-document-node-parser.ts +0 -132
  51. package/src/util/graphql-types.ts +0 -28
  52. package/src/util/hydration-cache-api.ts +0 -30
  53. package/src/util/merge-gql-context.ts +0 -35
  54. package/src/util/purge-caches.ts +0 -14
  55. package/src/util/request-api.ts +0 -65
  56. package/src/util/request-fulfillment.ts +0 -121
  57. package/src/util/request-tracking.ts +0 -211
  58. package/src/util/result-from-cache-response.ts +0 -30
  59. package/src/util/scoped-in-memory-cache.ts +0 -121
  60. package/src/util/serializable-in-memory-cache.ts +0 -44
  61. package/src/util/ssr-cache.ts +0 -193
  62. package/src/util/status.ts +0 -35
  63. package/src/util/to-gql-operation.ts +0 -43
  64. package/src/util/types.ts +0 -145
  65. package/tsconfig-build.json +0 -12
  66. package/tsconfig-build.tsbuildinfo +0 -1
@@ -1,121 +0,0 @@
1
- import type {Result, ValidCacheData} from "./types";
2
-
3
- import {DataError, DataErrors} from "./data-error";
4
-
5
- type RequestCache = {
6
- [id: string]: Promise<Result<any>>;
7
- };
8
-
9
- type FulfillOptions<TData extends ValidCacheData> = {
10
- handler: () => Promise<TData>;
11
- hydrate?: boolean;
12
- };
13
-
14
- let _default: RequestFulfillment;
15
-
16
- /**
17
- * This fulfills a request, making sure that in-flight requests are shared.
18
- */
19
- export class RequestFulfillment {
20
- static get Default(): RequestFulfillment {
21
- if (!_default) {
22
- _default = new RequestFulfillment();
23
- }
24
- return _default;
25
- }
26
-
27
- _requests: RequestCache = {};
28
-
29
- /**
30
- * Get a promise of a request for a given handler and options.
31
- *
32
- * This will return an inflight request if one exists, otherwise it will
33
- * make a new request. Inflight requests are deleted once they resolve.
34
- */
35
- fulfill: <TData extends ValidCacheData>(
36
- id: string,
37
- options: FulfillOptions<TData>,
38
- ) => Promise<Result<TData>> = <TData extends ValidCacheData>(
39
- id: string,
40
- {handler, hydrate = true}: FulfillOptions<TData>,
41
- ): Promise<Result<TData>> => {
42
- /**
43
- * If we have an inflight request, we'll provide that.
44
- */
45
- const inflight = this._requests[id];
46
- if (inflight) {
47
- return inflight;
48
- }
49
-
50
- /**
51
- * We don't have an inflight request, so let's set one up.
52
- */
53
- const request = handler()
54
- .then(
55
- (data: TData): Result<TData> => ({
56
- status: "success",
57
- data,
58
- }),
59
- )
60
- .catch((error: string | Error): Result<TData> => {
61
- const actualError =
62
- typeof error === "string"
63
- ? new DataError("Request failed", DataErrors.Unknown, {
64
- metadata: {
65
- unexpectedError: error,
66
- },
67
- })
68
- : error;
69
-
70
- // Return aborted result if the request was aborted.
71
- // The only way to detect this reliably, it seems, is to
72
- // check the error name and see if it's "AbortError" (this
73
- // is also what Apollo does).
74
- // Even then, it's reliant on the handler supporting aborts.
75
- // TODO(somewhatabstract, FEI-4276): Add first class abort
76
- // support to the handler API.
77
- if (actualError.name === "AbortError") {
78
- return {
79
- status: "aborted",
80
- };
81
- }
82
- return {
83
- status: "error",
84
- error: actualError,
85
- };
86
- })
87
- .finally(() => {
88
- delete this._requests[id];
89
- });
90
-
91
- // Store the request in our cache.
92
- this._requests[id] = request;
93
-
94
- return request;
95
- };
96
-
97
- /**
98
- * Abort an inflight request.
99
- *
100
- * NOTE: Currently, this does not perform an actual abort. It merely
101
- * removes the request from being tracked.
102
- */
103
- abort: (id: string) => void = (id) => {
104
- // TODO(somewhatabstract, FEI-4276): Add first class abort
105
- // support to the handler API.
106
- // For now, we will just clear the request out of the list.
107
- // When abort is implemented, the `finally` in the `fulfill` method
108
- // would handle the deletion.
109
- delete this._requests[id];
110
- };
111
-
112
- /**
113
- * Abort all inflight requests.
114
- *
115
- * NOTE: Currently, this does not perform actual aborts. It merely
116
- * removes the requests from our tracking.
117
- */
118
- abortAll: () => void = (): void => {
119
- Object.keys(this._requests).forEach((id) => this.abort(id));
120
- };
121
- }
@@ -1,211 +0,0 @@
1
- import * as React from "react";
2
- import {SsrCache} from "./ssr-cache";
3
- import {RequestFulfillment} from "./request-fulfillment";
4
-
5
- import type {ResponseCache, ValidCacheData} from "./types";
6
-
7
- type TrackerFn = <TData extends ValidCacheData>(
8
- id: string,
9
- handler: () => Promise<TData>,
10
- hydrate: boolean,
11
- ) => void;
12
-
13
- type RequestCache = {
14
- [id: string]: {
15
- hydrate: boolean;
16
- handler: () => Promise<any>;
17
- };
18
- };
19
-
20
- /**
21
- * Used to inject our tracking function into the render framework.
22
- *
23
- * INTERNAL USE ONLY
24
- */
25
- const TrackerContext: React.Context<TrackerFn | null | undefined> =
26
- React.createContext<TrackerFn | null | undefined>(null);
27
- TrackerContext.displayName = "TrackerContext";
28
- export {TrackerContext};
29
-
30
- /**
31
- * The default instance is stored here.
32
- * It's created below in the Default() static property.
33
- */
34
- let _default: RequestTracker;
35
-
36
- /**
37
- * Implements request tracking and fulfillment.
38
- *
39
- * INTERNAL USE ONLY
40
- */
41
- export class RequestTracker {
42
- static get Default(): RequestTracker {
43
- if (!_default) {
44
- _default = new RequestTracker();
45
- }
46
- return _default;
47
- }
48
-
49
- /**
50
- * These are the caches for tracked requests, their handlers, and responses.
51
- */
52
- _trackedRequests: RequestCache = {};
53
- _responseCache: SsrCache;
54
- _requestFulfillment: RequestFulfillment;
55
-
56
- constructor(responseCache?: SsrCache | null) {
57
- this._responseCache = responseCache || SsrCache.Default;
58
- this._requestFulfillment = new RequestFulfillment();
59
- }
60
-
61
- /**
62
- * Track a request.
63
- *
64
- * This method caches a request and its handler for use during server-side
65
- * rendering to allow us to fulfill requests before producing a final render.
66
- */
67
- trackDataRequest: <TData extends ValidCacheData>(
68
- id: string,
69
- handler: () => Promise<TData>,
70
- hydrate: boolean,
71
- ) => void = <TData extends ValidCacheData>(
72
- id: string,
73
- handler: () => Promise<TData>,
74
- hydrate: boolean,
75
- ): void => {
76
- /**
77
- * If we don't already have this tracked, then let's track it.
78
- */
79
- if (this._trackedRequests[id] == null) {
80
- this._trackedRequests[id] = {
81
- handler,
82
- hydrate,
83
- };
84
- }
85
- };
86
-
87
- /**
88
- * Reset our tracking info.
89
- */
90
- reset: () => void = () => {
91
- this._trackedRequests = {};
92
- };
93
-
94
- /**
95
- * Indicates if we have requests waiting to be fulfilled.
96
- */
97
- get hasUnfulfilledRequests(): boolean {
98
- return Object.keys(this._trackedRequests).length > 0;
99
- }
100
-
101
- /**
102
- * Initiate fulfillment of all tracked requests.
103
- *
104
- * This loops over the requests that were tracked using TrackData, and asks
105
- * the respective handlers to fulfill those requests in the order they were
106
- * tracked.
107
- *
108
- * Calling this method marks tracked requests as fulfilled; requests are
109
- * removed from the list of tracked requests by calling this method.
110
- *
111
- * @returns {Promise<ResponseCache>} The promise of the data that was
112
- * cached as a result of fulfilling the tracked requests.
113
- */
114
- fulfillTrackedRequests: () => Promise<ResponseCache> =
115
- (): Promise<ResponseCache> => {
116
- const promises = [];
117
- const {cacheData, cacheError} = this._responseCache;
118
-
119
- for (const requestKey of Object.keys(this._trackedRequests)) {
120
- const options = this._trackedRequests[requestKey];
121
-
122
- try {
123
- promises.push(
124
- this._requestFulfillment
125
- .fulfill(requestKey, {...options})
126
- .then((result) => {
127
- switch (result.status) {
128
- case "success":
129
- /**
130
- * Let's cache the data!
131
- *
132
- * NOTE: This only caches when we're
133
- * server side.
134
- */
135
- cacheData(
136
- requestKey,
137
- result.data,
138
- options.hydrate,
139
- );
140
- break;
141
-
142
- case "error":
143
- /**
144
- * Let's cache the error!
145
- *
146
- * NOTE: This only caches when we're
147
- * server side.
148
- */
149
- cacheError(
150
- requestKey,
151
- result.error,
152
- options.hydrate,
153
- );
154
- break;
155
- }
156
-
157
- // For status === "loading":
158
- // Could never get here unless we wrote
159
- // the code wrong. Rather than bloat
160
- // code with useless error, just ignore.
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
-
167
- // For status === "aborted":
168
- // We won't cache this.
169
- // We don't hydrate aborted requests,
170
- // so the client would just see them
171
- // as unfulfilled data.
172
- return;
173
- }),
174
- );
175
- } catch (e: any) {
176
- // This captures if there are problems in the code that
177
- // begins the requests.
178
- promises.push(
179
- Promise.resolve(
180
- cacheError(requestKey, e, options.hydrate),
181
- ),
182
- );
183
- }
184
- }
185
-
186
- /**
187
- * Clear out our tracked info.
188
- *
189
- * We call this now for a simpler API.
190
- *
191
- * If we reset the tracked calls after all promises resolve, any
192
- * request tracking done while promises are in flight would be lost.
193
- *
194
- * If we don't reset at all, then we have to expose the `reset` call
195
- * for consumers to use, or they'll only ever be able to accumulate
196
- * more and more tracked requests, having to fulfill them all every
197
- * time.
198
- *
199
- * Calling it here means we can have multiple "track -> request"
200
- * cycles in a row and in an easy to reason about manner.
201
- */
202
- this.reset();
203
-
204
- /**
205
- * Let's wait for everything to fulfill, and then clone the cached data.
206
- */
207
- return Promise.all(promises).then(() =>
208
- this._responseCache.cloneHydratableData(),
209
- );
210
- };
211
- }
@@ -1,30 +0,0 @@
1
- import {Status} from "./status";
2
- import {DataError, DataErrors} from "./data-error";
3
- import type {ValidCacheData, CachedResponse, Result} from "./types";
4
-
5
- /**
6
- * Turns a cache entry into a stateful result.
7
- */
8
- export const resultFromCachedResponse = <TData extends ValidCacheData>(
9
- cacheEntry?: CachedResponse<TData> | null,
10
- ): Result<TData> | null | undefined => {
11
- // No cache entry means no result to be hydrated.
12
- if (cacheEntry == null) {
13
- return null;
14
- }
15
-
16
- const {data, error} = cacheEntry;
17
- if (error != null) {
18
- // Let's hydrate the error. We don't persist everything about the
19
- // original error on the server, hence why we only superficially
20
- // hydrate it to a GqlHydratedError.
21
- return Status.error(new DataError(error, DataErrors.Hydrated));
22
- }
23
-
24
- if (data != null) {
25
- return Status.success(data);
26
- }
27
-
28
- // We shouldn't get here since we don't actually cache null data.
29
- return Status.aborted();
30
- };
@@ -1,121 +0,0 @@
1
- import {DataError, DataErrors} from "./data-error";
2
- import type {ScopedCache, RawScopedCache, ValidCacheData} from "./types";
3
-
4
- /**
5
- * Describe an in-memory cache.
6
- */
7
- export class ScopedInMemoryCache implements ScopedCache {
8
- _cache: RawScopedCache;
9
-
10
- constructor(initialCache: RawScopedCache = {}) {
11
- this._cache = initialCache;
12
- }
13
-
14
- /**
15
- * Indicate if this cache is being used or not.
16
- *
17
- * When the cache has entries, returns `true`; otherwise, returns `false`.
18
- */
19
- get inUse(): boolean {
20
- return Object.keys(this._cache).length > 0;
21
- }
22
-
23
- /**
24
- * Set a value in the cache.
25
- */
26
- set(scope: string, id: string, value: ValidCacheData): void {
27
- if (!id || typeof id !== "string") {
28
- throw new DataError(
29
- "id must be non-empty string",
30
- DataErrors.InvalidInput,
31
- );
32
- }
33
-
34
- if (!scope || typeof scope !== "string") {
35
- throw new DataError(
36
- "scope must be non-empty string",
37
- DataErrors.InvalidInput,
38
- );
39
- }
40
-
41
- if (typeof value === "function") {
42
- throw new DataError(
43
- "value must be a non-function value",
44
- DataErrors.InvalidInput,
45
- );
46
- }
47
-
48
- this._cache[scope] = this._cache[scope] ?? {};
49
- this._cache[scope][id] = value;
50
- }
51
-
52
- /**
53
- * Retrieve a value from the cache.
54
- */
55
- get(scope: string, id: string): ValidCacheData | null | undefined {
56
- return this._cache[scope]?.[id] ?? null;
57
- }
58
-
59
- /**
60
- * Purge an item from the cache.
61
- */
62
- purge(scope: string, id: string): void {
63
- if (!this._cache[scope]?.[id]) {
64
- return;
65
- }
66
- delete this._cache[scope][id];
67
- if (Object.keys(this._cache[scope]).length === 0) {
68
- delete this._cache[scope];
69
- }
70
- }
71
-
72
- /**
73
- * Purge a scope of items that match the given predicate.
74
- *
75
- * If the predicate is omitted, then all items in the scope are purged.
76
- */
77
- purgeScope(
78
- scope: string,
79
- predicate?: (id: string, value: ValidCacheData) => boolean,
80
- ): void {
81
- if (!this._cache[scope]) {
82
- return;
83
- }
84
-
85
- if (predicate == null) {
86
- delete this._cache[scope];
87
- return;
88
- }
89
-
90
- for (const key of Object.keys(this._cache[scope])) {
91
- if (predicate(key, this._cache[scope][key])) {
92
- delete this._cache[scope][key];
93
- }
94
- }
95
- if (Object.keys(this._cache[scope]).length === 0) {
96
- delete this._cache[scope];
97
- }
98
- }
99
-
100
- /**
101
- * Purge all items from the cache that match the given predicate.
102
- *
103
- * If the predicate is omitted, then all items in the cache are purged.
104
- */
105
- purgeAll(
106
- predicate?: (
107
- scope: string,
108
- id: string,
109
- value: ValidCacheData,
110
- ) => boolean,
111
- ): void {
112
- if (predicate == null) {
113
- this._cache = {};
114
- return;
115
- }
116
-
117
- for (const scope of Object.keys(this._cache)) {
118
- this.purgeScope(scope, (id, value) => predicate(scope, id, value));
119
- }
120
- }
121
- }
@@ -1,44 +0,0 @@
1
- import {clone} from "@khanacademy/wonder-stuff-core";
2
- import {DataError, DataErrors} from "./data-error";
3
- import {ScopedInMemoryCache} from "./scoped-in-memory-cache";
4
- import type {ValidCacheData, RawScopedCache} from "./types";
5
-
6
- /**
7
- * Describe a serializable in-memory cache.
8
- */
9
- export class SerializableInMemoryCache extends ScopedInMemoryCache {
10
- constructor(initialCache: RawScopedCache = {}) {
11
- try {
12
- super(clone(initialCache));
13
- } catch (e: any) {
14
- throw new DataError(
15
- `An error occurred trying to initialize from a response cache snapshot: ${e}`,
16
- DataErrors.InvalidInput,
17
- );
18
- }
19
- }
20
-
21
- /**
22
- * Set a value in the cache.
23
- */
24
- set(scope: string, id: string, value: ValidCacheData): void {
25
- super.set(scope, id, Object.freeze(clone(value)));
26
- }
27
-
28
- /**
29
- * Clone the cache.
30
- */
31
- clone(): RawScopedCache {
32
- try {
33
- return clone(this._cache);
34
- } catch (e: any) {
35
- throw new DataError(
36
- "An error occurred while trying to clone the cache",
37
- DataErrors.Internal,
38
- {
39
- cause: e,
40
- },
41
- );
42
- }
43
- }
44
- }