auto-promisify browser.* methods on call

This commit is contained in:
tophf 2020-11-11 14:53:40 +03:00
parent 3db6662d2f
commit 0b3e027bfd
13 changed files with 253 additions and 220 deletions

View File

@ -38,14 +38,11 @@ const sync = (() => {
}, },
getState(drive) { getState(drive) {
const key = `sync/state/${drive.name}`; const key = `sync/state/${drive.name}`;
return chromeLocal.get(key) return chromeLocal.getValue(key);
.then(obj => obj[key]);
}, },
setState(drive, state) { setState(drive, state) {
const key = `sync/state/${drive.name}`; const key = `sync/state/${drive.name}`;
return chromeLocal.set({ return chromeLocal.setValue(key, state);
[key]: state
});
} }
}); });

View File

@ -1,12 +1,8 @@
/* global chromeLocal promisifyChrome webextLaunchWebAuthFlow FIREFOX */ /* global chromeLocal webextLaunchWebAuthFlow FIREFOX */
/* exported tokenManager */ /* exported tokenManager */
'use strict'; 'use strict';
const tokenManager = (() => { const tokenManager = (() => {
promisifyChrome({
'windows': ['create', 'update', 'remove'],
'tabs': ['create', 'update', 'remove']
});
const AUTH = { const AUTH = {
dropbox: { dropbox: {
flow: 'token', flow: 'token',
@ -93,24 +89,20 @@ const tokenManager = (() => {
}); });
} }
function revokeToken(name) { async function revokeToken(name) {
const provider = AUTH[name]; const provider = AUTH[name];
const k = buildKeys(name); const k = buildKeys(name);
return revoke() if (provider.revoke) {
.then(() => chromeLocal.remove(k.LIST)); try {
const token = await chromeLocal.getValue(k.TOKEN);
function revoke() { if (token) {
if (!provider.revoke) { await provider.revoke(token);
return Promise.resolve();
} }
return chromeLocal.get(k.TOKEN) } catch (e) {
.then(obj => { console.error(e);
if (obj[k.TOKEN]) {
return provider.revoke(obj[k.TOKEN]);
} }
})
.catch(console.error);
} }
await chromeLocal.remove(k.LIST);
} }
function refreshToken(name, k, obj) { function refreshToken(name, k, obj) {

View File

@ -264,9 +264,9 @@
debounce(flushQueue, text && checkingAll ? 1000 : 0); debounce(flushQueue, text && checkingAll ? 1000 : 0);
} }
function flushQueue(lines) { async function flushQueue(lines) {
if (!lines) { if (!lines) {
chromeLocal.getValue('updateLog', []).then(flushQueue); flushQueue(await chromeLocal.getValue('updateLog') || []);
return; return;
} }
const time = Date.now() - logLastWriteTime > 11e3 ? const time = Date.now() - logLastWriteTime > 11e3 ?

View File

@ -151,11 +151,10 @@ lazyInit();
if (onBoundsChanged) { if (onBoundsChanged) {
// * movement is reported even if the window wasn't resized // * movement is reported even if the window wasn't resized
// * fired just once when done so debounce is not needed // * fired just once when done so debounce is not needed
onBoundsChanged.addListener(wnd => { onBoundsChanged.addListener(async wnd => {
// getting the current window id as it may change if the user attached/detached the tab // getting the current window id as it may change if the user attached/detached the tab
chrome.windows.getCurrent(ownWnd => { const {id} = await browser.windows.getCurrent();
if (wnd.id === ownWnd.id) saveWindowPos(); if (id === wnd.id) saveWindowPos();
});
}); });
} }
window.on('resize', () => { window.on('resize', () => {
@ -325,7 +324,15 @@ lazyInit();
/* Stuff not needed for the main init so we can let it run at its own tempo */ /* Stuff not needed for the main init so we can let it run at its own tempo */
function lazyInit() { function lazyInit() {
let ownTabId; let ownTabId;
getOwnTab().then(async tab => { // not using `await` so we don't block the subsequent code
getOwnTab().then(patchHistoryBack);
// no windows on android
if (chrome.windows) {
restoreWindowSize();
detectWindowedState();
chrome.tabs.onAttached.addListener(onAttached);
}
async function patchHistoryBack(tab) {
ownTabId = tab.id; ownTabId = tab.id;
// use browser history back when 'back to manage' is clicked // use browser history back when 'back to manage' is clicked
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) { if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
@ -336,29 +343,23 @@ function lazyInit() {
history.back(); history.back();
}; };
} }
});
// no windows on android
if (!chrome.windows) {
return;
} }
// resize on 'undo close' /** resize on 'undo close' */
function restoreWindowSize() {
const pos = tryJSONparse(sessionStorage.windowPos); const pos = tryJSONparse(sessionStorage.windowPos);
delete sessionStorage.windowPos; delete sessionStorage.windowPos;
if (pos && pos.left != null && chrome.windows) { if (pos && pos.left != null && chrome.windows) {
chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos); chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
} }
// detect isWindowed
if (prefs.get('openEditInWindow') && history.length === 1) {
chrome.tabs.query({currentWindow: true}, tabs => {
if (tabs.length === 1) {
chrome.windows.getAll(windows => {
isWindowed = windows.length > 1; // not modifying the main browser window
});
} }
}); async function detectWindowedState() {
isWindowed =
prefs.get('openEditInWindow') &&
history.length === 1 &&
browser.windows.getAll().length > 1 &&
(await browser.tabs.query({currentWindow: true})).length === 1;
} }
// toggle openEditInWindow async function onAttached(tabId, info) {
chrome.tabs.onAttached.addListener((tabId, info) => {
if (tabId !== ownTabId) { if (tabId !== ownTabId) {
return; return;
} }
@ -366,16 +367,15 @@ function lazyInit() {
prefs.set('openEditInWindow', false); prefs.set('openEditInWindow', false);
return; return;
} }
chrome.windows.get(info.newWindowId, {populate: true}, win => { const win = await browser.windows.get(info.newWindowId, {populate: true});
// If there's only one tab in this window, it's been dragged to new window // If there's only one tab in this window, it's been dragged to new window
const openEditInWindow = win.tabs.length === 1; const openEditInWindow = win.tabs.length === 1;
if (openEditInWindow && FIREFOX) {
// FF-only because Chrome retardedly resets the size during dragging // FF-only because Chrome retardedly resets the size during dragging
if (openEditInWindow && FIREFOX) {
chrome.windows.update(info.newWindowId, prefs.get('windowPosition')); chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
} }
prefs.set('openEditInWindow', openEditInWindow); prefs.set('openEditInWindow', openEditInWindow);
}); }
});
} }
function onRuntimeMessage(request) { function onRuntimeMessage(request) {

View File

@ -398,11 +398,13 @@
r.resolve(code); r.resolve(code);
} }
}); });
port.onDisconnect.addListener(() => { port.onDisconnect.addListener(async () => {
chrome.tabs.get(tabId, tab => const tab = await browser.tabs.get(tabId);
!chrome.runtime.lastError && tab.url === initialUrl if (!chrome.runtime.lastError && tab.url === initialUrl) {
? location.reload() location.reload();
: closeCurrentTab()); } else {
closeCurrentTab();
}
}); });
return (opts = {}) => new Promise((resolve, reject) => { return (opts = {}) => new Promise((resolve, reject) => {
const id = performance.now(); const id = performance.now();

View File

@ -1,7 +1,6 @@
/* exported getTab getActiveTab onTabReady stringAsRegExp openURL ignoreChromeError /* exported getTab getActiveTab onTabReady stringAsRegExp openURL ignoreChromeError
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */ closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */
/* global promisifyChrome */
'use strict'; 'use strict';
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(\d+)|$/)[1]); const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(\d+)|$/)[1]);
@ -92,10 +91,6 @@ if (chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() =
if (cls) document.documentElement.classList.add(cls); if (cls) document.documentElement.classList.add(cls);
} }
promisifyChrome({
tabs: ['create', 'get', 'getCurrent', 'move', 'query', 'update'],
windows: ['create', 'update'], // Android doesn't have chrome.windows
});
// FF57+ supports openerTabId, but not in Android // FF57+ supports openerTabId, but not in Android
// (detecting FF57 by the feature it added, not navigator.ua which may be spoofed in about:config) // (detecting FF57 by the feature it added, not navigator.ua which may be spoofed in about:config)
const openerTabIdSupported = (!FIREFOX || window.AbortController) && chrome.windows != null; const openerTabIdSupported = (!FIREFOX || window.AbortController) && chrome.windows != null;

