Source: cancel-token.js

/**
 * @author Toru Nagashima <https://github.com/mysticatea>
 * @copyright 2016 Toru Nagashima. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
"use strict"

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const EventTargetShim = require("event-target-shim")

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const REASON = Symbol("reason")
const reasonMap = new WeakSet()

/**
 * Check whether the given cancel token has been canceled already.
 * @param {CancelToken} token The cancel token to check.
 * @returns {boolean} `true` if the token has been canceled.
 * @private
 */
function isCanceled(token) {
    return token.canceled
}

/**
 * When a parent token was canceled then it cancels this token.
 * @this CancelToken
 * @param {Event} event The cancel event to forward.
 * @returns {void}
 * @private
 */
function propagateCancel(event) {
    this.cancel(event.reason)
}

/**
 * Check whether the given object is a cancel reason.
 * @param {any} x The object to check.
 * @returns {boolean} `true` if the object is a cancel reason.
 * @memberof module:ajx
 */
function isCancel(x) {
    return reasonMap.has(x)
}

/**
 * CancelToken handles 'cancel' events.
 * @memberof module:ajx
 */
class CancelToken extends EventTargetShim("cancel") {
    /**
     * Create a cancel token.
     * @returns {CancelToken} The created token.
     */
    static new() {
        return new CancelToken()
    }

    /**
     * Create a cancel token which will be canceled if one of the given tokens
     * was canceled.
     * @param {CancelToken[]} parentTokens The tokens to forward events.
     * @returns {CancelToken} The created race token.
     */
    static race(parentTokens) {
        const ct = new CancelToken()

        // If parent tokens are nothing, just return new token.
        if (parentTokens.length === 0) {
            return ct
        }

        // If one of parent tokens has been canceled already, new token should
        // be canceled as well.
        {
            let t = null
            if ((t = parentTokens.find(isCanceled)) != null) {
                ct.cancel(t.reason)
                return ct
            }
        }

        // Setup propagation.
        const onParentCancel = propagateCancel.bind(ct)
        for (const token of parentTokens) {
            token.addEventListener("cancel", onParentCancel)
        }

        return ct
    }

    /**
     * Initialize this cancel token.
     */
    constructor() {
        super()
        this[REASON] = null
    }

    /**
     * The flag to indicate it has been canceled.
     * @type {boolean}
     */
    get canceled() {
        return Boolean(this[REASON])
    }

    /**
     * The error object which indicates the reason of cancel.
     * @type {Error}
     */
    get reason() {
        console.assert(this.canceled, "This cancel token has not been canceled yet.")
        return this[REASON]
    }

    /**
     * Throw an error if this token has been canceled already.
     * @returns {void}
     */
    throwIfCanceled() {
        const reason = this[REASON]
        if (reason != null) {
            throw reason
        }
    }

    /**
     * Cancel this token.
     * @param {Error} [reason] The error object which indicates the reason of cancel.
     * @returns {void}
     */
    cancel(reason) {
        /*eslint-disable no-param-reassign */
        if (typeof reason === "string") {
            reason = new Error(reason)
        }
        if (reason == null) {
            reason = new Error("unknown")
        }
        console.assert(reason instanceof Error, "The reason of cancel tokens should be an Error object or a string.")
        /*eslint-enable */

        if (this[REASON] == null) {
            reasonMap.add(reason)
            this[REASON] = reason
            this.dispatchEvent({type: "cancel", reason})
        }
    }
}

//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------

module.exports = {
    CancelToken,
    isCancel,
}