| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- /* global API msg */// msg.js
- /* global chromeLocal */// storage-util.js
- /* global compareRevision */// common.js
- /* global iconMan */
- /* global prefs */
- /* global tokenMan */
- 'use strict';
- const syncMan = (() => {
- //#region Init
- const SYNC_DELAY = 1; // minutes
- const SYNC_INTERVAL = 30; // minutes
- const STATES = Object.freeze({
- connected: 'connected',
- connecting: 'connecting',
- disconnected: 'disconnected',
- disconnecting: 'disconnecting',
- });
- const STORAGE_KEY = 'sync/state/';
- const status = /** @namespace SyncManager.Status */ {
- STATES,
- state: STATES.disconnected,
- syncing: false,
- progress: null,
- currentDriveName: null,
- errorMessage: null,
- login: false,
- };
- let lastError = null;
- let ctrl;
- let currentDrive;
- /** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
- let ready = prefs.ready.then(() => {
- ready = true;
- prefs.subscribe('sync.enabled',
- (_, val) => val === 'none'
- ? syncMan.stop()
- : syncMan.start(val, true),
- {runNow: true});
- });
- chrome.alarms.onAlarm.addListener(async ({name}) => {
- if (name === 'syncNow') {
- await syncMan.syncNow();
- }
- });
- //#endregion
- //#region Exports
- return {
- async delete(...args) {
- if (ready.then) await ready;
- if (!currentDrive) return;
- schedule();
- return ctrl.delete(...args);
- },
- /** @returns {Promise<SyncManager.Status>} */
- async getStatus() {
- return status;
- },
- async login(name) {
- if (ready.then) await ready;
- if (!name) name = prefs.get('sync.enabled');
- await tokenMan.revokeToken(name);
- try {
- await tokenMan.getToken(name, true);
- status.login = true;
- } catch (err) {
- status.login = false;
- throw err;
- } finally {
- emitStatusChange();
- }
- },
- async put(...args) {
- if (ready.then) await ready;
- if (!currentDrive) return;
- schedule();
- return ctrl.put(...args);
- },
- async start(name, fromPref = false) {
- if (ready.then) await ready;
- if (!ctrl) await initController();
- if (currentDrive) return;
- currentDrive = getDrive(name);
- ctrl.use(currentDrive);
- status.state = STATES.connecting;
- status.currentDriveName = currentDrive.name;
- emitStatusChange();
- if (fromPref) {
- status.login = true;
- } else {
- try {
- await syncMan.login(name);
- } catch (err) {
- console.error(err);
- status.errorMessage = err.message;
- lastError = err;
- emitStatusChange();
- return syncMan.stop();
- }
- }
- await ctrl.init();
- await syncMan.syncNow(name);
- prefs.set('sync.enabled', name);
- status.state = STATES.connected;
- schedule(SYNC_INTERVAL);
- emitStatusChange();
- },
- async stop() {
- if (ready.then) await ready;
- if (!currentDrive) return;
- chrome.alarms.clear('syncNow');
- status.state = STATES.disconnecting;
- emitStatusChange();
- try {
- await ctrl.uninit();
- await tokenMan.revokeToken(currentDrive.name);
- await chromeLocal.remove(STORAGE_KEY + currentDrive.name);
- } catch (e) {}
- currentDrive = null;
- prefs.set('sync.enabled', 'none');
- status.state = STATES.disconnected;
- status.currentDriveName = null;
- status.login = false;
- emitStatusChange();
- },
- async syncNow() {
- if (ready.then) await ready;
- if (!currentDrive || !status.login) {
- console.warn('cannot sync when disconnected');
- return;
- }
- try {
- await ctrl.syncNow();
- status.errorMessage = null;
- lastError = null;
- } catch (err) {
- err.message = translateErrorMessage(err);
- status.errorMessage = err.message;
- lastError = err;
- if (isGrantError(err)) {
- status.login = false;
- }
- }
- emitStatusChange();
- },
- };
- //#endregion
- //#region Utils
- async function initController() {
- await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */
- ctrl = dbToCloud.dbToCloud({
- onGet(id) {
- return API.styles.getByUUID(id);
- },
- onPut(doc) {
- return API.styles.putByUUID(doc);
- },
- onDelete(id, rev) {
- return API.styles.deleteByUUID(id, rev);
- },
- async onFirstSync() {
- for (const i of await API.styles.getAll()) {
- ctrl.put(i._id, i._rev);
- }
- },
- onProgress(e) {
- if (e.phase === 'start') {
- status.syncing = true;
- } else if (e.phase === 'end') {
- status.syncing = false;
- status.progress = null;
- } else {
- status.progress = e;
- }
- emitStatusChange();
- },
- compareRevision,
- getState(drive) {
- return chromeLocal.getValue(STORAGE_KEY + drive.name);
- },
- setState(drive, state) {
- return chromeLocal.setValue(STORAGE_KEY + drive.name, state);
- },
- retryMaxAttempts: 10,
- retryExp: 1.2,
- retryDelay: 6,
- });
- }
- function emitStatusChange() {
- msg.broadcastExtension({method: 'syncStatusUpdate', status});
- iconMan.overrideBadge(getErrorBadge());
- }
- function isNetworkError(err) {
- return (
- err.name === 'TypeError' && /networkerror|failed to fetch/i.test(err.message) ||
- err.code === 502
- );
- }
- function isGrantError(err) {
- if (err.code === 401) return true;
- if (err.code === 400 && /invalid_grant/.test(err.message)) return true;
- if (err.name === 'TokenError') return true;
- return false;
- }
- function getErrorBadge() {
- if (status.state === STATES.connected &&
- (!status.login || lastError && !isNetworkError(lastError))) {
- return {
- text: 'x',
- color: '#F00',
- title: !status.login ? 'syncErrorRelogin' : `${
- chrome.i18n.getMessage('syncError')
- }\n---------------------\n${
- // splitting to limit each line length
- lastError.message.replace(/.{60,}?\s(?=.{30,})/g, '$&\n')
- }`,
- };
- }
- }
- function getDrive(name) {
- if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
- return dbToCloud.drive[name]({
- getAccessToken: () => tokenMan.getToken(name),
- });
- }
- throw new Error(`unknown cloud name: ${name}`);
- }
- function schedule(delay = SYNC_DELAY) {
- chrome.alarms.create('syncNow', {
- delayInMinutes: delay, // fractional values are supported
- periodInMinutes: SYNC_INTERVAL,
- });
- }
- function translateErrorMessage(err) {
- if (err.name === 'LockError') {
- return browser.i18n.getMessage('syncErrorLock', new Date(err.expire).toLocaleString([], {timeStyle: 'short'}));
- }
- return err.message || String(err);
- }
- //#endregion
- })();
|