bend_puzzle/client/input/frame-scheduler.js
2026-07-31 12:54:46 +09:00

85 lines
3.3 KiB
JavaScript

(function(root,factory){
const api=factory();
if(typeof module==='object'&&module.exports)module.exports=api;
root.BendFrameScheduler=api;
})(typeof globalThis!=='undefined'?globalThis:this,function(){
'use strict';
const DEFAULT_INTERVAL=1000/60;
function createFrameScheduler(options={}){
const requestFrame=options.requestFrame||globalThis.requestAnimationFrame?.bind(globalThis);
const cancelFrame=options.cancelFrame||globalThis.cancelAnimationFrame?.bind(globalThis);
const setDelay=options.setDelay||globalThis.setTimeout?.bind(globalThis);
const clearDelay=options.clearDelay||globalThis.clearTimeout?.bind(globalThis);
const now=options.now||(()=>globalThis.performance?.now?.()||Date.now());
const interval=Math.max(1,Number(options.interval)||DEFAULT_INTERVAL);
const tolerance=Math.max(0,Number(options.tolerance)||0);
const watchdogDelay=Math.max(interval,Number(options.watchdogDelay)||interval+2);
const commit=options.commit;
const validTiming=typeof requestFrame==='function'&&typeof cancelFrame==='function'
&&typeof setDelay==='function'&&typeof clearDelay==='function';
if(!validTiming)throw new TypeError('Frame scheduler timing functions are required');
if(typeof commit!=='function')throw new TypeError('Frame scheduler commit callback is required');
let frame=0,timer=0,lastCommitAt=0,latest=null,revision=0,committedRevision=0,disposed=false;
const clearArmed=()=>{
if(frame)cancelFrame(frame);
if(timer)clearDelay(timer);
frame=0;timer=0;
};
const arm=()=>{
if(disposed||frame||timer||latest==null)return false;
const step=timestamp=>{
if(disposed||latest==null){clearArmed();return false}
const at=Number.isFinite(timestamp)?timestamp:now();
if(lastCommitAt&&at+tolerance<lastCommitAt+interval){
if(frame)cancelFrame(frame);
frame=requestFrame(step);
return false;
}
clearArmed();
const value=latest,currentRevision=revision;
latest=null;lastCommitAt=at;committedRevision=currentRevision;
commit(value,at,currentRevision);
if(latest!=null)arm();
return true;
};
frame=requestFrame(step);
timer=setDelay(()=>{
timer=0;
if(typeof options.onWatchdog==='function')options.onWatchdog();
step(now());
},watchdogDelay);
return true;
};
const push=value=>{
if(disposed)return false;
latest=value;revision++;arm();return revision;
};
const flush=()=>{
if(disposed||latest==null)return false;
clearArmed();
const value=latest,currentRevision=revision,at=now();
latest=null;lastCommitAt=at;committedRevision=currentRevision;
commit(value,at,currentRevision);
if(latest!=null)arm();
return true;
};
const cancel=({resetCadence=false}={})=>{
clearArmed();latest=null;
if(resetCadence)lastCommitAt=0;
};
const dispose=()=>{cancel({resetCadence:true});disposed=true};
const inspect=()=>Object.freeze({
armed:Boolean(frame||timer),
pending:latest!=null,
revision,
committedRevision,
lastCommitAt
});
return Object.freeze({push,flush,cancel,dispose,inspect});
}
return Object.freeze({DEFAULT_INTERVAL,createFrameScheduler});
});