UNPKG

@zedux/atoms

Version:

A Molecular State Engine for React

182 lines (181 loc) 6.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Scheduler = void 0; 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(); }); } } exports.Scheduler = Scheduler;