@zedux/atoms
Version:
A Molecular State Engine for React
178 lines (177 loc) • 6.25 kB
JavaScript
export class Scheduler {
constructor(ecosystem) {
this.ecosystem = ecosystem;
// private _runStartTime?: number
/**
* The dynamic list of "full" jobs to run. Full jobs are:
*
* - EvaluateGraphNode (2)
* - UpdateExternalDependent (3)
* - RunEffect (4)
*/
this.jobs = [];
/**
* The dynamic list of "now" jobs to run. Now jobs are:
*
* - UpdateStore (0)
* - InformSubscribers(1)
*/
this.nows = [];
}
/**
* Kill any current timeout and run all jobs immediately.
*
* IMPORTANT: Setting and clearing timeouts is expensive. We need to always
* pass `shouldSetTimeout: false` to scheduler.schedule() when we're going
* to immediately flush
*/
flush() {
if (this._isRunning)
return; // already flushing
if (this._handle) {
clearTimeout(this._handle);
this._handle = undefined;
}
this.runJobs();
}
/**
* Insert a job into the queue. Insertion point depends on job's type and
* weight.
*
* IMPORTANT: Setting and clearing timeouts is expensive. We need to always
* pass `shouldSetTimeout: false` when we're going to immediately flush
*/
schedule(newJob, shouldSetTimeout = true) {
if (newJob.type === 4) {
// RunEffect (4) jobs run in any order, after everything else
this.jobs.push(newJob);
}
else {
this.insertJob(newJob);
}
// we just pushed the first job onto the queue
if (shouldSetTimeout && this.jobs.length === 1) {
this.setTimeout();
}
}
/**
* UpdateStore (0) jobs must run immediately but also need the scheduler to be
* running all "now" jobs.
*
* InformSubscriber (1) jobs must run immediately after the current task.
*/
scheduleNow(newJob) {
if (this._isRunningNows && newJob.type === 0)
return newJob.task();
this.nows[newJob.type === 1 ? 'push' : 'unshift'](newJob);
this.runJobs(true);
}
unschedule(task) {
const index = this.jobs.findIndex(job => job.task === task);
if (index !== -1)
this.jobs.splice(index, 1);
}
wipe() {
// allow external jobs to proceed. TODO: should we flush here?
this.jobs = this.jobs.filter(job => job.type === 3 // UpdateExternalDependent (3)
);
}
// An O(log n) replacement for this.jobs.findIndex()
findIndex(cb, index = Math.ceil(this.jobs.length / 2) - 1, iteration = 1) {
const job = this.jobs[index];
if (job == null)
return index;
const direction = cb(job);
if (!direction)
return index;
const divisor = Math.pow(2, iteration);
const isDone = divisor > this.jobs.length;
if (isDone) {
return index + (direction === 1 ? 1 : 0);
}
const effectualSize = Math.round(this.jobs.length / divisor);
const newIndex = Math.min(this.jobs.length - 1, Math.max(0, index + Math.ceil(effectualSize / 2) * direction));
return this.findIndex(cb, newIndex, iteration + 1);
}
/**
* Schedule an EvaluateGraphNode (2) or UpdateExternalDependent (3) job
*/
insertJob(newJob) {
var _a;
const { nodes } = this.ecosystem._graph;
const flags = (_a = newJob.flags) !== null && _a !== void 0 ? _a : 0;
const weight = newJob.id ? nodes[newJob.id].weight : 0;
const index = this.findIndex(job => {
if (job.type !== newJob.type)
return +(newJob.type - job.type > 0) || -1; // 1 or -1
// EvaluateGraphNode (2) jobs use weight comparison
if (job.id) {
const jobWeight = nodes[job.id].weight;
return weight < jobWeight ? -1 : +(weight > jobWeight); // + = 0 or 1
}
// UpdateExternalDependent (3) jobs use flags comparison
return flags < job.flags
? -1
: +(flags > job.flags);
});
if (index === -1) {
this.jobs.push(newJob);
}
else {
this.jobs.splice(index, 0, newJob);
}
}
/**
* Run either all "full" jobs or all "now" jobs. Since the jobs are split, we
* can essentially have two schedulers running at once. "Now" jobs must always
* run before any "full" jobs, so the "full" jobs runner has to flush any
* "now"s that come up while it's flushing "full"s.
*
* Don't run "full" jobs while "now"s are running. It leads to "now"s being
* deferred until after "full"s finish. This is backwards and can lead to
* reevaluation loops.
*/
runJobs(isNows) {
// we prevent this function from running at all if no "full" jobs are
// scheduled
if (this._isRunningNows) {
// schedule a "full" jobs run after "now"s finish
this._runAfterNows = !isNows;
return;
}
const jobs = isNows ? this.nows : this.jobs;
const runningKey = isNows
? '_isRunningNows'
: '_isRunning';
const nows = this.nows;
// this._runStartTime = performance.now()
// let counter = 0
this[runningKey] = true;
try {
while (jobs.length) {
const job = (nows.length ? nows : jobs).shift();
job.task();
// this "break" idea could only break for "full" jobs, not "now" jobs
// if (!(++counter % 20) && performance.now() - this._runStartTime >= 100) {
// setTimeout(() => this.runJobs())
// break
// }
}
}
finally {
this[runningKey] = false;
}
if (this._runAfterNows) {
this._runAfterNows = false;
this.runJobs();
}
}
setTimeout() {
if (this._isRunning)
return;
this._handle = setTimeout(() => {
this._handle = undefined;
this.runJobs();
});
}
}