diff --git a/ext/js/general.js b/ext/js/general.js
index 16a5f0f..75efb05 100644
--- a/ext/js/general.js
+++ b/ext/js/general.js
@@ -3,9 +3,9 @@
browser.runtime.sendMessage({ action: 'enablePageAction' });
- const BANNER_URL = browser.extension.getURL('images/satori_banner.png');
- const TCS_LOGO_URL = browser.extension.getURL('images/tcslogo.svg');
- const ALT_TCS_LOGO_URL = browser.extension.getURL('images/alttcslogo.png');
+ const BANNER_URL = browser.runtime.getURL('images/satori_banner.png');
+ const TCS_LOGO_URL = browser.runtime.getURL('images/tcslogo.svg');
+ const ALT_TCS_LOGO_URL = browser.runtime.getURL('images/alttcslogo.png');
let storage = browser.storage.sync || browser.storage.local;
diff --git a/ext/js/offscreen.html b/ext/js/offscreen.html
new file mode 100644
index 0000000..177e040
--- /dev/null
+++ b/ext/js/offscreen.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/ext/js/offscreen.js b/ext/js/offscreen.js
new file mode 100644
index 0000000..e26ae88
--- /dev/null
+++ b/ext/js/offscreen.js
@@ -0,0 +1,48 @@
+importScripts(
+ "../vendor/browser-polyfill.js",
+ "config.js",
+ "../vendor/bower/jquery.min.js"
+);
+
+browser.runtime.onMessage.addListener(handleMessages);
+
+async function handleMessages(message) {
+ if (message.target !== 'offscreen') {
+ return false;
+ }
+
+ if (message.type === 'parseProblemList') {
+ parseProblemList(message.data);
+ } else {
+ console.warn(`Unexpected message type received: '${message.type}'.`);
+ return false;
+ }
+}
+
+function parseProblemList(htmlText) {
+ const jqueryHandles = $.parseHTML(htmlText);
+
+ return Object.fromEntries(
+ jqueryHandles.flatMap((el) =>
+ [
+ ...$(el).find('#content table.results tr:not(:first-of-type)'),
+ ].map((tr) => [
+ $(tr).find('td:nth-child(1)').text(),
+ {
+ title: $(tr).find('td:nth-child(2)').text(),
+ href: $(tr).find('td:nth-child(2) a').attr('href'),
+ pdfHref: $(tr).find('td:nth-child(3) a').attr('href'),
+ submitHref: $(tr).find('td:nth-child(5) a').attr('href'),
+ },
+ ]),
+ ),
+ );
+}
+
+function sendToBackground(type, data) {
+ browser.runtime.sendMessage({
+ type,
+ target: 'background',
+ data
+ });
+}
\ No newline at end of file
diff --git a/ext/js/results.js b/ext/js/results.js
index 3fe8cca..2800c79 100644
--- a/ext/js/results.js
+++ b/ext/js/results.js
@@ -17,7 +17,10 @@
let problemStatus = tr.find('td:last').text();
let url = document.location.href;
+ console.log("XD");
const contestID = getContestID(url);
+ console.log("XD2");
+
/**
* Get submit extension if possible
diff --git a/ext/js/service-worker.js b/ext/js/service-worker.js
new file mode 100644
index 0000000..32c3739
--- /dev/null
+++ b/ext/js/service-worker.js
@@ -0,0 +1,368 @@
+importScripts(
+ "../vendor/browser-polyfill.js",
+ "config.js",
+);
+
+(function () {
+ 'use strict';
+
+ const SATORI_URL_CONTEST = SATORI_URL_HTTPS + 'contest/';
+
+ let storage = browser.storage.sync || browser.storage.local;
+
+ let lastContestID;
+ /**
+ * A set that stores information about which tabs have Satori open. It is
+ * used to redirect the user to the last contest (if they are already
+ * browsing the website, then we shouldn't redirect).
+ * @type {Set}
+ */
+ let satoriTabs = new Set();
+
+ const contestProblemList = {};
+
+ function displayStatusNotification(submitID, problemCode, status) {
+ browser.notifications.create({
+ type: 'basic',
+ title: 'Submit ' + submitID + ' (' + problemCode + ') status',
+ message: 'New problem status: ' + status,
+ iconUrl: 'icon48.png',
+ });
+ }
+
+ /**
+ * Retrieve the ID of the last visited contest and store it in lastContestID
+ * variable.
+ * @see setUpLastContestRedirect
+ */
+ function retrieveLastContestID() {
+ storage.get('lastContestID').then((response) => {
+ lastContestID = response.lastContestID;
+ });
+ }
+
+ /**
+ * Save last contest ID both in Storage and our local variable.
+ * @param {string} contestID last contest ID
+ * @see setUpLastContestRedirect
+ */
+ function saveLastContestID(contestID) {
+ storage.set({ lastContestID: contestID });
+ lastContestID = contestID;
+ }
+
+ /**
+ * Display extension's page action.
+ * @param tab tab object
+ */
+ function enablePageAction(tab) {
+ const listener = () => {
+ browser.runtime.openOptionsPage();
+ };
+
+ if (chrome.action !== undefined) {
+ chrome.action.onClicked.addListener(listener);
+ } else {
+ browser.pageAction.show(tab.id);
+ browser.pageAction.onClicked.addListener(listener);
+ }
+ }
+
+ /**
+ * Add or remove a value to given list bound of a specified contest.
+ * @param {string} listName name of the list to look for in the storage
+ * @param {string} contestID ID of the contest to modify its list
+ * @param {string} value value to add or remove
+ * @param {boolean} add whether to add or remove given element
+ * @see getContestItemList
+ */
+ function modifyContestItemList(listName, contestID, value, add) {
+ storage
+ .get({
+ [listName]: {},
+ })
+ .then((items) => {
+ let list = items[listName];
+ if (!(contestID in list)) {
+ list[contestID] = [];
+ }
+
+ let s = new Set(list[contestID]);
+ if (add) {
+ s.add(value);
+ } else {
+ s.delete(value);
+ }
+ list[contestID] = [...s];
+
+ storage.set({ [listName]: list });
+ });
+ }
+
+ /**
+ * Retrieve a list bound to given contest.
+ *
+ * This function is used e.g. to store the list of problem hidden in for a
+ * contest.
+ *
+ * @param {string} listName name of the list to get
+ * @param {string} contestID ID of the contest to get the list for
+ * @param {function} sendResponse function to call with the data retrieved
+ * @see modifyContestItemList
+ */
+ function getContestItemList(listName, contestID, sendResponse) {
+ storage
+ .get({
+ [listName]: {},
+ })
+ .then((items) => {
+ if (contestID in items[listName]) {
+ sendResponse(items[listName][contestID]);
+ } else {
+ sendResponse([]);
+ }
+ });
+ }
+
+ /**
+ * Add an onBeforeRequest listener that redirects to the last contest
+ * if the user just entered Satori webpage.
+ *
+ * Also, the function adds webNavigation.onCommitted and tabs.onRemoved
+ * listeners to keep the list of Satori tabs.
+ */
+ function setUpLastContestRedirect() {
+ // Store which tabs have Satori open
+ browser.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
+ if (
+ tab.url &&
+ (tab.url.startsWith(SATORI_URL_HTTP) ||
+ tab.url.startsWith(SATORI_URL_HTTPS))
+ ) {
+ satoriTabs.add(tabId);
+ } else {
+ satoriTabs.delete(tabId);
+ }
+ });
+
+ browser.tabs.onRemoved.addListener((tabId) => satoriTabs.delete(tabId));
+
+ // browser.webRequest.onBeforeRequest.addListener(
+ // function (details) {
+ // if (
+ // typeof lastContestID === 'undefined' ||
+ // lastContestID === null ||
+ // satoriTabs.has(details.tabId)
+ // ) {
+ // // If we haven't saved any contest yet, then do nothing
+ // // Also, don't redirect if the user is already browsing
+ // // Satori
+ // return;
+ // }
+ //
+ // return {
+ // redirectUrl: SATORI_URL_CONTEST + lastContestID + '/',
+ // };
+ // },
+ // {
+ // urls: [
+ // '*://satori.tcs.uj.edu.pl/',
+ // '*://satori.tcs.uj.edu.pl/news',
+ // ],
+ // types: ['main_frame'],
+ // },
+ // ['blocking'],
+ // );
+ }
+
+ /**
+ * Add highlight.js CSS to the page using the selected style.
+ */
+ async function injectHighlightJsCss(tab) {
+ await storage
+ .get({
+ [HIGHLIGHT_JS_STYLE_KEY]:
+ DEFAULT_SETTINGS[HIGHLIGHT_JS_STYLE_KEY],
+ })
+ .then(async (response) => {
+ let style = response.highlightJsStyle;
+ if (style !== 'none') {
+ await browser.scripting.insertCSS({
+ files: [`vendor/bower/hjsstyles/${style}.css`],
+ target: { tabId: tab.id },
+ });
+ }
+ });
+ }
+
+ const satoriDomain = new URL(SATORI_URL_HTTPS).hostname;
+
+ function getKeepSignedInDuration() {
+ return storage
+ .get({
+ [KEEP_SIGNED_IN_DURATION_KEY]:
+ DEFAULT_SETTINGS[KEEP_SIGNED_IN_DURATION_KEY],
+ })
+ .then((response) => {
+ const duration = response[KEEP_SIGNED_IN_DURATION_KEY];
+ if (duration === 'none') {
+ return null;
+ }
+ return parseInt(duration, 10);
+ });
+ }
+
+ function updateCookie(cookie) {
+ return getKeepSignedInDuration().then((duration) => {
+ if (duration === null) {
+ return;
+ }
+ const newCookie = {
+ expirationDate:
+ Math.round(Date.now() / 1000) + duration * 24 * 60 * 60,
+ httpOnly: cookie.httpOnly,
+ name: cookie.name,
+ path: cookie.path,
+ sameSite: cookie.sameSite,
+ secure: cookie.secure,
+ storeId: cookie.storeId,
+ value: cookie.value,
+ url: SATORI_URL_HTTPS,
+ };
+ return browser.cookies.set(newCookie);
+ });
+ }
+
+ function getTokenCookies() {
+ return browser.cookies.getAll({
+ domain: satoriDomain,
+ name: 'satori_token',
+ path: '/',
+ });
+ }
+
+ function updateExistingCookie() {
+ return getTokenCookies().then((cookies) => {
+ if (cookies.length === 0) {
+ return;
+ }
+ if (cookies.length > 1) {
+ console.warn('Too many satori_token cookies');
+ return;
+ }
+ const [cookie] = cookies;
+ if (cookie.expirationDate === undefined && cookie.value !== '') {
+ return updateCookie(cookie);
+ }
+ });
+ }
+
+ function setUpSessionCookies() {
+ updateExistingCookie().catch(console.error);
+ browser.cookies.onChanged.addListener(({ removed, cookie }) => {
+ if (
+ !removed &&
+ cookie.domain === satoriDomain &&
+ cookie.name === 'satori_token' &&
+ cookie.path === '/'
+ ) {
+ updateExistingCookie().catch(console.error);
+ }
+ });
+ }
+
+ function saveContestProblemList(contestID, problems) {
+ contestProblemList[contestID] = problems;
+ }
+
+ async function getProblemList(contestID) {
+ if (!contestProblemList[contestID]) {
+ try {
+ const response = await fetch(
+ `${SATORI_URL_HTTPS}contest/${contestID}/problems`,
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP Status ${response.status}`);
+ }
+
+ await chrome.offscreen.createDocument({
+ url: "offscreen.html",
+ reasons: [chrome.offscreen.Reason.DOM_PARSER],
+ justification: 'Parse DOM'
+ });
+ const parseResponse = await chrome.runtime.sendMessage({
+ type: 'parseProblemList',
+ target: 'offscreen',
+ data: await response.text()
+ });
+ await chrome.offscreen.closeDocument();
+ console.log(parseResponse);
+
+ contestProblemList[contestID] = parseResponse;
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ return contestProblemList[contestID] ?? {};
+ }
+
+ /**
+ * Save joined contest list in Storage
+ * @param {array} contestList contest list
+ */
+ function saveJoinedContestList(contestList) {
+ storage.set({ contestList });
+ }
+
+ /**
+ * Save joined contest list in Storage
+ * @return {array} contest list
+ */
+ async function getJoinedContestList() {
+ return (await storage.get('contestList')).contestList ?? [];
+ }
+
+ retrieveLastContestID();
+ setUpLastContestRedirect();
+ setUpSessionCookies();
+
+ browser.runtime.onMessage.addListener(async (request, sender) => {
+ if (request.action === 'enablePageAction') {
+ enablePageAction(sender.tab);
+ } else if (request.action === 'saveLastContestID') {
+ saveLastContestID(getContestID(sender.url));
+ } else if (request.action === 'displayStatusNotification') {
+ displayStatusNotification(
+ request.submitID,
+ request.problemCode,
+ request.problemStatus,
+ );
+ } else if (request.action === 'modifyContestItemList') {
+ modifyContestItemList(
+ request.listName,
+ getContestID(sender.url),
+ request.value,
+ request.add,
+ );
+ } else if (request.action === 'getContestItemList') {
+ return new Promise((resolve) => {
+ getContestItemList(
+ request.listName,
+ getContestID(sender.url),
+ resolve,
+ );
+ });
+ } else if (request.action === 'injectHighlightJsCss') {
+ await injectHighlightJsCss(sender.tab);
+ } else if (request.action === 'saveContestProblemList') {
+ saveContestProblemList(request.contestID, request.problems);
+ } else if (request.action === 'getContestProblemList') {
+ return getProblemList(request.contestID);
+ } else if (request.action === 'setJoinedContestList') {
+ saveJoinedContestList(request.contestList);
+ } else if (request.action === 'getJoinedContestList') {
+ return getJoinedContestList();
+ }
+ return new Promise((resolve) => resolve(null));
+ });
+})();
diff --git a/ext/manifest.json b/ext/manifest.json
index 76d490b..32011c3 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -1,11 +1,11 @@
{
- "manifest_version": 2,
+ "manifest_version": 3,
"name": "Satori Enhancements",
"description": "Adds a few useful enhancements to Satori Online Judge website.",
- "version": "1.5",
+ "version": "2.0",
- "minimum_chrome_version": "62",
+ "minimum_chrome_version": "130",
"browser_specific_settings": {
"gecko": {
@@ -21,36 +21,36 @@
},
"options_ui": {
- "page": "options.html",
- "chrome_style": true,
- "browser_style": true
+ "page": "options.html"
},
"permissions": [
"storage",
"notifications",
"webRequest",
- "webRequestBlocking",
"cookies",
+ "scripting",
+ "offscreen"
+ ],
+ "host_permissions": [
"*://satori.tcs.uj.edu.pl/*"
],
- "page_action": {
+ "action": {
"default_icon": "icon128.png",
"default_title": "Satori Enhancements"
},
"background": {
- "scripts": [
- "vendor/browser-polyfill.js",
- "vendor/bower/jquery.min.js",
- "js/config.js",
- "js/common.js",
- "js/background.js"
- ]
+ "service_worker": "js/service-worker.js"
},
- "web_accessible_resources": ["images/*.png", "images/*.svg"],
+ "web_accessible_resources": [
+ {
+ "resources": ["images/*.png", "images/*.svg"],
+ "matches": ["*://satori.tcs.uj.edu.pl/*"]
+ }
+ ],
"content_scripts": [
{
diff --git a/ext/vendor/browser-polyfill.js b/ext/vendor/browser-polyfill.js
index d29c925..5f7f24b 100644
--- a/ext/vendor/browser-polyfill.js
+++ b/ext/vendor/browser-polyfill.js
@@ -1,843 +1,1224 @@
-/* webextension-polyfill - v0.1.0 - Tue Dec 27 2016 17:28:07 */
-/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set sts=2 sw=2 et tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
+(function (global, factory) {
+ if (typeof define === "function" && define.amd) {
+ define("webextension-polyfill", ["module"], factory);
+ } else if (typeof exports !== "undefined") {
+ factory(module);
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod);
+ global.browser = mod.exports;
+ }
+})(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (module) {
+ /* webextension-polyfill - v0.12.0 - Tue May 14 2024 18:01:29 */
+ /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+ /* vim: set sts=2 sw=2 et tw=80: */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ "use strict";
-if (typeof browser === "undefined") {
- // Wrapping the bulk of this polyfill in a one-time-use function is a minor
- // optimization for Firefox. Since Spidermonkey does not fully parse the
- // contents of a function until the first time it's called, and since it will
- // never actually need to be called, this allows the polyfill to be included
- // in Firefox nearly for free.
- const wrapAPIs = () => {
- const apiMetadata = {
- "alarms": {
- "clear": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "clearAll": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "get": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "getAll": {
- "minArgs": 0,
- "maxArgs": 0
- }
- },
- "bookmarks": {
- "create": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "export": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "get": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getChildren": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getRecent": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getTree": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "getSubTree": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "import": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "move": {
- "minArgs": 2,
- "maxArgs": 2
- },
- "remove": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "removeTree": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "search": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "update": {
- "minArgs": 2,
- "maxArgs": 2
- }
- },
- "browserAction": {
- "getBadgeBackgroundColor": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getBadgeText": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getPopup": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getTitle": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "setIcon": {
- "minArgs": 1,
- "maxArgs": 1
- }
- },
- "commands": {
- "getAll": {
- "minArgs": 0,
- "maxArgs": 0
- }
- },
- "contextMenus": {
- "update": {
- "minArgs": 2,
- "maxArgs": 2
- },
- "remove": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "removeAll": {
- "minArgs": 0,
- "maxArgs": 0
- }
- },
- "cookies": {
- "get": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getAll": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getAllCookieStores": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "remove": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "set": {
- "minArgs": 1,
- "maxArgs": 1
- }
- },
- "downloads": {
- "download": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "cancel": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "erase": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getFileIcon": {
- "minArgs": 1,
- "maxArgs": 2
- },
- "open": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "pause": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "removeFile": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "resume": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "search": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "show": {
- "minArgs": 1,
- "maxArgs": 1
- }
- },
- "extension": {
- "isAllowedFileSchemeAccess": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "isAllowedIncognitoAccess": {
- "minArgs": 0,
- "maxArgs": 0
- }
- },
- "history": {
- "addUrl": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getVisits": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "deleteAll": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "deleteRange": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "deleteUrl": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "search": {
- "minArgs": 1,
- "maxArgs": 1
- }
- },
- "i18n": {
- "detectLanguage": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getAcceptLanguages": {
- "minArgs": 0,
- "maxArgs": 0
- }
- },
- "idle": {
- "queryState": {
- "minArgs": 1,
- "maxArgs": 1
- }
- },
- "management": {
- "get": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getAll": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "getSelf": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "uninstallSelf": {
- "minArgs": 0,
- "maxArgs": 1
- }
- },
- "notifications": {
- "clear": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "create": {
- "minArgs": 1,
- "maxArgs": 2
- },
- "getAll": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "getPermissionLevel": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "update": {
- "minArgs": 2,
- "maxArgs": 2
- }
- },
- "pageAction": {
- "getPopup": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getTitle": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "setIcon": {
- "minArgs": 1,
- "maxArgs": 1
- }
- },
- "runtime": {
- "getBackgroundPage": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "getBrowserInfo": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "getPlatformInfo": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "openOptionsPage": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "requestUpdateCheck": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "sendMessage": {
- "minArgs": 1,
- "maxArgs": 3
- },
- "sendNativeMessage": {
- "minArgs": 2,
- "maxArgs": 2
- },
- "setUninstallURL": {
- "minArgs": 1,
- "maxArgs": 1
- }
- },
- "storage": {
- "local": {
+ if (!(globalThis.chrome && globalThis.chrome.runtime && globalThis.chrome.runtime.id)) {
+ throw new Error("This script should only be loaded in a browser extension.");
+ }
+ if (!(globalThis.browser && globalThis.browser.runtime && globalThis.browser.runtime.id)) {
+ const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received.";
+
+ // Wrapping the bulk of this polyfill in a one-time-use function is a minor
+ // optimization for Firefox. Since Spidermonkey does not fully parse the
+ // contents of a function until the first time it's called, and since it will
+ // never actually need to be called, this allows the polyfill to be included
+ // in Firefox nearly for free.
+ const wrapAPIs = extensionAPIs => {
+ // NOTE: apiMetadata is associated to the content of the api-metadata.json file
+ // at build time by replacing the following "include" with the content of the
+ // JSON file.
+ const apiMetadata = {
+ "alarms": {
"clear": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "clearAll": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "get": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "getAll": {
+ "minArgs": 0,
+ "maxArgs": 0
+ }
+ },
+ "bookmarks": {
+ "create": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "get": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getChildren": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getRecent": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getSubTree": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getTree": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "move": {
+ "minArgs": 2,
+ "maxArgs": 2
+ },
+ "remove": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeTree": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "search": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "update": {
+ "minArgs": 2,
+ "maxArgs": 2
+ }
+ },
+ "browserAction": {
+ "disable": {
+ "minArgs": 0,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "enable": {
+ "minArgs": 0,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "getBadgeBackgroundColor": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getBadgeText": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getPopup": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getTitle": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "openPopup": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "setBadgeBackgroundColor": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "setBadgeText": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "setIcon": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "setPopup": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "setTitle": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ }
+ },
+ "browsingData": {
+ "remove": {
+ "minArgs": 2,
+ "maxArgs": 2
+ },
+ "removeCache": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeCookies": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeDownloads": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeFormData": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeHistory": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeLocalStorage": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removePasswords": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removePluginData": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "settings": {
+ "minArgs": 0,
+ "maxArgs": 0
+ }
+ },
+ "commands": {
+ "getAll": {
+ "minArgs": 0,
+ "maxArgs": 0
+ }
+ },
+ "contextMenus": {
+ "remove": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeAll": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "update": {
+ "minArgs": 2,
+ "maxArgs": 2
+ }
+ },
+ "cookies": {
+ "get": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getAll": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getAllCookieStores": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "remove": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "set": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ },
+ "devtools": {
+ "inspectedWindow": {
+ "eval": {
+ "minArgs": 1,
+ "maxArgs": 2,
+ "singleCallbackArg": false
+ }
+ },
+ "panels": {
+ "create": {
+ "minArgs": 3,
+ "maxArgs": 3,
+ "singleCallbackArg": true
+ },
+ "elements": {
+ "createSidebarPane": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ }
+ }
+ },
+ "downloads": {
+ "cancel": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "download": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "erase": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getFileIcon": {
+ "minArgs": 1,
+ "maxArgs": 2
+ },
+ "open": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "pause": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeFile": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "resume": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "search": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "show": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ }
+ },
+ "extension": {
+ "isAllowedFileSchemeAccess": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "isAllowedIncognitoAccess": {
+ "minArgs": 0,
+ "maxArgs": 0
+ }
+ },
+ "history": {
+ "addUrl": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "deleteAll": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "deleteRange": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "deleteUrl": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getVisits": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "search": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ },
+ "i18n": {
+ "detectLanguage": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getAcceptLanguages": {
+ "minArgs": 0,
+ "maxArgs": 0
+ }
+ },
+ "identity": {
+ "launchWebAuthFlow": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ },
+ "idle": {
+ "queryState": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ },
+ "management": {
+ "get": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getAll": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "getSelf": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "setEnabled": {
+ "minArgs": 2,
+ "maxArgs": 2
+ },
+ "uninstallSelf": {
+ "minArgs": 0,
+ "maxArgs": 1
+ }
+ },
+ "notifications": {
+ "clear": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "create": {
+ "minArgs": 1,
+ "maxArgs": 2
+ },
+ "getAll": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "getPermissionLevel": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "update": {
+ "minArgs": 2,
+ "maxArgs": 2
+ }
+ },
+ "pageAction": {
+ "getPopup": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getTitle": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "hide": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "setIcon": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "setPopup": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "setTitle": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ },
+ "show": {
+ "minArgs": 1,
+ "maxArgs": 1,
+ "fallbackToNoCallback": true
+ }
+ },
+ "permissions": {
+ "contains": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getAll": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "remove": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "request": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ },
+ "runtime": {
+ "getBackgroundPage": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "getPlatformInfo": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "openOptionsPage": {
"minArgs": 0,
"maxArgs": 0
},
+ "requestUpdateCheck": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "sendMessage": {
+ "minArgs": 1,
+ "maxArgs": 3
+ },
+ "sendNativeMessage": {
+ "minArgs": 2,
+ "maxArgs": 2
+ },
+ "setUninstallURL": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ },
+ "sessions": {
+ "getDevices": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "getRecentlyClosed": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "restore": {
+ "minArgs": 0,
+ "maxArgs": 1
+ }
+ },
+ "storage": {
+ "local": {
+ "clear": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "get": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "getBytesInUse": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "remove": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "set": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ },
+ "managed": {
+ "get": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "getBytesInUse": {
+ "minArgs": 0,
+ "maxArgs": 1
+ }
+ },
+ "sync": {
+ "clear": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "get": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "getBytesInUse": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "remove": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "set": {
+ "minArgs": 1,
+ "maxArgs": 1
+ }
+ }
+ },
+ "tabs": {
+ "captureVisibleTab": {
+ "minArgs": 0,
+ "maxArgs": 2
+ },
+ "create": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "detectLanguage": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "discard": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "duplicate": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "executeScript": {
+ "minArgs": 1,
+ "maxArgs": 2
+ },
"get": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "getCurrent": {
+ "minArgs": 0,
+ "maxArgs": 0
+ },
+ "getZoom": {
"minArgs": 0,
"maxArgs": 1
},
- "getBytesInUse": {
+ "getZoomSettings": {
"minArgs": 0,
"maxArgs": 1
},
- "remove": {
+ "goBack": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "goForward": {
+ "minArgs": 0,
+ "maxArgs": 1
+ },
+ "highlight": {
"minArgs": 1,
"maxArgs": 1
},
- "set": {
+ "insertCSS": {
+ "minArgs": 1,
+ "maxArgs": 2
+ },
+ "move": {
+ "minArgs": 2,
+ "maxArgs": 2
+ },
+ "query": {
"minArgs": 1,
"maxArgs": 1
+ },
+ "reload": {
+ "minArgs": 0,
+ "maxArgs": 2
+ },
+ "remove": {
+ "minArgs": 1,
+ "maxArgs": 1
+ },
+ "removeCSS": {
+ "minArgs": 1,
+ "maxArgs": 2
+ },
+ "sendMessage": {
+ "minArgs": 2,
+ "maxArgs": 3
+ },
+ "setZoom": {
+ "minArgs": 1,
+ "maxArgs": 2
+ },
+ "setZoomSettings": {
+ "minArgs": 1,
+ "maxArgs": 2
+ },
+ "update": {
+ "minArgs": 1,
+ "maxArgs": 2
}
},
- "managed": {
+ "topSites": {
"get": {
"minArgs": 0,
+ "maxArgs": 0
+ }
+ },
+ "webNavigation": {
+ "getAllFrames": {
+ "minArgs": 1,
"maxArgs": 1
},
- "getBytesInUse": {
- "minArgs": 0,
+ "getFrame": {
+ "minArgs": 1,
"maxArgs": 1
}
},
- "sync": {
- "clear": {
+ "webRequest": {
+ "handlerBehaviorChanged": {
"minArgs": 0,
"maxArgs": 0
+ }
+ },
+ "windows": {
+ "create": {
+ "minArgs": 0,
+ "maxArgs": 1
},
"get": {
+ "minArgs": 1,
+ "maxArgs": 2
+ },
+ "getAll": {
"minArgs": 0,
"maxArgs": 1
},
- "getBytesInUse": {
+ "getCurrent": {
"minArgs": 0,
"maxArgs": 1
},
- "remove": {
- "minArgs": 1,
+ "getLastFocused": {
+ "minArgs": 0,
"maxArgs": 1
},
- "set": {
+ "remove": {
"minArgs": 1,
"maxArgs": 1
+ },
+ "update": {
+ "minArgs": 2,
+ "maxArgs": 2
}
}
- },
- "tabs": {
- "create": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "captureVisibleTab": {
- "minArgs": 0,
- "maxArgs": 2
- },
- "detectLanguage": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "duplicate": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "executeScript": {
- "minArgs": 1,
- "maxArgs": 2
- },
- "get": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getCurrent": {
- "minArgs": 0,
- "maxArgs": 0
- },
- "getZoom": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "getZoomSettings": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "highlight": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "insertCSS": {
- "minArgs": 1,
- "maxArgs": 2
- },
- "move": {
- "minArgs": 2,
- "maxArgs": 2
- },
- "reload": {
- "minArgs": 0,
- "maxArgs": 2
- },
- "remove": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "query": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "removeCSS": {
- "minArgs": 1,
- "maxArgs": 2
- },
- "sendMessage": {
- "minArgs": 2,
- "maxArgs": 3
- },
- "setZoom": {
- "minArgs": 1,
- "maxArgs": 2
- },
- "setZoomSettings": {
- "minArgs": 1,
- "maxArgs": 2
- },
- "update": {
- "minArgs": 1,
- "maxArgs": 2
- }
- },
- "webNavigation": {
- "getAllFrames": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "getFrame": {
- "minArgs": 1,
- "maxArgs": 1
- }
- },
- "webRequest": {
- "handlerBehaviorChanged": {
- "minArgs": 0,
- "maxArgs": 0
- }
- },
- "windows": {
- "create": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "get": {
- "minArgs": 1,
- "maxArgs": 2
- },
- "getAll": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "getCurrent": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "getLastFocused": {
- "minArgs": 0,
- "maxArgs": 1
- },
- "remove": {
- "minArgs": 1,
- "maxArgs": 1
- },
- "update": {
- "minArgs": 2,
- "maxArgs": 2
- }
- }
- };
-
- /**
- * A WeakMap subclass which creates and stores a value for any key which does
- * not exist when accessed, but behaves exactly as an ordinary WeakMap
- * otherwise.
- *
- * @param {function} createItem
- * A function which will be called in order to create the value for any
- * key which does not exist, the first time it is accessed. The
- * function receives, as its only argument, the key being created.
- */
- class DefaultWeakMap extends WeakMap {
- constructor(createItem, items = undefined) {
- super(items);
- this.createItem = createItem;
+ };
+ if (Object.keys(apiMetadata).length === 0) {
+ throw new Error("api-metadata.json has not been included in browser-polyfill");
}
- get(key) {
- if (!this.has(key)) {
- this.set(key, this.createItem(key));
+ /**
+ * A WeakMap subclass which creates and stores a value for any key which does
+ * not exist when accessed, but behaves exactly as an ordinary WeakMap
+ * otherwise.
+ *
+ * @param {function} createItem
+ * A function which will be called in order to create the value for any
+ * key which does not exist, the first time it is accessed. The
+ * function receives, as its only argument, the key being created.
+ */
+ class DefaultWeakMap extends WeakMap {
+ constructor(createItem, items = undefined) {
+ super(items);
+ this.createItem = createItem;
+ }
+ get(key) {
+ if (!this.has(key)) {
+ this.set(key, this.createItem(key));
+ }
+ return super.get(key);
}
-
- return super.get(key);
}
- }
- /**
- * Returns true if the given object is an object with a `then` method, and can
- * therefore be assumed to behave as a Promise.
- *
- * @param {*} value The value to test.
- * @returns {boolean} True if the value is thenable.
- */
- const isThenable = value => {
- return value && typeof value === "object" && typeof value.then === "function";
- };
-
- /**
- * Creates and returns a function which, when called, will resolve or reject
- * the given promise based on how it is called:
- *
- * - If, when called, `chrome.runtime.lastError` contains a non-null object,
- * the promise is rejected with that value.
- * - If the function is called with exactly one argument, the promise is
- * resolved to that value.
- * - Otherwise, the promise is resolved to an array containing all of the
- * function's arguments.
- *
- * @param {object} promise
- * An object containing the resolution and rejection functions of a
- * promise.
- * @param {function} promise.resolve
- * The promise's resolution function.
- * @param {function} promise.rejection
- * The promise's rejection function.
- *
- * @returns {function}
- * The generated callback function.
- */
- const makeCallback = promise => {
- return (...callbackArgs) => {
- if (chrome.runtime.lastError) {
- promise.reject(chrome.runtime.lastError);
- } else if (callbackArgs.length === 1) {
- promise.resolve(callbackArgs[0]);
- } else {
- promise.resolve(callbackArgs);
- }
+ /**
+ * Returns true if the given object is an object with a `then` method, and can
+ * therefore be assumed to behave as a Promise.
+ *
+ * @param {*} value The value to test.
+ * @returns {boolean} True if the value is thenable.
+ */
+ const isThenable = value => {
+ return value && typeof value === "object" && typeof value.then === "function";
};
- };
- /**
- * Creates a wrapper function for a method with the given name and metadata.
- *
- * @param {string} name
- * The name of the method which is being wrapped.
- * @param {object} metadata
- * Metadata about the method being wrapped.
- * @param {integer} metadata.minArgs
- * The minimum number of arguments which must be passed to the
- * function. If called with fewer than this number of arguments, the
- * wrapper will raise an exception.
- * @param {integer} metadata.maxArgs
- * The maximum number of arguments which may be passed to the
- * function. If called with more than this number of arguments, the
- * wrapper will raise an exception.
- *
- * @returns {function(object, ...*)}
- * The generated wrapper function.
- */
- const wrapAsyncFunction = (name, metadata) => {
- const pluralizeArguments = (numArgs) => numArgs == 1 ? "argument" : "arguments";
+ /**
+ * Creates and returns a function which, when called, will resolve or reject
+ * the given promise based on how it is called:
+ *
+ * - If, when called, `chrome.runtime.lastError` contains a non-null object,
+ * the promise is rejected with that value.
+ * - If the function is called with exactly one argument, the promise is
+ * resolved to that value.
+ * - Otherwise, the promise is resolved to an array containing all of the
+ * function's arguments.
+ *
+ * @param {object} promise
+ * An object containing the resolution and rejection functions of a
+ * promise.
+ * @param {function} promise.resolve
+ * The promise's resolution function.
+ * @param {function} promise.reject
+ * The promise's rejection function.
+ * @param {object} metadata
+ * Metadata about the wrapped method which has created the callback.
+ * @param {boolean} metadata.singleCallbackArg
+ * Whether or not the promise is resolved with only the first
+ * argument of the callback, alternatively an array of all the
+ * callback arguments is resolved. By default, if the callback
+ * function is invoked with only a single argument, that will be
+ * resolved to the promise, while all arguments will be resolved as
+ * an array if multiple are given.
+ *
+ * @returns {function}
+ * The generated callback function.
+ */
+ const makeCallback = (promise, metadata) => {
+ return (...callbackArgs) => {
+ if (extensionAPIs.runtime.lastError) {
+ promise.reject(new Error(extensionAPIs.runtime.lastError.message));
+ } else if (metadata.singleCallbackArg || callbackArgs.length <= 1 && metadata.singleCallbackArg !== false) {
+ promise.resolve(callbackArgs[0]);
+ } else {
+ promise.resolve(callbackArgs);
+ }
+ };
+ };
+ const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments";
- return function asyncFunctionWrapper(target, ...args) {
- if (args.length < metadata.minArgs) {
- throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`);
- }
+ /**
+ * Creates a wrapper function for a method with the given name and metadata.
+ *
+ * @param {string} name
+ * The name of the method which is being wrapped.
+ * @param {object} metadata
+ * Metadata about the method being wrapped.
+ * @param {integer} metadata.minArgs
+ * The minimum number of arguments which must be passed to the
+ * function. If called with fewer than this number of arguments, the
+ * wrapper will raise an exception.
+ * @param {integer} metadata.maxArgs
+ * The maximum number of arguments which may be passed to the
+ * function. If called with more than this number of arguments, the
+ * wrapper will raise an exception.
+ * @param {boolean} metadata.singleCallbackArg
+ * Whether or not the promise is resolved with only the first
+ * argument of the callback, alternatively an array of all the
+ * callback arguments is resolved. By default, if the callback
+ * function is invoked with only a single argument, that will be
+ * resolved to the promise, while all arguments will be resolved as
+ * an array if multiple are given.
+ *
+ * @returns {function(object, ...*)}
+ * The generated wrapper function.
+ */
+ const wrapAsyncFunction = (name, metadata) => {
+ return function asyncFunctionWrapper(target, ...args) {
+ if (args.length < metadata.minArgs) {
+ throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`);
+ }
+ if (args.length > metadata.maxArgs) {
+ throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`);
+ }
+ return new Promise((resolve, reject) => {
+ if (metadata.fallbackToNoCallback) {
+ // This API method has currently no callback on Chrome, but it return a promise on Firefox,
+ // and so the polyfill will try to call it with a callback first, and it will fallback
+ // to not passing the callback if the first call fails.
+ try {
+ target[name](...args, makeCallback({
+ resolve,
+ reject
+ }, metadata));
+ } catch (cbError) {
+ console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError);
+ target[name](...args);
- if (args.length > metadata.maxArgs) {
- throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`);
- }
+ // Update the API method metadata, so that the next API calls will not try to
+ // use the unsupported callback anymore.
+ metadata.fallbackToNoCallback = false;
+ metadata.noCallback = true;
+ resolve();
+ }
+ } else if (metadata.noCallback) {
+ target[name](...args);
+ resolve();
+ } else {
+ target[name](...args, makeCallback({
+ resolve,
+ reject
+ }, metadata));
+ }
+ });
+ };
+ };
- return new Promise((resolve, reject) => {
- target[name](...args, makeCallback({resolve, reject}));
+ /**
+ * Wraps an existing method of the target object, so that calls to it are
+ * intercepted by the given wrapper function. The wrapper function receives,
+ * as its first argument, the original `target` object, followed by each of
+ * the arguments passed to the original method.
+ *
+ * @param {object} target
+ * The original target object that the wrapped method belongs to.
+ * @param {function} method
+ * The method being wrapped. This is used as the target of the Proxy
+ * object which is created to wrap the method.
+ * @param {function} wrapper
+ * The wrapper function which is called in place of a direct invocation
+ * of the wrapped method.
+ *
+ * @returns {Proxy}
+ * A Proxy object for the given method, which invokes the given wrapper
+ * method in its place.
+ */
+ const wrapMethod = (target, method, wrapper) => {
+ return new Proxy(method, {
+ apply(targetMethod, thisObj, args) {
+ return wrapper.call(thisObj, target, ...args);
+ }
});
};
- };
+ let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
- /**
- * Wraps an existing method of the target object, so that calls to it are
- * intercepted by the given wrapper function. The wrapper function receives,
- * as its first argument, the original `target` object, followed by each of
- * the arguments passed to the orginal method.
- *
- * @param {object} target
- * The original target object that the wrapped method belongs to.
- * @param {function} method
- * The method being wrapped. This is used as the target of the Proxy
- * object which is created to wrap the method.
- * @param {function} wrapper
- * The wrapper function which is called in place of a direct invocation
- * of the wrapped method.
- *
- * @returns {Proxy}
- * A Proxy object for the given method, which invokes the given wrapper
- * method in its place.
- */
- const wrapMethod = (target, method, wrapper) => {
- return new Proxy(method, {
- apply(targetMethod, thisObj, args) {
- return wrapper.call(thisObj, target, ...args);
- },
- });
- };
+ /**
+ * Wraps an object in a Proxy which intercepts and wraps certain methods
+ * based on the given `wrappers` and `metadata` objects.
+ *
+ * @param {object} target
+ * The target object to wrap.
+ *
+ * @param {object} [wrappers = {}]
+ * An object tree containing wrapper functions for special cases. Any
+ * function present in this object tree is called in place of the
+ * method in the same location in the `target` object tree. These
+ * wrapper methods are invoked as described in {@see wrapMethod}.
+ *
+ * @param {object} [metadata = {}]
+ * An object tree containing metadata used to automatically generate
+ * Promise-based wrapper functions for asynchronous. Any function in
+ * the `target` object tree which has a corresponding metadata object
+ * in the same location in the `metadata` tree is replaced with an
+ * automatically-generated wrapper function, as described in
+ * {@see wrapAsyncFunction}
+ *
+ * @returns {Proxy