motion-dom
Version:
102 lines (100 loc) • 3.8 kB
JavaScript
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 Set();
let nextFrame = new Set();
/**
* 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;
/**
* The callback currently running. If it schedules itself with `immediate`
* it's left in place, so as before it doesn't run again this step.
*/
let running;
/**
* A set of processes which were marked keepAlive when scheduled.
* A keepAlive process is always also held by a frame queue until
* it's cancelled, so a Set has the same lifetime semantics as a
* WeakSet here while being considerably faster to query every frame.
*/
const toKeepAlive = new Set();
let latestFrameData = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
function triggerCallback(callback) {
if (toKeepAlive.has(callback)) {
nextFrame.add(callback);
runNextFrame();
}
(running = callback)(latestFrameData);
}
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);
/**
* A callback scheduled with `immediate` during processing may
* already have run this step (for instance, a value it reads was
* changed by a later callback). Re-adding moves it to the end of
* the queue, so it runs again. The running callback is excluded,
* so one that re-schedules itself can't loop forever.
*/
addToCurrentFrame && callback !== running && queue.delete(callback);
queue.add(callback);
return callback;
},
/**
* Cancel the provided callback from running on the next frame.
*/
cancel: (callback) => {
nextFrame.delete(callback);
toKeepAlive.delete(callback);
},
/**
* Execute all schedule callbacks.
*/
process: (frameData) => {
latestFrameData = 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;
// Swap this frame and the next to avoid GC
const prevFrame = thisFrame;
thisFrame = nextFrame;
nextFrame = prevFrame;
// Execute this frame
thisFrame.forEach(triggerCallback);
// Clear the frame so no callbacks remain. This is to avoid
// memory leaks should this render step not run for a while.
thisFrame.clear();
running = undefined;
isProcessing = false;
if (flushNextFrame) {
flushNextFrame = false;
step.process(frameData);
}
},
};
return step;
}
export { createRenderStep };
//# sourceMappingURL=render-step.mjs.map