diff --git a/README.md b/README.md
index 5d6a5e8..9254c67 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ satori-enhancements is a WebExtension built for Jagiellonian University's
to improve usability, ease of use and add some useful features, that should
have been there since the beginning (but for some reason, they aren't).
-![Satori Enhancements results page](screenshots/results.png)
+![Satori Enhancements results page](screenshots/results-2x.png)
## Features
diff --git a/ext/html/offscreen.html b/ext/html/offscreen.html
new file mode 100644
index 0000000..c707b01
--- /dev/null
+++ b/ext/html/offscreen.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ext/js/background.js b/ext/js/background.js
index cd56dd4..fce50f2 100644
--- a/ext/js/background.js
+++ b/ext/js/background.js
@@ -1,3 +1,7 @@
+if (typeof importScripts !== 'undefined') {
+ importScripts('../vendor/browser-polyfill.js', 'config.js', 'common.js');
+}
+
(function () {
'use strict';
@@ -16,6 +20,21 @@
const contestProblemList = {};
+ /**
+ * The offscreen document being currently created, if any.
+ */
+ let offscreenBeingCreated;
+
+ /**
+ * Return the web browser name this extension is running on.
+ * @returns {string} 'chrome' or 'firefox' depending on the browser
+ */
+ function currentBrowser() {
+ return browser.runtime.getURL('').startsWith('chrome-extension://')
+ ? 'chrome'
+ : 'firefox';
+ }
+
function displayStatusNotification(submitID, problemCode, status) {
browser.notifications.create({
type: 'basic',
@@ -30,31 +49,32 @@
* variable.
* @see setUpLastContestRedirect
*/
- function retrieveLastContestID() {
- storage.get('lastContestID').then((response) => {
- lastContestID = response.lastContestID;
- });
+ async function retrieveLastContestID() {
+ const response = await storage.get('lastContestID');
+ lastContestID = response.lastContestID;
}
/**
- * Save last contest ID both in Storage and our local variable.
+ * Save last contest ID both in Storage and our local variable
+ * and update redirect rules.
* @param {string} contestID last contest ID
* @see setUpLastContestRedirect
*/
function saveLastContestID(contestID) {
storage.set({ lastContestID: contestID });
lastContestID = contestID;
+ updateLastContestRedirectRules();
}
/**
- * Display extension's page action.
- * @param tab tab object
+ * Display extension's action.
*/
- function enablePageAction(tab) {
- browser.pageAction.show(tab.id);
- browser.pageAction.onClicked.addListener(() =>
- browser.runtime.openOptionsPage(),
- );
+ function enableAction() {
+ const listener = () => {
+ browser.runtime.openOptionsPage();
+ };
+
+ browser.action.onClicked.addListener(listener);
}
/**
@@ -114,13 +134,48 @@
}
/**
- * Add an onBeforeRequest listener that redirects to the last contest
+ * Sets up a declarative net request rules to redirect to the last contest
* if the user just entered Satori webpage.
+ */
+ function updateLastContestRedirectRules() {
+ const addRules = [];
+ if (typeof lastContestID !== 'undefined' && lastContestID !== null) {
+ addRules.push(
+ ...[
+ { id: 1, urlFilter: '||satori.tcs.uj.edu.pl/|' },
+ { id: 2, urlFilter: '||satori.tcs.uj.edu.pl/news' },
+ ].map(({ id, urlFilter }) => ({
+ id,
+ condition: {
+ urlFilter,
+ // Redirect only if the user just opened Satori on a given tab
+ excludedTabIds: [...satoriTabs.values()],
+ resourceTypes: ['main_frame'],
+ },
+ action: {
+ type: 'redirect',
+ redirect: {
+ url: SATORI_URL_CONTEST + lastContestID + '/',
+ },
+ },
+ })),
+ );
+ }
+ browser.declarativeNetRequest.updateSessionRules({
+ removeRuleIds: [1, 2],
+ addRules,
+ });
+ }
+
+ /**
+ * Enables the last contest redirect.
*
* Also, the function adds webNavigation.onCommitted and tabs.onRemoved
* listeners to keep the list of Satori tabs.
*/
function setUpLastContestRedirect() {
+ updateLastContestRedirectRules();
+
// Store which tabs have Satori open
browser.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (
@@ -132,52 +187,30 @@
} else {
satoriTabs.delete(tabId);
}
+ updateLastContestRedirectRules();
});
- 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'],
- );
+ browser.tabs.onRemoved.addListener((tabId) => {
+ satoriTabs.delete(tabId);
+ updateLastContestRedirectRules();
+ });
}
/**
* Add highlight.js CSS to the page using the selected style.
*/
- function injectHighlightJsCss(tab) {
- storage
+ async function injectHighlightJsCss(tab) {
+ await storage
.get({
[HIGHLIGHT_JS_STYLE_KEY]:
DEFAULT_SETTINGS[HIGHLIGHT_JS_STYLE_KEY],
})
- .then((response) => {
+ .then(async (response) => {
let style = response.highlightJsStyle;
if (style !== 'none') {
- browser.tabs.insertCSS(tab.id, {
- file: `vendor/bower/hjsstyles/${style}.css`,
+ await browser.scripting.insertCSS({
+ files: [`vendor/bower/hjsstyles/${style}.css`],
+ target: { tabId: tab.id },
});
}
});
@@ -272,9 +305,22 @@
if (!response.ok) {
throw new Error(`HTTP Status ${response.status}`);
}
- contestProblemList[contestID] = parseProblemList(
- $.parseHTML(await response.text()),
- );
+ const responseText = await response.text();
+
+ if (currentBrowser() === 'chrome') {
+ await setupOffscreenDocument('/html/offscreen.html');
+ const parseResponse = await chrome.runtime.sendMessage({
+ type: 'parseProblemList',
+ target: 'offscreen',
+ data: responseText,
+ });
+ await chrome.offscreen.closeDocument();
+
+ contestProblemList[contestID] = parseResponse;
+ } else {
+ contestProblemList[contestID] =
+ parseProblemList(responseText);
+ }
} catch (error) {
console.error(error);
}
@@ -282,6 +328,35 @@
return contestProblemList[contestID] ?? {};
}
+ /**
+ * Setup offscreen document for parsing HTML
+ * @param path path to the offscreen document
+ * @returns {Promise} promise that resolves when the document is ready
+ */
+ async function setupOffscreenDocument(path) {
+ const offscreenUrl = chrome.runtime.getURL(path);
+ const existingContexts = await chrome.runtime.getContexts({
+ contextTypes: ['OFFSCREEN_DOCUMENT'],
+ documentUrls: [offscreenUrl],
+ });
+
+ if (existingContexts.length > 0) {
+ return;
+ }
+
+ if (offscreenBeingCreated) {
+ await offscreenBeingCreated;
+ } else {
+ offscreenBeingCreated = chrome.offscreen.createDocument({
+ url: path,
+ reasons: [chrome.offscreen.Reason.DOM_PARSER],
+ justification: 'Parse DOM',
+ });
+ await offscreenBeingCreated;
+ offscreenBeingCreated = null;
+ }
+ }
+
/**
* Save joined contest list in Storage
* @param {array} contestList contest list
@@ -298,21 +373,25 @@
return (await storage.get('contestList')).contestList ?? [];
}
- retrieveLastContestID();
- setUpLastContestRedirect();
+ retrieveLastContestID().then(() => {
+ setUpLastContestRedirect();
+ });
setUpSessionCookies();
- browser.runtime.onMessage.addListener((request, sender) => {
- if (request.action === 'enablePageAction') {
- enablePageAction(sender.tab);
+ browser.runtime.onMessage.addListener(async (request, sender) => {
+ if (request.action === 'enableAction') {
+ enableAction(sender.tab);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'saveLastContestID') {
saveLastContestID(getContestID(sender.url));
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'displayStatusNotification') {
displayStatusNotification(
request.submitID,
request.problemCode,
request.problemStatus,
);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'modifyContestItemList') {
modifyContestItemList(
request.listName,
@@ -320,6 +399,7 @@
request.value,
request.add,
);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'getContestItemList') {
return new Promise((resolve) => {
getContestItemList(
@@ -329,16 +409,21 @@
);
});
} else if (request.action === 'injectHighlightJsCss') {
- injectHighlightJsCss(sender.tab);
+ await injectHighlightJsCss(sender.tab);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'saveContestProblemList') {
saveContestProblemList(request.contestID, request.problems);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'getContestProblemList') {
return getProblemList(request.contestID);
} else if (request.action === 'setJoinedContestList') {
saveJoinedContestList(request.contestList);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'getJoinedContestList') {
return getJoinedContestList();
}
- return new Promise((resolve) => resolve(null));
+
+ console.warn(`Unexpected message type received: '${request.action}'.`);
+ return false;
});
})();
diff --git a/ext/js/common.js b/ext/js/common.js
index eb8c55f..5e76a97 100644
--- a/ext/js/common.js
+++ b/ext/js/common.js
@@ -27,7 +27,9 @@ if (typeof module !== 'undefined') {
};
}
-function parseProblemList(jqueryHandles) {
+function parseProblemList(htmlText) {
+ const jqueryHandles = $.parseHTML(htmlText);
+
return Object.fromEntries(
jqueryHandles.flatMap((el) =>
[
diff --git a/ext/js/general.js b/ext/js/general.js
index 16a5f0f..a80285d 100644
--- a/ext/js/general.js
+++ b/ext/js/general.js
@@ -1,11 +1,11 @@
(function () {
'use strict';
- browser.runtime.sendMessage({ action: 'enablePageAction' });
+ browser.runtime.sendMessage({ action: 'enableAction' });
- 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.js b/ext/js/offscreen.js
new file mode 100644
index 0000000..20c0aa1
--- /dev/null
+++ b/ext/js/offscreen.js
@@ -0,0 +1,12 @@
+browser.runtime.onMessage.addListener(async (request) => {
+ if (request.target !== 'offscreen') {
+ return false;
+ }
+
+ if (request.type === 'parseProblemList') {
+ return parseProblemList(request.data);
+ }
+
+ console.warn(`Unexpected message type received: '${request.type}'.`);
+ return false;
+});
diff --git a/ext/manifest.json b/ext/manifest.json
index 76d490b..e5a11c2 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -1,16 +1,16 @@
{
- "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": {
"id": "{a7a2f4f4-8648-4221-8e88-af047e0fe5ca}",
- "strict_min_version": "42.0"
+ "strict_min_version": "132.0"
}
},
@@ -21,36 +21,43 @@
},
"options_ui": {
- "page": "options.html",
- "chrome_style": true,
- "browser_style": true
+ "page": "options.html"
},
"permissions": [
"storage",
"notifications",
"webRequest",
- "webRequestBlocking",
+ "declarativeNetRequestWithHostAccess",
"cookies",
- "*://satori.tcs.uj.edu.pl/*"
+ "scripting",
+ "offscreen"
],
- "page_action": {
+ "host_permissions": ["*://satori.tcs.uj.edu.pl/*"],
+
+ "action": {
"default_icon": "icon128.png",
"default_title": "Satori Enhancements"
},
"background": {
+ "service_worker": "js/background.js",
"scripts": [
"vendor/browser-polyfill.js",
- "vendor/bower/jquery.min.js",
"js/config.js",
+ "vendor/bower/jquery.min.js",
"js/common.js",
"js/background.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/scss/options.scss b/ext/scss/options.scss
index aee1589..a607e06 100644
--- a/ext/scss/options.scss
+++ b/ext/scss/options.scss
@@ -1,3 +1,19 @@
+html,
+body {
+ font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ color: #333;
+ background-color: #fff;
+}
+
+body {
+ margin: 1em;
+}
+
+h2 {
+ font-weight: 400;
+}
+
h2:not(:first-child) {
margin-top: 35px;
}
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