151 lines
5.6 KiB
TypeScript
151 lines
5.6 KiB
TypeScript
import { Request, Actions, Response } from "../common";
|
|
import { getTabByID } from "./actions";
|
|
import { logger } from "../common/logger";
|
|
|
|
|
|
export type ResponseCheckerSync<T> = (r: Response<T>, err: chrome.runtime.LastError, count: number) => T;
|
|
export type ResponseCheckerAsync<T> = (r: Response<T>, err: chrome.runtime.LastError, count: number) => Promise<T>;
|
|
export type ResponseChecker<T> = ResponseCheckerSync<T> | ResponseCheckerAsync<T>;
|
|
/**
|
|
* Sending a message to target tab repeatedly until the response is not undefined.
|
|
* @param {object} tab the table where to send the message
|
|
* @param {object} req the request data.
|
|
* @param {function} dataChecker (reulst:any, err:error, tryCount:number) => any.
|
|
* Check and decide what value finally returns.
|
|
* Return undefined to make 'sendMessage' retry.
|
|
* Return MSG_USER_ABORT to cancel this promise.
|
|
* @param {number} interval retry interval, default: 500ms.
|
|
* @param {number} limit retry limit, default: 0, no limit.
|
|
* @param {string} log messages logged to console.
|
|
* @return {Promise} a promise of the response.
|
|
*/
|
|
export function sendMessage<T>(
|
|
tab: chrome.tabs.Tab,
|
|
req: Request,
|
|
log?: string,
|
|
dataChecker?: ResponseChecker<T>,
|
|
timeout?: number,
|
|
interval?: number,
|
|
limit?: number
|
|
) {
|
|
timeout = timeout || 10;
|
|
interval = interval || 500;
|
|
limit = isNaN(limit) ? 0 : limit;
|
|
let count = 0;
|
|
return new Promise<T>((resolve, reject) => {
|
|
|
|
loop();
|
|
|
|
async function loop() {
|
|
logger.debug("Request for", Actions[req.action]);
|
|
let tabAvailable = await getTabByID(tab.id);
|
|
if (!tabAvailable) {
|
|
reject("Task interrupted due to the target tab is closed.");
|
|
return;
|
|
}
|
|
|
|
if (limit && count >= limit) {
|
|
reject(`sendMessage loop limit ${limit} reached.`);
|
|
return;
|
|
}
|
|
count++;
|
|
let timeout = setTimeout(() => { reject(`${Actions[req.action]} requset timeout after ${timeout}s`) }, 10000);
|
|
chrome.tabs.sendMessage(tab.id, req, async (r: Response<T>) => {
|
|
clearTimeout(timeout);
|
|
// check error but do nothing until dataChecker.
|
|
let err = chrome.runtime.lastError;
|
|
let [result, error] = await checkResponse(dataChecker, r, err, count);
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
let flag = result !== undefined;
|
|
if (log) logger.info(log, flag ? '(OK)' : '(failed)');
|
|
if (flag) {
|
|
resolve(result);
|
|
} else {
|
|
setTimeout(() => {
|
|
logger.debug('Invalid response', r, 'retry...');
|
|
loop();
|
|
}, interval);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
async function checkResponse<T>(
|
|
dataChecker: ResponseChecker<T>,
|
|
response: Response<T>,
|
|
error: chrome.runtime.LastError,
|
|
tryCount: number
|
|
): Promise<[T, string]> {
|
|
// response could be undefined if the content script is interrupted.
|
|
// don't check, tell sendMessage to retry.
|
|
if (!response) return [undefined, undefined];
|
|
if (!dataChecker) {
|
|
return [response.result, response.error];
|
|
}
|
|
let result: T;
|
|
let pms: T | Promise<T>;
|
|
try {
|
|
pms = dataChecker(response, error, tryCount);
|
|
} catch (err) {
|
|
return [undefined, err];
|
|
}
|
|
// don't catch if it's not a Promise
|
|
if (pms instanceof Promise) {
|
|
let checkerError: any;
|
|
pms = pms.catch(e => checkerError = e);
|
|
result = await pms;
|
|
if (checkerError) {
|
|
return [undefined, checkerError];
|
|
}
|
|
} else {
|
|
result = pms;
|
|
}
|
|
return [result, undefined];
|
|
}
|
|
|
|
export type ActionSubscriberSync = (request: Request, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => void;
|
|
export type ActionSubscriberAsync = (request: Request, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => Promise<void>;
|
|
export type ActionSubscriber = ActionSubscriberSync | ActionSubscriberAsync;
|
|
|
|
class MessageSubscribers {
|
|
private listeners: { [key: number]: ActionSubscriber[] } = {};
|
|
addListener(action: Actions, subscriber: ActionSubscriber) {
|
|
this.listeners[action] || (this.listeners[action] = []);
|
|
this.listeners[action].push(subscriber);
|
|
}
|
|
removeListener(action: Actions, subscriber: ActionSubscriber) {
|
|
this.listeners[action] || (this.listeners[action] = []);
|
|
for (let i = 0; i < this.listeners[action].length; i++) {
|
|
if (this.listeners[action][i] == subscriber) {
|
|
this.listeners[action].splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
logger.debug(`${this.listeners[action].length} subscriber(s) remained for action ${Actions[action]}`);
|
|
}
|
|
getListeners(action: Actions): ActionSubscriber[] {
|
|
return this.listeners[action]
|
|
}
|
|
}
|
|
export const messageSubscribers = new MessageSubscribers();
|
|
|
|
chrome.runtime.onMessage.addListener(function (request: Request, sender, sendResponse) {
|
|
let subscribers = messageSubscribers.getListeners(request.action);
|
|
if (!subscribers || !subscribers.length) {
|
|
sendResponse("Request not supported.");
|
|
return;
|
|
}
|
|
let promises: Promise<any>[] = [];
|
|
for (let subscriber of subscribers) {
|
|
let p = subscriber(request, sender, sendResponse);
|
|
if (p instanceof Promise) promises.push(p);
|
|
}
|
|
if (promises.length)
|
|
return Promise.all(promises);
|
|
return;
|
|
});
|