stylus/background/sync.js

190 lines
4.2 KiB
JavaScript

/* global dbToCloud styleManager chromeLocal prefs tokenManager msg */
/* exported sync */
'use strict';
const sync = (() => {
const SYNC_DELAY = 1;
const SYNC_INTERVAL = 30;
const status = {
state: 'disconnected',
syncing: false,
currentDriveName: null
};
let currentDrive;
const ctrl = dbToCloud.dbToCloud({
onGet(id) {
return styleManager.getByUUID(id);
},
onPut(doc) {
return styleManager.putByUUID(doc);
},
onDelete(id, rev) {
return styleManager.deleteByUUID(id, rev);
},
onFirstSync() {
return styleManager.getAllStyles()
.then(styles => {
styles.forEach(i => ctrl.put(i._id, i._rev));
});
},
compareRevision(a, b) {
return styleManager.compareRevision(a, b);
},
getState(drive) {
const key = `sync/state/${drive.name}`;
return chromeLocal.get(key)
.then(obj => obj[key]);
},
setState(drive, state) {
const key = `sync/state/${drive.name}`;
return chromeLocal.set({
[key]: state
});
}
});
prefs.subscribe(['sync.enabled'], onPrefChange);
onPrefChange(null, prefs.get('sync.enabled'));
chrome.alarms.onAlarm.addListener(info => {
if (info.name === 'syncNow') {
ctrl.syncNow()
.catch(handle401Error)
.catch(console.error);
}
});
return {
start,
stop,
put: (...args) => {
schedule();
return ctrl.put(...args);
},
delete: (...args) => {
schedule();
return ctrl.delete(...args);
},
syncNow,
getStatus: () => status
};
function schedule() {
chrome.alarms.create('syncNow', {
delayInMinutes: SYNC_DELAY,
periodInMinutes: SYNC_INTERVAL
});
}
function onPrefChange(key, value) {
if (value === 'none') {
stop().catch(console.error);
} else {
start(value).catch(console.error);
}
}
function withFinally(p, cleanup) {
return p.then(
result => {
cleanup();
return result;
},
err => {
cleanup();
throw err;
}
);
}
function syncNow() {
if (status.syncing) {
return Promise.reject(new Error('still syncing'));
}
status.syncing = true;
emitChange();
return withFinally(
ctrl.syncNow().catch(handle401Error),
() => {
status.syncing = false;
emitChange();
}
);
}
function handle401Error(err) {
if (err.code === 401) {
return tokenManager.revokeToken(currentDrive.name)
.then(() => {
throw err;
});
}
throw err;
}
function emitChange() {
msg.broadcastExtension({method: 'syncStatusUpdate', status});
}
function start(name) {
if (currentDrive) {
return Promise.resolve();
}
currentDrive = getDrive(name);
ctrl.use(currentDrive);
prefs.set('sync.enabled', name);
status.state = 'connecting';
status.currentDriveName = currentDrive.name;
emitChange();
return withFinally(
ctrl.start()
.catch(err => {
if (/Authorization page could not be loaded/i.test(err.message)) {
// FIXME: Chrome always fail at the first login so we try again
return ctrl.syncNow();
}
throw err;
})
.catch(handle401Error),
() => {
chrome.alarms.create('syncNow', {periodInMinutes: SYNC_INTERVAL});
status.state = 'connected';
emitChange();
}
);
}
function getDrive(name) {
if (name === 'dropbox') {
return dbToCloud.drive.dropbox({
getAccessToken: () => tokenManager.getToken(name)
});
}
throw new Error(`unknown cloud name: ${name}`);
}
function stop() {
if (!currentDrive) {
return Promise.resolve();
}
chrome.alarms.clear('syncNow');
status.state = 'disconnecting';
emitChange();
return withFinally(
ctrl.stop()
.then(() => tokenManager.revokeToken(currentDrive.name))
.then(() => chromeLocal.remove(`sync/state/${currentDrive.name}`)),
() => {
currentDrive = null;
prefs.set('sync.enabled', 'none');
status.state = 'disconnected';
status.currentDriveName = null;
emitChange();
}
);
}
})();