All files / src scheduler.js

97.01% Statements 65/67
93.33% Branches 28/30
100% Functions 18/18
96.77% Lines 60/62

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188142x       10x         1540x 1540x                 11288915x     11288915x       11288915x 11288915x 11288915x   11288915x 11288915x 11288915x 11594983x 11594983x   11288915x 11288915x 11288915x 11288915x 8614519x   2674396x                 15982317x 11286656x   6423093x   4863563x   11286656x 7277408x   11286656x 11286656x               51014721x 51014721x 4670744x 4670744x 4670744x 4670744x   51014721x               22x 13x               188x 371x                 218x                 186x 186x                 15825x                   15825x 15825x 15825x 15825x 15825x 15825x       27276167x             4070x             11284845x 11284845x       4697472x         3855x         3176x 3176x               572x 531x   41x        
const MaxHeadroom = 0xffffffff;
 
export class Scheduler {
    static get MaxHeadroom() {
        return MaxHeadroom;
    }
 
    constructor() {
        /** @type {ScheduledTask|null} */
        this.scheduled = null;
        this.epoch = 0;
    }
 
    /**
     * Schedule a task to run after a delay.
     * @param {ScheduledTask} task
     * @param {number} delay
     */
    schedule(task, delay) {
        Iif (task.scheduler !== this) {
            throw new Error("Wrong scheduler for task, or non-task");
        }
        Iif (task.scheduled()) {
            throw new Error("Task is already scheduled");
        }
 
        const expireEpoch = delay + this.epoch;
        task.expireEpoch = expireEpoch;
        task._scheduled = true;
 
        let before = this.scheduled;
        let prev = null;
        while (before && before.expireEpoch <= expireEpoch) {
            prev = before;
            before = before.next;
        }
        task.next = before;
        task.prev = prev;
        if (task.next) task.next.prev = task;
        if (task.prev) {
            task.prev.next = task;
        } else {
            this.scheduled = task;
        }
    }
 
    /**
     * Cancel a task.
     * @param {ScheduledTask} task
     */
    cancel(task) {
        if (!task.scheduled()) return;
        if (!task.prev) {
            // First element, we need to update the head element.
            this.scheduled = task.next;
        } else {
            task.prev.next = task.next;
        }
        if (task.next) {
            task.next.prev = task.prev;
        }
        task.next = task.prev = null;
        task._scheduled = false;
    }
 
    /**
     * Run all tasks that are due in the next ticks.
     * @param {number} ticks number of cycles to run
     */
    polltime(ticks) {
        const targetEpoch = this.epoch + ticks;
        while (this.scheduled && this.scheduled.expireEpoch <= targetEpoch) {
            const head = this.scheduled;
            this.epoch = head.expireEpoch;
            head.cancel(); // cancel first
            head.onExpire(); // expiry may reschedule
        }
        this.epoch = targetEpoch;
    }
 
    /**
     * The minimum number of cycles that can be run without needing to polltime.
     * @returns {number} number of cycles
     */
    headroom() {
        if (this.scheduled === null) return MaxHeadroom;
        return this.scheduled.expireEpoch - this.epoch;
    }
 
    /**
     * Cancel all scheduled tasks.
     * Used during state restore - components will re-register their own tasks.
     */
    cancelAll() {
        while (this.scheduled) {
            this.scheduled.cancel();
        }
    }
 
    /**
     * Capture the scheduler's state for snapshotting.
     * @returns {{epoch: number}}
     */
    snapshotState() {
        return { epoch: this.epoch };
    }
 
    /**
     * Restore the scheduler's state from a snapshot.
     * Cancels all existing tasks - components must re-register theirs after this call.
     * @param {{epoch: number}} state
     */
    restoreState(state) {
        this.cancelAll();
        this.epoch = state.epoch;
    }
 
    /**
     * Create a new task.
     * @param {function(): void} onExpire function to call when the task expires
     * @returns {ScheduledTask} a handle to the new task
     */
    newTask(onExpire) {
        return new ScheduledTask(this, onExpire);
    }
}
 
class ScheduledTask {
    /**
     * @param {Scheduler} scheduler
     * @param {function(): void} onExpire
     */
    constructor(scheduler, onExpire) {
        this.scheduler = scheduler;
        this.prev = null;
        this.next = null;
        this.expireEpoch = 0;
        this.onExpire = onExpire;
        this._scheduled = false;
    }
 
    scheduled() {
        return this._scheduled;
    }
 
    /**
     * @param {number} delay
     */
    schedule(delay) {
        this.scheduler.schedule(this, delay);
    }
 
    /**
     * @param {number} delay
     */
    reschedule(delay) {
        this.scheduler.cancel(this);
        this.scheduler.schedule(this, delay);
    }
 
    cancel() {
        this.scheduler.cancel(this);
    }
 
    /** @returns {number|null} cycles until the task expires, or null if it is not scheduled */
    offset() {
        return this._scheduled ? this.expireEpoch - this.scheduler.epoch : null;
    }
 
    /** @param {number|null|undefined} offset from offset(); null or a missing value leaves the task unscheduled */
    restoreOffset(offset) {
        this.cancel();
        if (offset !== null && offset !== undefined) this.schedule(offset);
    }
 
    /**
     * @param {boolean} state
     * @param {number} delay
     */
    ensureScheduled(state, delay) {
        if (state) {
            if (!this.scheduled()) this.schedule(delay);
        } else {
            this.cancel();
        }
    }
}