locks.js

/* eslint no-unsafe-finally: "off" */

import {Future} from './futures.js';


/**
 * A classic multitasking Condition mechanism.
 *
 * @param {Lock} [lock] - A shared lock object that is used to synchronize multiple Conditions.
 * @borrows {Lock.acquire} as foo
 * @borrows Lock.release as bar
 *
 * @example
 * const cond = new Condition();
 * await cond.acquire();
 * setTimeout(() => cond.notify(), 1000);
 * await cond.wait(); // will wait for 1000ms
 * // do work...
 * cond.release();
 * @see Python's [asyncio.Condition]{@link https://docs.python.org/3/library/asyncio-sync.html#condition}
 */
export class Condition {

    constructor(lock) {
        if (lock === undefined) {
            lock = new Lock();
        }
        this._lock = lock;
        this.release = lock.release.bind(lock);
        this._waiters = [];
    }

    /**
     * The internal lock state.
     *
     * @see [Lock.locked]{@link Lock#locked}
     *
     * @returns {boolean}
     */
    locked() {
        return this.lock.locked();
    }

    /**
     * Acquire the internal lock.
     *
     * @see [Lock.acquire]{@link Lock#acquire}
     */
    async acquire() {
        return await this.lock.acquire();
    }

    /**
     * Release the internal lock.
     *
     * @see [Lock.release]{@link Lock#release}
     */
    release() {
        return this.lock.release();
    }

    /**
     * Wait until the condition is satisfied.  When multiple awaiters exist they will
     * be woken up one at a time if [notify]{@link Condition#notify} is used.  If
     * [notifyAll]{@link Condition#notifyAll} is used then all awaiters will be woken up.
     * Once completed the internal {@link Lock} is reacquired.
     */
    async wait() {
        if (!this.locked()) {
            throw new Error('Lock not acquired');
        }
        this.release();
        try {
            const f = new Future();
            this._waiters.push(f);
            try {
                return await f;
            } finally {
                this._waiters.splice(this._waiters.indexOf(f), 1);
            }
        } finally {
            await this.acquire();
        }
    }

    /**
     * Wake up any awaiters using [wait]{@link Condition#wait}.
     *
     * @param {Number} [n=1] - The number of awaiters to wake up.
     */
    notify(n=1) {
        if (!this.locked()) {
            throw new Error('Lock not acquired');
        }
        let idx = 0;
        for (const f of this._waiters) {
            if (idx >= n) {
                break;
            }
            if (!f.done()) {
                idx++;
                f.setResult(true);
            }
        }
    }

    /**
     * Wake up ALL awaiters using [wait]{@link Condition#wait}.
     */
    notifyAll() {
        this.notify(this._waiters.length);
    }
}


/**
 * A classic multitasking lock mechanism.
 *
 * @see Python's [asyncio.Lock]{@link https://docs.python.org/3/library/asyncio-sync.html#lock}
 */
export class Lock {
    constructor() {
        this._waiters = [];
        this._locked = false;
    }

    /**
     * Indicates the internal locked state of the Lock.
     *
     * @returns {boolean}
     */
    locked() {
        return this._locked;
    }

    /**
     * Acquire the lock if available, otherwise wait until it is released
     * and then take the lock and return.
     */
    async acquire() {
        if (!this._locked) {
            this._locked = true;
            return true;
        }
        const f = new Future();
        f.addImmediateCallback(() => {
            if (!f.error()) {
                this._locked = true;
            }
        });
        this._waiters.push(f);
        try {
            await f;
            return true;
        } finally {
            this._waiters.splice(this._waiters.indexOf(f), 1);
        }
    }

    /**
     * Release this lock and wake up and calls to [acquire]{@link Lock#acquire}.
     */
    release() {
        if (!this._locked) {
            throw new Error('Lock is not acquired');
        }
        this._locked = false;
        for (const f of this._waiters) {
            if (!f.done()) {
                f.setResult(true);
                break;
            }
        }
    }
}


/**
 * A classic counting Semaphore used to regulate access to a resource.
 *
 * @param {Number} [value=1] - The number of simultaneous acquisitions
 *                             this semaphore will permit before blocking.
 * @see Python's [asyncio.Semaphore]{@link https://docs.python.org/3/library/asyncio-sync.html#semaphore}
 */
export class Semaphore {
    constructor(value=1) {
        if (value < 0) {
            throw new Error('Value must be >= 0');
        }
        this._value = value;
        this._waiters = [];
    }

    _wakeUpNext() {
        while (this._waiters.length) {
            const waiter = this._waiters.shift();
            if (!waiter.done()) {
                waiter.setResult();
                return;
            }
        }
    }

    /**
     * Has the semaphore exhausted all acquisitions.
     *
     * @returns {boolean} True if it will block an [acquire]{@link Semaphore#acquire}
     */
    locked() {
        return this._value === 0;
    }

    /**
     * Attempt to acquire one of the available slots in this semaphore.
     * If none are available, wait in line until one is available.
     *
     * @returns {boolean} true
     */
    async acquire() {
        while (this._value <= 0) {
            const f = new Future();
            this._waiters.push(f);
            try {
                await f;
            } catch(e) {
                if (this._value > 0) {
                    this._wakeUpNext();
                }
                throw e;
            }
        }
        this._value--;
        return true;
    }

    /**
     * Release a slot previously acquired with [acquire]{@link Semaphore#acquire}
     */
    async release() {
        this._value++;
        this._wakeUpNext();
    }
}


/**
 * A very simple object for indicating when some event has been triggered.
 *
 * @see Python's [asyncio.Event]{@link https://docs.python.org/3/library/asyncio-sync.html#event}
 */
export class Event {
    constructor() {
        this._waiters = [];
        this._isSet = false;
    }

    /**
     * @returns {boolean} True if [set]{@link Event#set} was called.
     */
    isSet() {
        return this._isSet;
    }

    /**
     * Wake ALL awaiters of [wait]{@link Event#wait}
     */
    set() {
        if (!this._isSet) {
            this._isSet = true;
            for (const f of this._waiters) {
                f.setResult(true);
            }
        }
    }

    /**
     * Opposite of [set]{@link Event#set}.  Clear the Event state so future
     * calls to [wait]{@link Event#wait} will block.
     */
    clear() {
        this._isSet = false;
    }

    /**
     * Wait until this event object is triggered with [set]{@link Event#set}.
     *
     * @returns {boolean} true
     */
    async wait() {
        if (this._isSet) {
            return true;
        }
        const f = new Future();
        this._waiters.push(f);
        try {
            return await f;
        } finally {
            this._waiters.splice(this._waiters.indexOf(f), 1);
        }
    }
}