Api invoke with progress (#483)

* Create an internal API function to open a port

* Create system for running actions over a special port

* Don't assign in expression
This commit is contained in:
toasted-nutbread 2020-05-02 12:57:13 -04:00 committed by GitHub
parent 6c341a13d8
commit 5a61c311ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 192 additions and 1 deletions

View File

@ -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;

View File

@ -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) => {