2017-07-28 07:40:24 +00:00
|
|
|
import localDbSvc from './localDbSvc';
|
|
|
|
import store from '../store';
|
|
|
|
import welcomeFile from '../data/welcomeFile.md';
|
|
|
|
import utils from './utils';
|
2017-08-15 10:43:26 +00:00
|
|
|
import userActivitySvc from './userActivitySvc';
|
2017-08-17 23:10:35 +00:00
|
|
|
import gdriveAppDataProvider from './providers/gdriveAppDataProvider';
|
2017-08-15 10:43:26 +00:00
|
|
|
import googleHelper from './helpers/googleHelper';
|
2017-08-17 23:10:35 +00:00
|
|
|
import emptyContent from '../data/emptyContent';
|
|
|
|
import emptySyncContent from '../data/emptySyncContent';
|
2017-08-15 10:43:26 +00:00
|
|
|
|
|
|
|
const lastSyncActivityKey = 'lastSyncActivity';
|
|
|
|
let lastSyncActivity;
|
|
|
|
const getStoredLastSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
|
|
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
2017-08-17 23:10:35 +00:00
|
|
|
const restartSyncAfter = 30 * 1000; // 30 sec
|
|
|
|
const autoSyncAfter = utils.randomize(restartSyncAfter);
|
2017-08-15 10:43:26 +00:00
|
|
|
const isSyncAvailable = () => window.navigator.onLine !== false &&
|
|
|
|
!!store.getters['data/loginToken'];
|
|
|
|
|
|
|
|
function isSyncWindow() {
|
|
|
|
const storedLastSyncActivity = getStoredLastSyncActivity();
|
|
|
|
return lastSyncActivity === storedLastSyncActivity ||
|
|
|
|
Date.now() > inactivityThreshold + storedLastSyncActivity;
|
|
|
|
}
|
|
|
|
|
2017-08-17 23:10:35 +00:00
|
|
|
function isAutoSyncReady() {
|
2017-08-15 10:43:26 +00:00
|
|
|
const storedLastSyncActivity = getStoredLastSyncActivity();
|
|
|
|
return Date.now() > autoSyncAfter + storedLastSyncActivity;
|
|
|
|
}
|
|
|
|
|
|
|
|
function setLastSyncActivity() {
|
|
|
|
const currentDate = Date.now();
|
|
|
|
lastSyncActivity = currentDate;
|
|
|
|
localStorage[lastSyncActivityKey] = currentDate;
|
|
|
|
}
|
|
|
|
|
2017-08-17 23:10:35 +00:00
|
|
|
function getSyncProvider(syncLocation) {
|
|
|
|
switch (syncLocation.provider) {
|
|
|
|
default:
|
|
|
|
return gdriveAppDataProvider;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getSyncToken(syncLocation) {
|
|
|
|
switch (syncLocation.provider) {
|
|
|
|
default:
|
|
|
|
return store.getters['data/loginToken'];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function applyChanges(changes) {
|
|
|
|
const storeItemMap = { ...store.getters.allItemMap };
|
|
|
|
const syncData = { ...store.getters['data/syncData'] };
|
|
|
|
let syncDataChanged = false;
|
|
|
|
|
|
|
|
changes.forEach((change) => {
|
|
|
|
const existingSyncData = syncData[change.fileId];
|
|
|
|
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
|
|
|
|
if (change.removed && existingSyncData) {
|
|
|
|
if (existingItem) {
|
|
|
|
// Remove object from the store
|
|
|
|
store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
|
|
|
|
delete storeItemMap[existingItem.id];
|
|
|
|
}
|
|
|
|
delete syncData[change.fileId];
|
|
|
|
syncDataChanged = true;
|
|
|
|
} else if (!change.removed && change.item && change.item.updated) {
|
|
|
|
if (!existingSyncData || (existingSyncData.updated !== change.item.updated && (
|
|
|
|
!existingItem || existingItem.updated !== change.item.updated
|
|
|
|
))) {
|
|
|
|
// Put object in the store
|
|
|
|
if (change.item.type !== 'content') { // Merge contents later
|
|
|
|
store.commit(`${change.item.type}/setItem`, change.item);
|
|
|
|
storeItemMap[change.item.id] = change.item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
syncData[change.fileId] = change.syncData;
|
|
|
|
syncDataChanged = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (syncDataChanged) {
|
|
|
|
store.dispatch('data/setSyncData', syncData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-15 10:43:26 +00:00
|
|
|
function sync() {
|
|
|
|
const googleToken = store.getters['data/loginToken'];
|
|
|
|
return googleHelper.getChanges(googleToken)
|
|
|
|
.then((changes) => {
|
2017-08-17 23:10:35 +00:00
|
|
|
// Apply changes
|
|
|
|
applyChanges(changes);
|
|
|
|
googleHelper.updateNextPageToken(googleToken, changes);
|
|
|
|
|
|
|
|
// Prevent from sending items too long after changes have been retrieved
|
|
|
|
const syncStartTime = Date.now();
|
|
|
|
const ifNotTooLate = cb => (res) => {
|
|
|
|
if (syncStartTime + restartSyncAfter < Date.now()) {
|
|
|
|
throw new Error('too_late');
|
|
|
|
}
|
|
|
|
return cb(res);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Called until no item to save
|
|
|
|
const saveNextItem = ifNotTooLate(() => {
|
|
|
|
const storeItemMap = store.getters.syncedItemMap;
|
|
|
|
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
|
|
|
let result;
|
|
|
|
Object.keys(storeItemMap).some((id) => {
|
|
|
|
const item = storeItemMap[id];
|
|
|
|
const existingSyncData = syncDataByItemId[id];
|
|
|
|
if (!existingSyncData || existingSyncData.updated !== item.updated) {
|
|
|
|
result = googleHelper.saveItem(
|
|
|
|
googleToken,
|
|
|
|
// Use deepCopy to freeze objects
|
|
|
|
utils.deepCopy(item),
|
|
|
|
utils.deepCopy(existingSyncData),
|
|
|
|
ifNotTooLate,
|
|
|
|
)
|
|
|
|
.then(resultSyncData => store.dispatch('data/patchSyncData', {
|
|
|
|
[resultSyncData.id]: resultSyncData,
|
|
|
|
}))
|
|
|
|
.then(() => saveNextItem());
|
|
|
|
}
|
|
|
|
return result;
|
2017-08-15 10:43:26 +00:00
|
|
|
});
|
2017-08-17 23:10:35 +00:00
|
|
|
return result;
|
2017-08-15 10:43:26 +00:00
|
|
|
});
|
2017-08-17 23:10:35 +00:00
|
|
|
|
|
|
|
// Called until no item to remove
|
|
|
|
const removeNextItem = ifNotTooLate(() => {
|
|
|
|
const storeItemMap = store.getters.syncedItemMap;
|
|
|
|
const syncData = store.getters['data/syncData'];
|
|
|
|
let result;
|
|
|
|
Object.keys(syncData).some((id) => {
|
|
|
|
const existingSyncData = syncData[id];
|
|
|
|
if (!storeItemMap[existingSyncData.itemId]) {
|
|
|
|
// Use deepCopy to freeze objects
|
|
|
|
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
|
|
|
result = googleHelper.removeItem(googleToken, syncDataToRemove, ifNotTooLate)
|
|
|
|
.then(() => {
|
|
|
|
const syncDataCopy = { ...store.getters['data/syncData'] };
|
|
|
|
delete syncDataCopy[syncDataToRemove.id];
|
|
|
|
store.dispatch('data/setSyncData', syncDataCopy);
|
|
|
|
})
|
|
|
|
.then(() => removeNextItem());
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Get content `updated` field from itemMap or from localDbSvc if not loaded
|
|
|
|
const getContentUpdated = (contentId) => {
|
|
|
|
const loadedContent = store.state.content.itemMap[contentId];
|
|
|
|
return loadedContent ? loadedContent.updated : localDbSvc.updatedMap.content[contentId];
|
2017-08-15 10:43:26 +00:00
|
|
|
};
|
2017-08-17 23:10:35 +00:00
|
|
|
|
|
|
|
// Download current file content and contents that have changed
|
|
|
|
const forceContentIds = { [`${store.getters['file/current'].id}/content`]: true };
|
|
|
|
store.getters['file/items'].forEach((file) => {
|
|
|
|
const contentId = `${file.id}/content`;
|
|
|
|
const updated = getContentUpdated(contentId);
|
|
|
|
const existingSyncData = store.getters['data/syncDataByItemId'][contentId];
|
|
|
|
});
|
|
|
|
|
|
|
|
const syncOneContent = fileId => localDbSvc.retrieveItem(`${fileId}/syncContent`)
|
|
|
|
.catch(() => ({ ...emptySyncContent(), id: `${fileId}/syncContent` }))
|
|
|
|
.then(syncContent => localDbSvc.retrieveItem(`${fileId}/content`)
|
|
|
|
.catch(() => ({ ...emptyContent(), id: `${fileId}/content` }))
|
|
|
|
.then((content) => {
|
|
|
|
const syncOneContentLocation = (syncLocation) => {
|
|
|
|
return Promise.resolve()
|
|
|
|
.then(() => {
|
|
|
|
const provider = getSyncProvider(syncLocation);
|
|
|
|
const token = getSyncToken(syncLocation);
|
|
|
|
return provider && token && provider.downloadContent()
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const syncLocations = [{ provider: null }, ...content.syncLocations];
|
|
|
|
return syncOneContentLocation(syncLocations[0]);
|
|
|
|
}));
|
|
|
|
|
|
|
|
// Called until no content to save
|
|
|
|
const saveNextContent = ifNotTooLate(() => {
|
|
|
|
let saveContentPromise;
|
|
|
|
const getSaveContentPromise = (contentId) => {
|
|
|
|
const updated = getContentUpdated(contentId);
|
|
|
|
const existingSyncData = store.getters['data/syncDataByItemId'][contentId];
|
|
|
|
if (!existingSyncData || existingSyncData.updated !== updated) {
|
|
|
|
saveContentPromise = localDbSvc.retrieveItem(contentId)
|
|
|
|
.then(content => googleHelper.saveItem(
|
|
|
|
googleToken,
|
|
|
|
// Use deepCopy to freeze objects
|
|
|
|
utils.deepCopy(content),
|
|
|
|
utils.deepCopy(existingSyncData),
|
|
|
|
ifNotTooLate,
|
|
|
|
))
|
|
|
|
.then(resultSyncData => store.dispatch('data/patchSyncData', {
|
|
|
|
[resultSyncData.id]: resultSyncData,
|
|
|
|
}))
|
|
|
|
.then(() => saveNextContent());
|
|
|
|
}
|
|
|
|
return saveContentPromise;
|
|
|
|
};
|
|
|
|
Object.keys(localDbSvc.updatedMap.content)
|
|
|
|
.some(id => getSaveContentPromise(id, syncDataByItemId));
|
|
|
|
return saveContentPromise;
|
|
|
|
});
|
|
|
|
|
|
|
|
return Promise.resolve()
|
|
|
|
.then(() => saveNextItem())
|
|
|
|
.then(() => removeNextItem())
|
|
|
|
.catch((err) => {
|
|
|
|
if (err && err.message === 'too_late') {
|
|
|
|
// Restart sync
|
|
|
|
return sync();
|
|
|
|
}
|
|
|
|
throw err;
|
|
|
|
});
|
2017-08-15 10:43:26 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function requestSync() {
|
|
|
|
store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => {
|
|
|
|
let intervalId;
|
|
|
|
const attempt = () => {
|
|
|
|
// Only start syncing when these conditions are met
|
|
|
|
if (userActivitySvc.isActive() && isSyncWindow()) {
|
|
|
|
clearInterval(intervalId);
|
|
|
|
if (!isSyncAvailable()) {
|
|
|
|
// Cancel sync
|
|
|
|
reject();
|
|
|
|
} else {
|
|
|
|
// Call setLastSyncActivity periodically
|
|
|
|
intervalId = utils.setInterval(() => setLastSyncActivity(), 1000);
|
|
|
|
setLastSyncActivity();
|
|
|
|
const cleaner = cb => (res) => {
|
|
|
|
clearInterval(intervalId);
|
|
|
|
cb(res);
|
|
|
|
};
|
|
|
|
sync().then(cleaner(resolve), cleaner(reject));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
intervalId = utils.setInterval(() => attempt(), 1000);
|
|
|
|
attempt();
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sync periodically
|
|
|
|
utils.setInterval(() => {
|
|
|
|
if (isSyncAvailable() &&
|
|
|
|
userActivitySvc.isActive() &&
|
|
|
|
isSyncWindow() &&
|
2017-08-17 23:10:35 +00:00
|
|
|
isAutoSyncReady()
|
2017-08-15 10:43:26 +00:00
|
|
|
) {
|
|
|
|
requestSync();
|
|
|
|
}
|
|
|
|
}, 1000);
|
2017-07-28 07:40:24 +00:00
|
|
|
|
|
|
|
const ifNoId = cb => (obj) => {
|
|
|
|
if (obj.id) {
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
return cb();
|
|
|
|
};
|
|
|
|
|
2017-07-31 09:04:01 +00:00
|
|
|
// Load the DB on boot
|
|
|
|
localDbSvc.sync()
|
2017-08-15 10:43:26 +00:00
|
|
|
// And watch file changing
|
2017-07-31 09:04:01 +00:00
|
|
|
.then(() => store.watch(
|
2017-08-17 23:10:35 +00:00
|
|
|
() => store.getters['file/current'].id,
|
|
|
|
() => Promise.resolve(store.getters['file/current'])
|
|
|
|
// If current file has no ID, get the most recent file
|
|
|
|
.then(ifNoId(() => store.getters['file/lastOpened']))
|
|
|
|
// If still no ID, create a new file
|
|
|
|
.then(ifNoId(() => {
|
|
|
|
const id = utils.uid();
|
|
|
|
store.commit('content/setItem', {
|
|
|
|
id: `${id}/content`,
|
|
|
|
text: welcomeFile,
|
|
|
|
});
|
|
|
|
store.commit('file/setItem', {
|
|
|
|
id,
|
|
|
|
name: 'Welcome file',
|
|
|
|
});
|
|
|
|
return store.state.file.itemMap[id];
|
|
|
|
}))
|
|
|
|
.then((currentFile) => {
|
|
|
|
// Fix current file ID
|
|
|
|
if (store.getters['file/current'].id !== currentFile.id) {
|
|
|
|
store.commit('file/setCurrentId', currentFile.id);
|
|
|
|
// Wait for the next watch tick
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// Set last opened
|
|
|
|
store.dispatch('data/setLastOpenedId', currentFile.id);
|
|
|
|
return Promise.resolve()
|
|
|
|
// Load contentState from DB
|
|
|
|
.then(() => localDbSvc.retrieveItem(`${currentFile.id}/contentState`)
|
|
|
|
// contentState does not exist, create it
|
|
|
|
.catch(() => store.commit('contentState/setItem', {
|
|
|
|
id: `${currentFile.id}/contentState`,
|
|
|
|
})))
|
|
|
|
// Load syncContent from DB
|
|
|
|
.then(() => localDbSvc.retrieveItem(`${currentFile.id}/syncContent`)
|
|
|
|
// syncContent does not exist, create it
|
|
|
|
.catch(() => store.commit('syncContent/setItem', {
|
|
|
|
id: `${currentFile.id}/syncContent`,
|
|
|
|
})))
|
|
|
|
// Load content from DB
|
|
|
|
.then(() => localDbSvc.retrieveItem(`${currentFile.id}/content`));
|
|
|
|
}),
|
|
|
|
{
|
2017-07-31 09:04:01 +00:00
|
|
|
immediate: true,
|
|
|
|
}));
|
2017-07-28 07:40:24 +00:00
|
|
|
|
2017-08-15 10:43:26 +00:00
|
|
|
// Sync local DB periodically
|
|
|
|
utils.setInterval(() => localDbSvc.sync(), 1000);
|
|
|
|
|
|
|
|
export default {
|
|
|
|
isSyncAvailable,
|
|
|
|
requestSync,
|
|
|
|
};
|