main
js 191 lines 4.96 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 {ReactContext, Thenable} from 'shared/ReactTypes';
11
12 import * as React from 'react';
13
14 import {createLRU} from './LRU';
15
16 interface Suspender {
17 then(resolve: () => mixed, reject: () => mixed): mixed;
18 }
19
20 type PendingResult = {
21 status: 0,
22 value: Suspender,
23 };
24
25 type ResolvedResult<V> = {
26 status: 1,
27 value: V,
28 };
29
30 type RejectedResult = {
31 status: 2,
32 value: mixed,
33 };
34
35 type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
36
37 type Resource<I, V> = {
38 read(I): V,
39 preload(I): void,
40 ...
41 };
42
43 const Pending = 0;
44 const Resolved = 1;
45 const Rejected = 2;
46
47 const SharedInternals =
48 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
49
50 function readContext(Context: ReactContext<mixed>) {
51 const dispatcher = SharedInternals.H;
52 if (dispatcher === null) {
53 // This wasn't being minified but we're going to retire this package anyway.
54 // eslint-disable-next-line react-internal/prod-error-codes
55 throw new Error(
56 'react-cache: read and preload may only be called from within a ' +
57 "component's render. They are not supported in event handlers or " +
58 'lifecycle methods.',
59 );
60 }
61 return dispatcher.readContext(Context);
62 }
63
64 // $FlowFixMe[missing-local-annot]
65 function identityHashFn(input) {
66 if (__DEV__) {
67 if (
68 typeof input !== 'string' &&
69 typeof input !== 'number' &&
70 typeof input !== 'boolean' &&
71 input !== undefined &&
72 input !== null
73 ) {
74 console.error(
75 'Invalid key type. Expected a string, number, symbol, or boolean, ' +
76 'but instead received: %s' +
77 '\n\nTo use non-primitive values as keys, you must pass a hash ' +
78 'function as the second argument to createResource().',
79 input,
80 );
81 }
82 }
83 return input;
84 }
85
86 const CACHE_LIMIT = 500;
87 const lru = createLRU<$FlowFixMe>(CACHE_LIMIT);
88
89 const entries: Map<Resource<any, any>, Map<any, any>> = new Map();
90
91 const CacheContext = React.createContext<mixed>(null);
92
93 function accessResult<I, K, V>(
94 resource: any,
95 fetch: I => Thenable<V>,
96 input: I,
97 key: K,
98 ): Result<V> {
99 let entriesForResource = entries.get(resource);
100 if (entriesForResource === undefined) {
101 entriesForResource = new Map();
102 entries.set(resource, entriesForResource);
103 }
104 const entry = entriesForResource.get(key);
105 if (entry === undefined) {
106 const thenable = fetch(input);
107 thenable.then(
108 value => {
109 if (newResult.status === Pending) {
110 const resolvedResult: ResolvedResult<V> = newResult as any;
111 resolvedResult.status = Resolved;
112 resolvedResult.value = value;
113 }
114 },
115 error => {
116 if (newResult.status === Pending) {
117 const rejectedResult: RejectedResult = newResult as any;
118 rejectedResult.status = Rejected;
119 rejectedResult.value = error;
120 }
121 },
122 );
123 const newResult: PendingResult = {
124 status: Pending,
125 value: thenable,
126 };
127 const newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key));
128 entriesForResource.set(key, newEntry);
129 return newResult;
130 } else {
131 return lru.access(entry) as any;
132 }
133 }
134
135 function deleteEntry(resource: any, key: mixed) {
136 const entriesForResource = entries.get(resource);
137 if (entriesForResource !== undefined) {
138 entriesForResource.delete(key);
139 if (entriesForResource.size === 0) {
140 entries.delete(resource);
141 }
142 }
143 }
144
145 export function unstable_createResource<I, K: string | number, V>(
146 fetch: I => Thenable<V>,
147 maybeHashInput?: I => K,
148 ): Resource<I, V> {
149 const hashInput: I => K =
150 maybeHashInput !== undefined ? maybeHashInput : (identityHashFn as any);
151
152 const resource = {
153 read(input: I): V {
154 // react-cache currently doesn't rely on context, but it may in the
155 // future, so we read anyway to prevent access outside of render.
156 readContext(CacheContext);
157 const key = hashInput(input);
158 const result: Result<V> = accessResult(resource, fetch, input, key);
159 switch (result.status) {
160 case Pending: {
161 const suspender = result.value;
162 throw suspender;
163 }
164 case Resolved: {
165 const value = result.value;
166 return value;
167 }
168 case Rejected: {
169 const error = result.value;
170 throw error;
171 }
172 default:
173 // Should be unreachable
174 return undefined as any;
175 }
176 },
177
178 preload(input: I): void {
179 // react-cache currently doesn't rely on context, but it may in the
180 // future, so we read anyway to prevent access outside of render.
181 readContext(CacheContext);
182 const key = hashInput(input);
183 accessResult(resource, fetch, input, key);
184 },
185 };
186 return resource;
187 }
188
189 export function unstable_setGlobalCacheLimit(limit: number) {
190 lru.setLimit(limit);
191 }