View File

@ -1,13 +1,8 @@
/* global promisifyChrome */
/* global deepCopy getOwnTab URLS */ // not used in content scripts /* global deepCopy getOwnTab URLS */ // not used in content scripts
'use strict'; 'use strict';
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
window.INJECTED !== 1 && (() => { window.INJECTED !== 1 && (() => {
promisifyChrome({
runtime: ['sendMessage', 'getBackgroundPage'],
tabs: ['sendMessage', 'query'],
});
const TARGETS = Object.assign(Object.create(null), { const TARGETS = Object.assign(Object.create(null), {
all: ['both', 'tab', 'extension'], all: ['both', 'tab', 'extension'],
extension: ['both', 'extension'], extension: ['both', 'extension'],

View File

@ -5,52 +5,65 @@ self.INJECTED !== 1 && (() => {
//#region for content scripts and our extension pages //#region for content scripts and our extension pages
if (!window.browser || !browser.runtime) { if (!((window.browser || {}).runtime || {}).sendMessage) {
const createTrap = (base, parent) => { /* Auto-promisifier with a fallback to direct call on signature error.
const target = typeof base === 'function' ? () => {} : {}; The fallback isn't used now since we call all synchronous methods via `chrome` */
target.isTrap = true; const directEvents = ['addListener', 'removeListener', 'hasListener', 'hasListeners'];
return new Proxy(target, { // generated by tools/chrome-api-no-cb.js
get: (target, prop) => { const directMethods = {
if (target[prop]) return target[prop]; alarms: ['create'],
if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) { extension: ['getBackgroundPage', 'getExtensionTabs', 'getURL', 'getViews', 'setUpdateUrlData'],
target[prop] = createTrap(base[prop], base); i18n: ['getMessage', 'getUILanguage'],
return target[prop]; identity: ['getRedirectURL'],
} runtime: ['connect', 'connectNative', 'getManifest', 'getURL', 'reload', 'restart'],
return base[prop]; tabs: ['connect'],
},
apply: (target, thisArg, args) => base.apply(parent, args)
});
}; };
window.browser = createTrap(chrome, null); const promisify = function (fn, ...args) {
let res;
try {
let resolve, reject;
/* Some callbacks have 2 parameters so we're resolving as an array in that case.
For example, chrome.runtime.requestUpdateCheck and chrome.webRequest.onAuthRequired */
args.push((...results) =>
chrome.runtime.lastError ?
reject(new Error(chrome.runtime.lastError.message)) :
resolve(results.length <= 1 ? results[0] : results));
fn.apply(this, args);
res = new Promise((...rr) => ([resolve, reject] = rr));
} catch (err) {
if (!err.message.includes('No matching signature')) {
throw err;
}
args.pop();
res = fn.apply(this, args);
}
return res;
};
const proxify = (src, srcName, target, key) => {
let res = src[key];
if (res && typeof res === 'object') {
res = createProxy(res, key); // eslint-disable-line no-use-before-define
} else if (typeof res === 'function') {
res = (directMethods[srcName] || directEvents).includes(key)
? res.bind(src)
: promisify.bind(src, res);
}
target[key] = res;
return res;
};
const createProxy = (src, srcName) =>
new Proxy({}, {
get(target, key) {
return target[key] || proxify(src, srcName, target, key);
},
});
window.browser = createProxy(chrome);
} }
/* Promisifies the specified `chrome` methods into `browser`. //#endregion
The definitions is an object like this: {
'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.`
windows: ['create', 'update'], // items and sub-objects will only be created if present in `chrome`
} */
window.promisifyChrome = definitions => {
for (const [scopeName, methods] of Object.entries(definitions)) {
const path = scopeName.split('.');
const src = path.reduce((obj, p) => obj && obj[p], chrome);
if (!src) continue;
const dst = path.reduce((obj, p) => obj[p] || (obj[p] = {}), browser);
for (const name of methods) {
const fn = src[name];
if (!fn || dst[name] && !dst[name].isTrap) continue;
dst[name] = (...args) => new Promise((resolve, reject) =>
fn.call(src, ...args, (...results) =>
chrome.runtime.lastError ?
reject(chrome.runtime.lastError) :
resolve(results.length <= 1 ? results[0] : results)));
// a couple of callbacks have 2 parameters (we don't use those methods, but just in case)
}
}
};
if (!chrome.tabs) return; if (!chrome.tabs) return;
//#endregion
//#region for our extension pages //#region for our extension pages
for (const storage of ['localStorage', 'sessionStorage']) { for (const storage of ['localStorage', 'sessionStorage']) {
@ -77,5 +90,6 @@ self.INJECTED !== 1 && (() => {
} }
}; };
} }
//#endregion //#endregion
})(); })();

View File

@ -1,4 +1,4 @@
/* global promisifyChrome msg API */ /* global msg API */
/* global deepCopy debounce */ // not used in content scripts /* global deepCopy debounce */ // not used in content scripts
'use strict'; 'use strict';
@ -114,11 +114,6 @@ window.INJECTED !== 1 && (() => {
any: new Set(), any: new Set(),
specific: {}, specific: {},
}; };
if (msg.isBg) {
promisifyChrome({
'storage.sync': ['get', 'set'],
});
}
// getPrefs may fail on browser startup in the active tab as it loads before the background script // getPrefs may fail on browser startup in the active tab as it loads before the background script
const initializing = (msg.isBg ? readStorage() : API.getPrefs().catch(readStorage)) const initializing = (msg.isBg ? readStorage() : API.getPrefs().catch(readStorage))
.then(setAll); .then(setAll);
@ -236,11 +231,8 @@ window.INJECTED !== 1 && (() => {
} }
function readStorage() { function readStorage() {
/* Using a non-promisified call since this code may also run in a content script return browser.storage.sync.get(STORAGE_KEY)
when API.getPrefs occasionally fails during browser startup in the active tab */ .then(data => data[STORAGE_KEY]);
return new Promise(resolve =>
chrome.storage.sync.get(STORAGE_KEY, data =>
resolve(data[STORAGE_KEY])));
} }
function updateStorage() { function updateStorage() {

View File

@ -1,72 +1,51 @@
/* global loadScript tryJSONparse promisifyChrome */ /* global loadScript tryJSONparse */
/* exported chromeLocal chromeSync */
'use strict'; 'use strict';
promisifyChrome({ (() => {
'storage.local': ['get', 'remove', 'set'], /** @namespace StorageExtras */
'storage.sync': ['get', 'remove', 'set'], const StorageExtras = {
}); async getValue(key) {
return (await this.get(key))[key];
const [chromeLocal, chromeSync] = (() => { },
return [ async setValue(key, value) {
createWrapper('local'), await this.set({[key]: value});
createWrapper('sync'), },
]; async getLZValue(key) {
return (await this.getLZValues([key]))[key];
function createWrapper(name) { },
const storage = browser.storage[name]; async getLZValues(keys = Object.values(this.LZ_KEY)) {
const wrapper = { const [data, LZString] = await Promise.all([
get: storage.get.bind(storage), this.get(keys),
set: data => storage.set(data).then(() => data), this.getLZString(),
remove: storage.remove.bind(storage), ]);
/**
* @param {String} key
* @param {Any} [defaultValue]
* @returns {Promise<any>}
*/
getValue: (key, defaultValue) =>
wrapper.get(
defaultValue !== undefined ?
{[key]: defaultValue} :
key
).then(data => data[key]),
setValue: (key, value) => wrapper.set({[key]: value}),
getLZValue: key => wrapper.getLZValues([key]).then(data => data[key]),
getLZValues: (keys = Object.values(wrapper.LZ_KEY)) =>
Promise.all([
wrapper.get(keys),
loadLZStringScript(),
]).then(([data = {}, LZString]) => {
for (const key of keys) { for (const key of keys) {
const value = data[key]; const value = data[key];
data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value)); data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
} }
return data; return data;
}), },
setLZValue: (key, value) => async setLZValue(key, value) {
loadLZStringScript().then(LZString => const LZString = await this.getLZString();
wrapper.set({ return this.setValue(key, LZString.compressToUTF16(JSON.stringify(value)));
[key]: LZString.compressToUTF16(JSON.stringify(value)), },
})), async getLZString() {
if (!window.LZString) {
loadLZStringScript, await loadScript('/vendor/lz-string-unsafe/lz-string-unsafe.min.js');
window.LZString = window.LZString || window.LZStringUnsafe;
}
return window.LZString;
},
}; };
return wrapper; /** @namespace StorageExtrasSync */
} const StorageExtrasSync = {
LZ_KEY: {
function loadLZStringScript() {
return window.LZString ?
Promise.resolve(window.LZString) :
loadScript('/vendor/lz-string-unsafe/lz-string-unsafe.min.js').then(() =>
(window.LZString = window.LZString || window.LZStringUnsafe));
}
})();
chromeSync.LZ_KEY = {
csslint: 'editorCSSLintConfig', csslint: 'editorCSSLintConfig',
stylelint: 'editorStylelintConfig', stylelint: 'editorStylelintConfig',
usercssTemplate: 'usercssTemplate', usercssTemplate: 'usercssTemplate',
}; },
};
/** @type {chrome.storage.StorageArea|StorageExtras} */
window.chromeLocal = Object.assign(browser.storage.local, StorageExtras);
/** @type {chrome.storage.StorageArea|StorageExtras|StorageExtrasSync} */
window.chromeSync = Object.assign(browser.storage.sync, StorageExtras, StorageExtrasSync);
})();

View File

@ -257,8 +257,7 @@ async function importFromString(jsonString) {
// Must acquire the permission before setting the pref // Must acquire the permission before setting the pref
if (CHROME && !chrome.declarativeContent && if (CHROME && !chrome.declarativeContent &&
stats.options.names.find(_ => _.name === 'styleViaXhr' && _.isValid && _.val)) { stats.options.names.find(_ => _.name === 'styleViaXhr' && _.isValid && _.val)) {
await new Promise(resolve => await browser.permissions.request({permissions: ['declarativeContent']});
chrome.permissions.request({permissions: ['declarativeContent']}, resolve));
} }
const oldStorage = await chromeSync.get(); const oldStorage = await chromeSync.get();
for (const {name, val, isValid, isPref} of stats.options.names) { for (const {name, val, isValid, isPref} of stats.options.names) {

View File

@ -88,16 +88,12 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
} }
} }
function initTabUrls() { async function initTabUrls() {
return getActiveTab() let tab = await getActiveTab();
.then((tab = {}) => if (FIREFOX && tab.status === 'loading' && tab.url === ABOUT_BLANK) {
FIREFOX && tab.status === 'loading' && tab.url === ABOUT_BLANK tab = await waitForTabUrlFF(tab);
? waitForTabUrlFF(tab) }
: tab) let frames = await browser.webNavigation.getAllFrames({tabId: tab.id});
.then(tab => new Promise(resolve =>
chrome.webNavigation.getAllFrames({tabId: tab.id}, frames =>
resolve({frames, tab}))))
.then(({frames, tab}) => {
let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting
frames = sortTabFrames(frames); frames = sortTabFrames(frames);
if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) { if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) {
@ -109,7 +105,6 @@ function initTabUrls() {
} }
tabURL = frames[0].url = url; tabURL = frames[0].url = url;
return frames; return frames;
});
} }
/** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */ /** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */

73
tools/chrome-api-no-cb.js Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env node
/*
Generates a list of callbackless chrome.* API methods from chromium source
to be used in polyfill.js
*/
'use strict';
const manifest = require('../manifest.json');
const fetch = require('make-fetch-happen');
(async () => {
manifest.permissions.push('extension', 'i18n', 'runtime');
const FN_NO_CB = /\bstatic (\w+) (\w+)(?![^)]*callback)\(\s*([^)]*)\)/g;
const BASE = 'https://github.com/chromium/chromium/raw/master/';
const PATHS = [
[BASE + 'extensions/common/api/', 'schema.gni'],
[BASE + 'chrome/common/extensions/api/', 'api_sources.gni'],
];
console.debug('Downloading...');
const schemas = await Promise.all(PATHS.map(([path, name]) => fetchText(path + name)));
const files = {};
schemas.forEach((text, i) => {
const path = PATHS[i][0];
text.match(/\w+\.(idl|json)/g).forEach(name => {
files[name] = path;
});
});
const resList = [];
const resObj = {};
await Promise.all(Object.entries(files).map(processApi));
Object.entries(resObj)
.sort(([a], [b]) => a < b ? -1 : a > b)
.forEach(([key, val]) => {
delete resObj[key];
resObj[key] = val;
val.sort();
});
console.log(resList.sort().join('\n'));
console.log(JSON.stringify(resObj));
async function fetchText(file) {
return (await fetch(file)).text();
}
async function processApi([file, path]) {
const [name, ext] = file.split('.');
const api = manifest.permissions.find(p =>
name === p.replace(/([A-Z])/g, s => '_' + s.toLowerCase()) ||
name === p.replace(/\./g, '_'));
if (!api) return;
const text = await fetchText(path + file);
const noCmt = text.replace(/^\s*\/\/.*$/gm, '');
if (ext === 'idl') {
const fnBlock = (noCmt.split(/\n\s*interface Functions {\s*/)[1] || '')
.split(/\n\s*interface \w+ {/)[0];
for (let m; (m = FN_NO_CB.exec(fnBlock));) {
const [, type, name, params] = m;
resList.push(`chrome.${api}.${name}(${params.replace(/\n\s*/g, ' ')}): ${type}`);
(resObj[api] || (resObj[api] = [])).push(name);
}
} else {
for (const fn of JSON.parse(noCmt)[0].functions || []) {
const last = fn.parameters[fn.parameters.length - 1];
if (!fn.returns_async && (!last || last.type !== 'function')) {
resList.push(`chrome.${api}.${fn.name}(${
fn.parameters.map(p => `${p.optional ? '?' : ''}${p.name}: ${p.type}`).join(', ')
})`);
(resObj[api] || (resObj[api] = [])).push(fn.name);
}
}
}
}
})();