import { Request, Actions, Response } from "../common"; import { getTabByID } from "./actions"; import { logger } from "../common/logger"; export type ResponseCheckerSync = (r: Response, err: chrome.runtime.LastError, count: number) => T; export type ResponseCheckerAsync = (r: Response, err: chrome.runtime.LastError, count: number) => Promise; export type ResponseChecker = ResponseCheckerSync | ResponseCheckerAsync; /** * 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( tab: chrome.tabs.Tab, req: Request, log?: string, dataChecker?: ResponseChecker, timeout?: number, interval?: number, limit?: number ) { timeout = timeout || 10; interval = interval || 500; limit = isNaN(limit) ? 0 : limit; let count = 0; return new Promise((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) => { 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( dataChecker: ResponseChecker, response: Response, 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; 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; 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[] = []; 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; });