diff options
| -rw-r--r-- | ext/bg/js/backend.js | 75 | ||||
| -rw-r--r-- | ext/mixed/js/api.js | 118 | 
2 files changed, 192 insertions, 1 deletions
| diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 2fce4be9..ed01c8df 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -116,8 +116,10 @@ class Backend {              ['purgeDatabase', {handler: this._onApiPurgeDatabase.bind(this), async: true}],              ['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}],              ['log', {handler: this._onApiLog.bind(this), async: false}], -            ['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}] +            ['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}], +            ['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}]          ]); +        this._messageHandlersWithProgress = new Map();          this._commandHandlers = new Map([              ['search', this._onCommandSearch.bind(this)], @@ -787,8 +789,79 @@ class Backend {          this._updateBadge();      } +    _onApiCreateActionPort(params, sender) { +        if (!sender || !sender.tab) { throw new Error('Invalid sender'); } +        const tabId = sender.tab.id; +        if (typeof tabId !== 'number') { throw new Error('Sender has invalid tab ID'); } + +        const frameId = sender.frameId; +        const id = yomichan.generateId(16); +        const portName = `action-port-${id}`; + +        const port = chrome.tabs.connect(tabId, {name: portName, frameId}); +        try { +            this._createActionListenerPort(port, sender, this._messageHandlersWithProgress); +        } catch (e) { +            port.disconnect(); +            throw e; +        } + +        return portName; +    } +      // Command handlers +    _createActionListenerPort(port, sender, handlers) { +        let hasStarted = false; + +        const onProgress = (data) => { +            try { +                if (port === null) { return; } +                port.postMessage({type: 'progress', data}); +            } catch (e) { +                // NOP +            } +        }; + +        const onMessage = async ({action, params}) => { +            if (hasStarted) { return; } +            hasStarted = true; +            port.onMessage.removeListener(onMessage); + +            try { +                port.postMessage({type: 'ack'}); + +                const messageHandler = handlers.get(action); +                if (typeof messageHandler === 'undefined') { +                    throw new Error('Invalid action'); +                } +                const {handler, async} = messageHandler; + +                const promiseOrResult = handler(params, sender, onProgress); +                const result = async ? await promiseOrResult : promiseOrResult; +                port.postMessage({type: 'complete', data: result}); +            } catch (e) { +                if (port !== null) { +                    port.postMessage({type: 'error', data: e}); +                } +                cleanup(); +            } +        }; + +        const cleanup = () => { +            if (port === null) { return; } +            if (!hasStarted) { +                port.onMessage.removeListener(onMessage); +            } +            port.onDisconnect.removeListener(cleanup); +            port = null; +            handlers = null; +        }; + +        port.onMessage.addListener(onMessage); +        port.onDisconnect.addListener(cleanup); +    } +      _getErrorLevelValue(errorLevel) {          switch (errorLevel) {              case 'info': return 0; diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index afd68aa2..bf85338e 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -152,6 +152,124 @@ function apiLogIndicatorClear() {      return _apiInvoke('logIndicatorClear');  } +function _apiCreateActionPort(timeout=5000) { +    return new Promise((resolve, reject) => { +        let timer = null; +        let portNameResolve; +        let portNameReject; +        const portNamePromise = new Promise((resolve2, reject2) => { +            portNameResolve = resolve2; +            portNameReject = reject2; +        }); + +        const onConnect = async (port) => { +            try { +                const portName = await portNamePromise; +                if (port.name !== portName || timer === null) { return; } +            } catch (e) { +                return; +            } + +            clearTimeout(timer); +            timer = null; + +            chrome.runtime.onConnect.removeListener(onConnect); +            resolve(port); +        }; + +        const onError = (e) => { +            if (timer !== null) { +                clearTimeout(timer); +                timer = null; +            } +            chrome.runtime.onConnect.removeListener(onConnect); +            portNameReject(e); +            reject(e); +        }; + +        timer = setTimeout(() => onError(new Error('Timeout')), timeout); + +        chrome.runtime.onConnect.addListener(onConnect); +        _apiInvoke('createActionPort').then(portNameResolve, onError); +    }); +} + +function _apiInvokeWithProgress(action, params, onProgress, timeout=5000) { +    return new Promise((resolve, reject) => { +        let timer = null; +        let port = null; + +        if (typeof onProgress !== 'function') { +            onProgress = () => {}; +        } + +        const onMessage = (message) => { +            switch (message.type) { +                case 'ack': +                    if (timer !== null) { +                        clearTimeout(timer); +                        timer = null; +                    } +                    break; +                case 'progress': +                    try { +                        onProgress(message.data); +                    } catch (e) { +                        // NOP +                    } +                    break; +                case 'complete': +                    cleanup(); +                    resolve(message.data); +                    break; +                case 'error': +                    cleanup(); +                    reject(jsonToError(message.data)); +                    break; +            } +        }; + +        const onDisconnect = () => { +            cleanup(); +            reject(new Error('Disconnected')); +        }; + +        const cleanup = () => { +            if (timer !== null) { +                clearTimeout(timer); +                timer = null; +            } +            if (port !== null) { +                port.onMessage.removeListener(onMessage); +                port.onDisconnect.removeListener(onDisconnect); +                port.disconnect(); +                port = null; +            } +            onProgress = null; +        }; + +        timer = setTimeout(() => { +            cleanup(); +            reject(new Error('Timeout')); +        }, timeout); + +        (async () => { +            try { +                port = await _apiCreateActionPort(timeout); +                port.onMessage.addListener(onMessage); +                port.onDisconnect.addListener(onDisconnect); +                port.postMessage({action, params}); +            } catch (e) { +                cleanup(); +                reject(e); +            } finally { +                action = null; +                params = null; +            } +        })(); +    }); +} +  function _apiInvoke(action, params={}) {      const data = {action, params};      return new Promise((resolve, reject) => { |