main
js 172 lines 4.43 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 {__DEBUG__} from 'react-devtools-shared/src/constants';
11
12 import type {
13 Thenable,
14 FulfilledThenable,
15 RejectedThenable,
16 } from 'shared/ReactTypes';
17
18 import * as React from 'react';
19
20 const TIMEOUT = 30000;
21
22 type Module = any;
23 type ModuleLoaderFunction = () => Thenable<Module>;
24
25 // This is intentionally a module-level Map, rather than a React-managed one.
26 // Otherwise, refreshing the inspected element cache would also clear this cache.
27 // Modules are static anyway.
28 const moduleLoaderFunctionToModuleMap: Map<ModuleLoaderFunction, Module> =
29 new Map();
30 function readRecord<T>(record: Thenable<T>): T | null {
31 if (typeof React.use === 'function') {
32 try {
33 // eslint-disable-next-line react-hooks-published/rules-of-hooks
34 return React.use(record);
35 } catch (x) {
36 if (x === null) {
37 return null;
38 }
39 throw x;
40 }
41 }
42 if (record.status === 'fulfilled') {
43 return record.value;
44 } else if (record.status === 'rejected') {
45 return null;
46 } else {
47 throw record;
48 }
49 }
50
51 // TODO Flow type
52 export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
53 let record = moduleLoaderFunctionToModuleMap.get(moduleLoaderFunction);
54
55 // $FlowFixMe[constant-condition]
56 if (__DEBUG__) {
57 console.log(
58 `[dynamicImportCache] loadModule("${moduleLoaderFunction.name}")`,
59 );
60 }
61
62 if (!record) {
63 const callbacks = new Set<(value: any) => mixed>();
64 const rejectCallbacks = new Set<(reason: mixed) => mixed>();
65 const thenable: Thenable<Module> = {
66 status: 'pending',
67 value: null,
68 reason: null,
69 then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
70 callbacks.add(callback);
71 rejectCallbacks.add(reject);
72 },
73
74 // Optional property, read by React to name this I/O in async debug info:
75 displayName: `Loading module "${moduleLoaderFunction.name}"`,
76 };
77
78 const wake = () => {
79 if (timeoutID) {
80 clearTimeout(timeoutID);
81 timeoutID = null;
82 }
83
84 // This assumes they won't throw.
85 callbacks.forEach(callback => callback());
86 callbacks.clear();
87 rejectCallbacks.clear();
88 };
89 const wakeRejections = () => {
90 if (timeoutID) {
91 clearTimeout(timeoutID);
92 timeoutID = null;
93 }
94
95 // This assumes they won't throw.
96 rejectCallbacks.forEach(callback => callback((thenable as any).reason));
97 rejectCallbacks.clear();
98 callbacks.clear();
99 };
100
101 record = thenable;
102
103 let didTimeout = false;
104
105 moduleLoaderFunction().then(
106 module => {
107 // $FlowFixMe[constant-condition]
108 if (__DEBUG__) {
109 console.log(
110 `[dynamicImportCache] loadModule("${moduleLoaderFunction.name}") then()`,
111 );
112 }
113
114 if (didTimeout) {
115 return;
116 }
117
118 const fulfilledThenable: FulfilledThenable<Module> = thenable as any;
119 fulfilledThenable.status = 'fulfilled';
120 fulfilledThenable.value = module;
121
122 wake();
123 },
124 error => {
125 // $FlowFixMe[constant-condition]
126 if (__DEBUG__) {
127 console.log(
128 `[dynamicImportCache] loadModule("${moduleLoaderFunction.name}") catch()`,
129 );
130 }
131
132 if (didTimeout) {
133 return;
134 }
135
136 console.log(error);
137
138 const rejectedThenable: RejectedThenable<Module> = thenable as any;
139 rejectedThenable.status = 'rejected';
140 rejectedThenable.reason = error;
141
142 wakeRejections();
143 },
144 );
145
146 // Eventually timeout and stop trying to load the module.
147 let timeoutID: null | TimeoutID = setTimeout(function onTimeout() {
148 // $FlowFixMe[constant-condition]
149 if (__DEBUG__) {
150 console.log(
151 `[dynamicImportCache] loadModule("${moduleLoaderFunction.name}") onTimeout()`,
152 );
153 }
154
155 timeoutID = null;
156
157 didTimeout = true;
158
159 const rejectedThenable: RejectedThenable<Module> = thenable as any;
160 rejectedThenable.status = 'rejected';
161 rejectedThenable.reason = null;
162
163 wakeRejections();
164 }, TIMEOUT);
165
166 moduleLoaderFunctionToModuleMap.set(moduleLoaderFunction, record);
167 }
168
169 // $FlowFixMe[underconstrained-implicit-instantiation]
170 const response = readRecord(record);
171 return response;
172 }