main
js 239 lines 6.62 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 {PriorityLevel} from '../SchedulerPriorities';
11
12 declare class TaskController {
13 constructor(options?: {priority?: string}): TaskController;
14 signal: mixed;
15 abort(): void;
16 }
17
18 type PostTaskPriorityLevel = 'user-blocking' | 'user-visible' | 'background';
19
20 type CallbackNode = {
21 _controller: TaskController,
22 };
23
24 import {
25 ImmediatePriority,
26 UserBlockingPriority,
27 NormalPriority,
28 LowPriority,
29 IdlePriority,
30 } from '../SchedulerPriorities';
31
32 export {
33 ImmediatePriority as unstable_ImmediatePriority,
34 UserBlockingPriority as unstable_UserBlockingPriority,
35 NormalPriority as unstable_NormalPriority,
36 IdlePriority as unstable_IdlePriority,
37 LowPriority as unstable_LowPriority,
38 };
39
40 // Capture local references to native APIs, in case a polyfill overrides them.
41 const perf = window.performance;
42 const setTimeout = window.setTimeout;
43
44 // Use experimental Chrome Scheduler postTask API.
45 const scheduler = global.scheduler;
46
47 const getCurrentTime: () => DOMHighResTimeStamp = perf.now.bind(perf);
48
49 export const unstable_now = getCurrentTime;
50
51 // Scheduler periodically yields in case there is other work on the main
52 // thread, like user events. By default, it yields multiple times per frame.
53 // It does not attempt to align with frame boundaries, since most tasks don't
54 // need to be frame aligned; for those that do, use requestAnimationFrame.
55 const yieldInterval = 5;
56 let deadline = 0;
57
58 let currentPriorityLevel_DEPRECATED: PriorityLevel = NormalPriority;
59
60 // Always yield at the end of the frame.
61 export function unstable_shouldYield(): boolean {
62 return getCurrentTime() >= deadline;
63 }
64
65 export function unstable_requestPaint() {
66 // Since we yield every frame regardless, `requestPaint` has no effect.
67 }
68
69 type SchedulerCallback<T> = (didTimeout_DEPRECATED: boolean) =>
70 | T
71 // May return a continuation
72 | SchedulerCallback<T>;
73
74 export function unstable_scheduleCallback<T>(
75 priorityLevel: PriorityLevel,
76 callback: SchedulerCallback<T>,
77 options?: {delay?: number},
78 ): CallbackNode {
79 let postTaskPriority: PostTaskPriorityLevel;
80 switch (priorityLevel) {
81 case ImmediatePriority:
82 case UserBlockingPriority:
83 postTaskPriority = 'user-blocking';
84 break;
85 case LowPriority:
86 case NormalPriority:
87 postTaskPriority = 'user-visible';
88 break;
89 case IdlePriority:
90 postTaskPriority = 'background';
91 break;
92 default:
93 postTaskPriority = 'user-visible';
94 break;
95 }
96
97 const controller = new TaskController({priority: postTaskPriority});
98 const postTaskOptions = {
99 // $FlowFixMe[invalid-compare]
100 delay: typeof options === 'object' && options !== null ? options.delay : 0,
101 signal: controller.signal,
102 };
103
104 const node = {
105 _controller: controller,
106 };
107
108 scheduler
109 .postTask(
110 runTask.bind(null, priorityLevel, postTaskPriority, node, callback),
111 postTaskOptions,
112 )
113 .catch(handleAbortError);
114
115 return node;
116 }
117
118 function runTask<T>(
119 priorityLevel: PriorityLevel,
120 postTaskPriority: PostTaskPriorityLevel,
121 node: CallbackNode,
122 callback: SchedulerCallback<T>,
123 ) {
124 deadline = getCurrentTime() + yieldInterval;
125 try {
126 currentPriorityLevel_DEPRECATED = priorityLevel;
127 const didTimeout_DEPRECATED = false;
128 const result = callback(didTimeout_DEPRECATED);
129 if (typeof result === 'function') {
130 // Assume this is a continuation
131 const continuation: SchedulerCallback<T> = result as any;
132 const continuationOptions = {
133 signal: node._controller.signal,
134 };
135
136 const nextTask = runTask.bind(
137 null,
138 priorityLevel,
139 postTaskPriority,
140 node,
141 continuation,
142 );
143
144 if (scheduler.yield !== undefined) {
145 scheduler
146 .yield(continuationOptions)
147 .then(nextTask)
148 .catch(handleAbortError);
149 } else {
150 scheduler
151 .postTask(nextTask, continuationOptions)
152 .catch(handleAbortError);
153 }
154 }
155 } catch (error) {
156 // We're inside a `postTask` promise. If we don't handle this error, then it
157 // will trigger an "Unhandled promise rejection" error. We don't want that,
158 // but we do want the default error reporting behavior that normal
159 // (non-Promise) tasks get for unhandled errors.
160 //
161 // So we'll re-throw the error inside a regular browser task.
162 setTimeout(() => {
163 throw error;
164 });
165 } finally {
166 currentPriorityLevel_DEPRECATED = NormalPriority;
167 }
168 }
169
170 function handleAbortError(error: any) {
171 // Abort errors are an implementation detail. We don't expose the
172 // TaskController to the user, nor do we expose the promise that is returned
173 // from `postTask`. So we should suppress them, since there's no way for the
174 // user to handle them.
175 }
176
177 export function unstable_cancelCallback(node: CallbackNode) {
178 const controller = node._controller;
179 controller.abort();
180 }
181
182 export function unstable_runWithPriority<T>(
183 priorityLevel: PriorityLevel,
184 callback: () => T,
185 ): T {
186 const previousPriorityLevel = currentPriorityLevel_DEPRECATED;
187 currentPriorityLevel_DEPRECATED = priorityLevel;
188 try {
189 return callback();
190 } finally {
191 currentPriorityLevel_DEPRECATED = previousPriorityLevel;
192 }
193 }
194
195 export function unstable_getCurrentPriorityLevel(): PriorityLevel {
196 return currentPriorityLevel_DEPRECATED;
197 }
198
199 export function unstable_next<T>(callback: () => T): T {
200 let priorityLevel: PriorityLevel;
201 switch (currentPriorityLevel_DEPRECATED) {
202 case ImmediatePriority:
203 case UserBlockingPriority:
204 case NormalPriority:
205 // Shift down to normal priority
206 priorityLevel = NormalPriority;
207 break;
208 default:
209 // Anything lower than normal priority should remain at the current level.
210 priorityLevel = currentPriorityLevel_DEPRECATED;
211 break;
212 }
213
214 const previousPriorityLevel = currentPriorityLevel_DEPRECATED;
215 currentPriorityLevel_DEPRECATED = priorityLevel;
216 try {
217 return callback();
218 } finally {
219 currentPriorityLevel_DEPRECATED = previousPriorityLevel;
220 }
221 }
222
223 export function unstable_wrapCallback<T>(callback: () => T): () => T {
224 const parentPriorityLevel = currentPriorityLevel_DEPRECATED;
225 return () => {
226 const previousPriorityLevel = currentPriorityLevel_DEPRECATED;
227 currentPriorityLevel_DEPRECATED = parentPriorityLevel;
228 try {
229 return callback();
230 } finally {
231 currentPriorityLevel_DEPRECATED = previousPriorityLevel;
232 }
233 };
234 }
235
236 export function unstable_forceFrameRate() {}
237
238 // Currently no profiling build
239 export const unstable_Profiling = null;