main
js 191 lines 5.33 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {
11 ReactContext,
12 Thenable,
13 FulfilledThenable,
14 RejectedThenable,
15 } from 'shared/ReactTypes';
16
17 import * as React from 'react';
18 import {createContext} from 'react';
19
20 // TODO (cache) Remove this cache; it is outdated and will not work with newer APIs like startTransition.
21
22 // Cache implementation was forked from the React repo:
23 // https://github.com/facebook/react/blob/main/packages/react-cache/src/ReactCacheOld.js
24 //
25 // This cache is simpler than react-cache in that:
26 // 1. Individual items don't need to be invalidated.
27 // Profiling data is invalidated as a whole.
28 // 2. We didn't need the added overhead of an LRU cache.
29 // The size of this cache is bounded by how many renders were profiled,
30 // and it will be fully reset between profiling sessions.
31
32 export type {Thenable};
33
34 export type Resource<Input, Key, Value> = {
35 clear(): void,
36 invalidate(Key): void,
37 read(Input): Value,
38 preload(Input): void,
39 write(Key, Value): void,
40 };
41
42 let readContext;
43 if (typeof React.use === 'function') {
44 readContext = function (Context: ReactContext<null>) {
45 // eslint-disable-next-line react-hooks-published/rules-of-hooks
46 return React.use(Context);
47 };
48 } else if (
49 typeof (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ===
50 'object'
51 ) {
52 const ReactCurrentDispatcher = (React as any)
53 .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher;
54 readContext = function (Context: ReactContext<null>) {
55 const dispatcher = ReactCurrentDispatcher.current;
56 if (dispatcher === null) {
57 throw new Error(
58 'react-cache: read and preload may only be called from within a ' +
59 "component's render. They are not supported in event handlers or " +
60 'lifecycle methods.',
61 );
62 }
63 return dispatcher.readContext(Context);
64 };
65 } else {
66 throw new Error('react-cache: Unsupported React version');
67 }
68
69 const CacheContext = createContext(null);
70
71 type Config = {useWeakMap?: boolean, ...};
72
73 const entries: Map<
74 Resource<any, any, any>,
75 Map<any, any> | WeakMap<any, any>,
76 > = new Map();
77 const resourceConfigs: Map<Resource<any, any, any>, Config> = new Map();
78
79 function getEntriesForResource(
80 resource: any,
81 ): Map<any, any> | WeakMap<any, any> {
82 let entriesForResource: Map<any, any> | WeakMap<any, any> = entries.get(
83 resource,
84 ) as any as Map<any, any>;
85 if (entriesForResource === undefined) {
86 const config = resourceConfigs.get(resource);
87 entriesForResource =
88 config !== undefined && config.useWeakMap ? new WeakMap() : new Map();
89 entries.set(resource, entriesForResource);
90 }
91 return entriesForResource;
92 }
93
94 function accessResult<Input, Key, Value>(
95 resource: any,
96 fetch: Input => Thenable<Value>,
97 input: Input,
98 key: Key,
99 ): Thenable<Value> {
100 const entriesForResource = getEntriesForResource(resource);
101 const entry = entriesForResource.get(key);
102 if (entry === undefined) {
103 const thenable = fetch(input);
104 thenable.then(
105 value => {
106 const fulfilledThenable: FulfilledThenable<Value> = thenable as any;
107 fulfilledThenable.status = 'fulfilled';
108 fulfilledThenable.value = value;
109 },
110 error => {
111 const rejectedThenable: RejectedThenable<Value> = thenable as any;
112 rejectedThenable.status = 'rejected';
113 rejectedThenable.reason = error;
114 },
115 );
116 entriesForResource.set(key, thenable);
117 return thenable;
118 } else {
119 return entry;
120 }
121 }
122
123 export function createResource<Input, Key, Value>(
124 fetch: Input => Thenable<Value>,
125 hashInput: Input => Key,
126 config?: Config = {},
127 ): Resource<Input, Key, Value> {
128 const resource = {
129 clear(): void {
130 entries.delete(resource);
131 },
132
133 invalidate(key: Key): void {
134 const entriesForResource = getEntriesForResource(resource);
135 entriesForResource.delete(key);
136 },
137
138 read(input: Input): Value {
139 // Prevent access outside of render.
140 readContext(CacheContext);
141
142 const key = hashInput(input);
143 const result: Thenable<Value> = accessResult(resource, fetch, input, key);
144 if (typeof React.use === 'function') {
145 // eslint-disable-next-line react-hooks-published/rules-of-hooks
146 return React.use(result);
147 }
148
149 switch (result.status) {
150 case 'fulfilled': {
151 const value = result.value;
152 return value;
153 }
154 case 'rejected': {
155 const error = result.reason;
156 throw error;
157 }
158 default:
159 throw result;
160 }
161 },
162
163 preload(input: Input): void {
164 // Prevent access outside of render.
165 readContext(CacheContext);
166
167 const key = hashInput(input);
168 accessResult(resource, fetch, input, key);
169 },
170
171 write(key: Key, value: Value): void {
172 const entriesForResource = getEntriesForResource(resource);
173
174 const fulfilledThenable: FulfilledThenable<Value> = Promise.resolve(
175 value,
176 ) as any;
177 fulfilledThenable.status = 'fulfilled';
178 fulfilledThenable.value = value;
179
180 entriesForResource.set(key, fulfilledThenable);
181 },
182 };
183
184 resourceConfigs.set(resource, config);
185
186 return resource;
187 }
188
189 export function invalidateResources(): void {
190 entries.clear();
191 }