Stackedit/src/services/syncSvc.js

327 lines
11 KiB
JavaScript
Raw Normal View History

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,
};