UNPKG

7.43 kBPlain TextView Raw
1'use strict';
2import type {
3 MapperRawInputs,
4 MapperOutputs,
5 SharedValue,
6} from './commonTypes';
7import { isJest } from './PlatformChecker';
8import { runOnUI } from './threads';
9import { isSharedValue } from './isSharedValue';
10
11const IS_JEST = isJest();
12
13type MapperExtractedInputs = SharedValue[];
14
15type Mapper = {
16 id: number;
17 dirty: boolean;
18 worklet: () => void;
19 inputs: MapperExtractedInputs;
20 outputs?: MapperOutputs;
21};
22
23function createMapperRegistry() {
24 'worklet';
25 const mappers = new Map<number, Mapper>();
26 let sortedMappers: Mapper[] = [];
27
28 let runRequested = false;
29 let processingMappers = false;
30
31 function updateMappersOrder() {
32 // sort mappers topologically
33 // the algorithm here takes adventage of a fact that the topological order
34 // of a transposed graph is a reverse topological order of the original graph
35 // The graph in our case consists of mappers and an edge between two mappers
36 // A and B exists if there is a shared value that's on A's output lists and on
37 // B's input list.
38 //
39 // We don't need however to calculate that graph as it is easier to work with
40 // the transposed version of it that can be calculated ad-hoc. For the transposed
41 // version to be traversed we use "pre" map that maps share value to mappers that
42 // output that shared value. Then we can infer all the outgoing edges for a given
43 // mapper simply by scanning it's input list and checking if any of the shared values
44 // from that list exists in the "pre" map. If they do, then we have an edge between
45 // that mapper and the mappers from the "pre" list for the given shared value.
46 //
47 // For topological sorting we use a dfs-based approach that requires the graph to
48 // be traversed in dfs order and each node after being processed lands at the
49 // beginning of the topological order list. Since we traverse a transposed graph,
50 // instead of reversing that order we can use a normal array and push processed
51 // mappers to the end. There is no need to reverse that array after we are done.
52 const pre = new Map(); // map from sv -> mapper that outputs that sv
53 mappers.forEach((mapper) => {
54 if (mapper.outputs) {
55 for (const output of mapper.outputs) {
56 const preMappers = pre.get(output);
57 if (preMappers === undefined) {
58 pre.set(output, [mapper]);
59 } else {
60 preMappers.push(mapper);
61 }
62 }
63 }
64 });
65 const visited = new Set();
66 const newOrder: Mapper[] = [];
67 function dfs(mapper: Mapper) {
68 visited.add(mapper);
69 for (const input of mapper.inputs) {
70 const preMappers = pre.get(input);
71 if (preMappers) {
72 for (const preMapper of preMappers) {
73 if (!visited.has(preMapper)) {
74 dfs(preMapper);
75 }
76 }
77 }
78 }
79 newOrder.push(mapper);
80 }
81 mappers.forEach((mapper) => {
82 if (!visited.has(mapper)) {
83 dfs(mapper);
84 }
85 });
86 sortedMappers = newOrder;
87 }
88
89 function mapperRun() {
90 runRequested = false;
91 if (processingMappers) {
92 return;
93 }
94 try {
95 processingMappers = true;
96 if (mappers.size !== sortedMappers.length) {
97 updateMappersOrder();
98 }
99 for (const mapper of sortedMappers) {
100 if (mapper.dirty) {
101 mapper.dirty = false;
102 mapper.worklet();
103 }
104 }
105 } finally {
106 processingMappers = false;
107 }
108 }
109
110 function maybeRequestUpdates() {
111 if (IS_JEST) {
112 // On Jest environment we avoid using queueMicrotask as that'd require test
113 // to advance the clock manually. This on other hand would require tests
114 // to know how many times mappers need to run. As we don't want tests to
115 // make any assumptions on that number it is easier to execute mappers
116 // immediately for testing purposes and only expect test to advance timers
117 // if they want to make any assertions on the effects of animations being run.
118 mapperRun();
119 } else if (!runRequested) {
120 if (processingMappers) {
121 // In general, we should avoid having mappers trigger updates as this may
122 // result in unpredictable behavior. Specifically, the updated value can
123 // be read by mappers that run later in the same frame but previous mappers
124 // would access the old value. Updating mappers during the mapper-run phase
125 // breaks the order in which we should execute the mappers. However, doing
126 // that is still a possibility and there are some instances where people use
127 // the API in that way, hence we need to prevent mapper-run phase falling into
128 // an infinite loop. We do that by detecting when mapper-run is requested while
129 // we are already in mapper-run phase, and in that case we use `requestAnimationFrame`
130 // instead of `queueMicrotask` which will schedule mapper run for the next
131 // frame instead of queuing another set of updates in the same frame.
132 requestAnimationFrame(mapperRun);
133 } else {
134 queueMicrotask(mapperRun);
135 }
136 runRequested = true;
137 }
138 }
139
140 function extractInputs(
141 inputs: unknown,
142 resultArray: MapperExtractedInputs
143 ): MapperExtractedInputs {
144 if (Array.isArray(inputs)) {
145 for (const input of inputs) {
146 input && extractInputs(input, resultArray);
147 }
148 } else if (isSharedValue(inputs)) {
149 resultArray.push(inputs);
150 } else if (Object.getPrototypeOf(inputs) === Object.prototype) {
151 // we only extract inputs recursively from "plain" objects here, if object
152 // is of a derivative class (e.g. HostObject on web, or Map) we don't scan
153 // it recursively
154 for (const element of Object.values(inputs as Record<string, unknown>)) {
155 element && extractInputs(element, resultArray);
156 }
157 }
158 return resultArray;
159 }
160
161 return {
162 start: (
163 mapperID: number,
164 worklet: () => void,
165 inputs: MapperRawInputs,
166 outputs?: MapperOutputs
167 ) => {
168 const mapper: Mapper = {
169 id: mapperID,
170 dirty: true,
171 worklet,
172 inputs: extractInputs(inputs, []),
173 outputs,
174 };
175 mappers.set(mapper.id, mapper);
176 sortedMappers = [];
177 for (const sv of mapper.inputs) {
178 sv.addListener(mapper.id, () => {
179 mapper.dirty = true;
180 maybeRequestUpdates();
181 });
182 }
183 maybeRequestUpdates();
184 },
185 stop: (mapperID: number) => {
186 const mapper = mappers.get(mapperID);
187 if (mapper) {
188 mappers.delete(mapper.id);
189 sortedMappers = [];
190 for (const sv of mapper.inputs) {
191 sv.removeListener(mapper.id);
192 }
193 }
194 },
195 };
196}
197
198let MAPPER_ID = 9999;
199
200export function startMapper(
201 worklet: () => void,
202 inputs: MapperRawInputs = [],
203 outputs: MapperOutputs = []
204): number {
205 const mapperID = (MAPPER_ID += 1);
206
207 runOnUI(() => {
208 let mapperRegistry = global.__mapperRegistry;
209 if (mapperRegistry === undefined) {
210 mapperRegistry = global.__mapperRegistry = createMapperRegistry();
211 }
212 mapperRegistry.start(mapperID, worklet, inputs, outputs);
213 })();
214
215 return mapperID;
216}
217
218export function stopMapper(mapperID: number): void {
219 runOnUI(() => {
220 const mapperRegistry = global.__mapperRegistry;
221 mapperRegistry?.stop(mapperID);
222 })();
223}