index.js

import EventEmitter from "eventemitter3";
import Voice from "@dzek69/react-native-voice";

const instanceManager = { // eslint-disable-line object-shorthand
    _instance: null,
    _callback: null,
    set(instance, callback) {
        if (!instance) {
            this._instance = null;
            this._callback = null;
            return;
        }
        this._instance = instance;
        this._callback = callback;
    },
    get() {
        if (!this._instance) {
            return null;
        }
        return {
            instance: this._instance,
            callback: this._callback,
        };
    },
    clear() {
        this.set(null);
    },
    isCurrent(instance) {
        return this._instance === instance;
    },
    isCurrentOrNull(instance) {
        return !this._instance || this._instance === instance;
    },
};

/**
 * `start`, `end`, and `recognized` events data
 *
 * @typedef {Object} NoErrorData
 * @param {false} error
 */

/**
 * `volumeChanged` event data
 *
 * @typedef {Object} VolumeChangedData
 * @param {number} value
 */

/**
 * `partialResults` and `results` event data
 *
 * @typedef {Object} ResultsData
 * @param {Array<string>} value
 */

/**
 * `error` event data
 *
 * @typedef {Object} ErrorData
 * @param {string} error
 */

/**
 * @typedef {string} VoiceEvent
 */

/**
 * List of available events
 * @enum {VoiceEvent}
 */
const EVENTS = {
    start: "start",
    end: "end",
    volumeChanged: "volumeChanged",
    partialResults: "partialResults",
    results: "results",
    recognized: "recognized",
    error: "error",
};

const redirectEvent = (eventName, data) => {
    const current = instanceManager.get();
    if (!current) {
        return;
    }
    current.callback(eventName, data);
};

Voice.onSpeechStart = (data) => redirectEvent(EVENTS.start, data);
Voice.onSpeechEnd = (data) => redirectEvent(EVENTS.end, data);
Voice.onSpeechVolumeChanged = (data) => redirectEvent(EVENTS.volumeChanged, data);
Voice.onSpeechPartialResults = (data) => redirectEvent(EVENTS.partialResults, data);
Voice.onSpeechResults = (data) => redirectEvent(EVENTS.results, data);
Voice.onSpeechRecognized = (data) => redirectEvent(EVENTS.recognized, data);
Voice.onSpeechError = (data) => redirectEvent(EVENTS.error, data);

const FINAL_EVENTS = [EVENTS.error, EVENTS.results];

const METHODS = [
    "addEventListener", "removeEventListener", "start", "stop", "cancel", "_onVoiceEvent", "destroy",
];

/**
 * @class VoiceToText
 */
class VoiceToText {
    constructor() {
        this._ee = new EventEmitter();
        this._destroyed = false;

        METHODS.forEach((fn) => {
            const isPrivate = fn.substr(0, 1) === "_";
            if (isPrivate) {
                this[fn] = this[fn].bind(this);
                return;
            }
            const cb = this[fn];
            this[fn] = (...args) => {
                this._checkDestroyed();
                return cb.apply(this, args);
            };
        });
    }

    /**
     * Adds listener for specified event
     *
     * @param {VoiceEvent} eventName
     * @param {function} listener
     * @throws {Error} if called on destroyed instance
     */
    addEventListener(eventName, listener) {
        this._ee.addListener(eventName, listener);
    }

    /**
     * Removed specified listener from specified event
     *
     * @param {VoiceEvent} eventName
     * @param {function} listener
     * @throws {Error} if called on destroyed instance
     */
    removeEventListener(eventName, listener) {
        this._ee.removeListener(eventName, listener);
    }

    /**
     * Starts recognizing
     *
     * @param {string} locale
     * @throws {Error} if another instance is already recognizing (same instance is allowed to restart recognizing)
     * or when called on destroyed instance
     */
    start(locale) {
        if (instanceManager.isCurrentOrNull(this)) {
            instanceManager.set(this, this._onVoiceEvent);
            Voice.start(locale);
            return;
        }

        throw new Error(
            "Another instance is recognizing right now.",
        );
    }

    /**
     * Stops recognizing, it should cause `results` or `error` event to be send
     *
     * @throws {Error} if another instance started recognition (repeating stop when nothing is recognizing is allowed)
     * or when called on destroyed instance
     */
    stop() {
        if (instanceManager.isCurrentOrNull(this)) {
            Voice.stop();
            return;
        }

        throw new Error(
            "Another instance is recognizing right now.",
        );
    }

    /**
     * Cancels recognizing, no `results` or `error` events will be send.
     *
     * @throws {Error} if another instance started recognition (repeating cancel when nothing is recognizing is allowed)
     * or when called on destroyed instance
     */
    cancel() {
        if (instanceManager.isCurrentOrNull(this)) {
            Voice.cancel();
            instanceManager.clear();
            return;
        }

        throw new Error(
            "Another instance is recognizing right now.",
        );
    }

    /**
     * Handler of global recognition event
     *
     * @param {VoiceEvent} name
     * @param {NoErrorData|VolumeChangedData|ResultsData|ErrorData} data
     * @private
     */
    _onVoiceEvent(name, data) {
        this._ee.emit(name, data);
        if (FINAL_EVENTS.includes(name)) {
            instanceManager.clear();
        }
    }

    /**
     * Checks if instance was already destroyed
     *
     * @throws {Error} when called on destroyed instance
     * @private
     */
    _checkDestroyed() {
        if (this._destroyed) {
            throw new Error("Instance destroyed. You cannot use methods on it anymore. Create another one.");
        }
    }

    /**
     * Destroys the instance, removes listener, cancels recognition
     *
     * @throws {Error} if called on destroyed instance
     */
    destroy() {
        this._destroyed = true;
        this._ee.removeAllListeners();

        if (instanceManager.isCurrent(this)) {
            instanceManager.clear();
            Voice.cancel();
        }
    }
}

/**
 * Checks if voice recognition is available on current system.
 *
 * @type {function}
 */
VoiceToText.isAvailable = Voice.isAvailable.bind(Voice);

/**
 * Checks if voice recognition is in progress (on any instance).
 *
 * @type {function}
 */
VoiceToText.isRecognizing = Voice.isRecognizing.bind(Voice);

export default VoiceToText;
export {
    EVENTS,
};