105 lines
3.4 KiB
JavaScript
105 lines
3.4 KiB
JavaScript
|
class Queue {
|
||
|
constructor() {
|
||
|
this.order = [];
|
||
|
this.scheduled = new Set();
|
||
|
}
|
||
|
add(process) {
|
||
|
if (!this.scheduled.has(process)) {
|
||
|
this.scheduled.add(process);
|
||
|
this.order.push(process);
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
remove(process) {
|
||
|
const index = this.order.indexOf(process);
|
||
|
if (index !== -1) {
|
||
|
this.order.splice(index, 1);
|
||
|
this.scheduled.delete(process);
|
||
|
}
|
||
|
}
|
||
|
clear() {
|
||
|
this.order.length = 0;
|
||
|
this.scheduled.clear();
|
||
|
}
|
||
|
}
|
||
|
function createRenderStep(runNextFrame) {
|
||
|
/**
|
||
|
* We create and reuse two queues, one to queue jobs for the current frame
|
||
|
* and one for the next. We reuse to avoid triggering GC after x frames.
|
||
|
*/
|
||
|
let thisFrame = new Queue();
|
||
|
let nextFrame = new Queue();
|
||
|
let numToRun = 0;
|
||
|
/**
|
||
|
* Track whether we're currently processing jobs in this step. This way
|
||
|
* we can decide whether to schedule new jobs for this frame or next.
|
||
|
*/
|
||
|
let isProcessing = false;
|
||
|
let flushNextFrame = false;
|
||
|
/**
|
||
|
* A set of processes which were marked keepAlive when scheduled.
|
||
|
*/
|
||
|
const toKeepAlive = new WeakSet();
|
||
|
const step = {
|
||
|
/**
|
||
|
* Schedule a process to run on the next frame.
|
||
|
*/
|
||
|
schedule: (callback, keepAlive = false, immediate = false) => {
|
||
|
const addToCurrentFrame = immediate && isProcessing;
|
||
|
const queue = addToCurrentFrame ? thisFrame : nextFrame;
|
||
|
if (keepAlive)
|
||
|
toKeepAlive.add(callback);
|
||
|
if (queue.add(callback) && addToCurrentFrame && isProcessing) {
|
||
|
// If we're adding it to the currently running queue, update its measured size
|
||
|
numToRun = thisFrame.order.length;
|
||
|
}
|
||
|
return callback;
|
||
|
},
|
||
|
/**
|
||
|
* Cancel the provided callback from running on the next frame.
|
||
|
*/
|
||
|
cancel: (callback) => {
|
||
|
nextFrame.remove(callback);
|
||
|
toKeepAlive.delete(callback);
|
||
|
},
|
||
|
/**
|
||
|
* Execute all schedule callbacks.
|
||
|
*/
|
||
|
process: (frameData) => {
|
||
|
/**
|
||
|
* If we're already processing we've probably been triggered by a flushSync
|
||
|
* inside an existing process. Instead of executing, mark flushNextFrame
|
||
|
* as true and ensure we flush the following frame at the end of this one.
|
||
|
*/
|
||
|
if (isProcessing) {
|
||
|
flushNextFrame = true;
|
||
|
return;
|
||
|
}
|
||
|
isProcessing = true;
|
||
|
[thisFrame, nextFrame] = [nextFrame, thisFrame];
|
||
|
// Clear the next frame queue
|
||
|
nextFrame.clear();
|
||
|
// Execute this frame
|
||
|
numToRun = thisFrame.order.length;
|
||
|
if (numToRun) {
|
||
|
for (let i = 0; i < numToRun; i++) {
|
||
|
const callback = thisFrame.order[i];
|
||
|
callback(frameData);
|
||
|
if (toKeepAlive.has(callback)) {
|
||
|
step.schedule(callback);
|
||
|
runNextFrame();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
isProcessing = false;
|
||
|
if (flushNextFrame) {
|
||
|
flushNextFrame = false;
|
||
|
step.process(frameData);
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
return step;
|
||
|
}
|
||
|
|
||
|
export { createRenderStep };
|