Browse Source

fix: sync positions

Gerald 8 years ago
parent
commit
3111ad849b

+ 7 - 5
src/background/app.js

@@ -14,7 +14,7 @@ import {
   getScripts, removeScript, getData, checkRemove, getScriptsByURL,
   updateScriptInfo, getExportData, getScriptCode,
   getScriptByIds, moveScript, vacuum, parseScript, getScript,
-  normalizePosition,
+  sortScripts,
 } from './utils/db';
 import { resetBlacklist } from './utils/tester';
 import { setValueStore, updateValueStore } from './utils/values';
@@ -93,8 +93,8 @@ const commands = {
   UpdateScriptInfo({ id, config }) {
     return updateScriptInfo(id, {
       config,
-      custom: {
-        modified: Date.now(),
+      props: {
+        lastModified: Date.now(),
       },
     })
     .then(([script]) => {
@@ -127,7 +127,9 @@ const commands = {
   },
   Move({ id, offset }) {
     return moveScript(id, offset)
-    .then(() => { sync.sync(); });
+    .then(() => {
+      sync.sync();
+    });
   },
   Vacuum: vacuum,
   ParseScript(data) {
@@ -208,7 +210,7 @@ const commands = {
     .then(script => (script ? script.meta.version : null));
   },
   CheckPosition() {
-    return normalizePosition();
+    return sortScripts();
   },
 };
 

+ 45 - 20
src/background/sync/base.js

@@ -1,7 +1,14 @@
 import { debounce, normalizeKeys, request, noop } from 'src/common';
 import { objectPurify } from 'src/common/object';
 import { getEventEmitter, getOption, setOption, hookOptions } from '../utils';
-import { getScripts, getScriptCode, parseScript, removeScript, normalizePosition } from '../utils/db';
+import {
+  getScripts,
+  getScriptCode,
+  parseScript,
+  removeScript,
+  sortScripts,
+  updateScriptInfo,
+} from '../utils/db';
 
 const serviceNames = [];
 const serviceClasses = [];
@@ -221,10 +228,9 @@ export const BaseService = serviceFactory({
     throw err;
   },
   getMeta() {
-    return this.get(this.metaFile)
+    return this.get({ name: this.metaFile })
     .then(data => JSON.parse(data))
-    .catch(err => this.handleMetaError(err))
-    .then(data => ({ name: this.metaFile, data }));
+    .catch(err => this.handleMetaError(err));
   },
   initToken() {
     this.prepareHeaders();
@@ -281,8 +287,8 @@ export const BaseService = serviceFactory({
   },
   getSyncData() {
     return this.getMeta()
-    .then(remoteMeta => Promise.all([
-      remoteMeta,
+    .then(remoteMetaData => Promise.all([
+      { name: this.metaFile, data: remoteMetaData },
       this.list(),
       this.getLocalData(),
     ]));
@@ -294,7 +300,8 @@ export const BaseService = serviceFactory({
     };
     this.syncState.set('syncing');
     // Avoid simultaneous requests
-    return this.getSyncData()
+    return this.prepare()
+    .then(() => this.getSyncData())
     .then(([remoteMeta, remoteData, localData]) => {
       const { data: remoteMetaData } = remoteMeta;
       const remoteMetaInfo = remoteMetaData.info || {};
@@ -302,16 +309,18 @@ export const BaseService = serviceFactory({
       let remoteChanged = !remoteTimestamp
         || Object.keys(remoteMetaInfo).length !== remoteData.length;
       const now = Date.now();
+      const globalLastModified = getOption('lastModified');
       const remoteItemMap = {};
       const localMeta = this.config.get('meta', {});
       const firstSync = !localMeta.timestamp;
       const outdated = firstSync || remoteTimestamp > localMeta.timestamp;
       this.log('First sync:', firstSync);
       this.log('Outdated:', outdated, '(', 'local:', localMeta.timestamp, 'remote:', remoteTimestamp, ')');
-      const getRemote = [];
+      const putLocal = [];
       const putRemote = [];
       const delRemote = [];
       const delLocal = [];
+      const updateLocal = [];
       remoteMetaData.info = remoteData.reduce((info, item) => {
         remoteItemMap[item.uri] = item;
         let itemInfo = remoteMetaInfo[item.uri];
@@ -332,12 +341,21 @@ export const BaseService = serviceFactory({
         if (remoteInfo) {
           const remoteItem = remoteItemMap[uri];
           if (firstSync || !lastModified || remoteInfo.modified > lastModified) {
-            getRemote.push({ local: item, remote: remoteItem });
-          } else if (remoteInfo.modified < lastModified) {
-            putRemote.push({ local: item, remote: remoteItem });
-          } else if (remoteInfo.position !== position) {
-            remoteInfo.position = position;
-            remoteChanged = true;
+            putLocal.push({ local: item, remote: remoteItem, info: remoteInfo });
+          } else {
+            if (remoteInfo.modified < lastModified) {
+              putRemote.push({ local: item, remote: remoteItem });
+              remoteInfo.modified = lastModified;
+              remoteChanged = true;
+            }
+            if (remoteInfo.position !== position) {
+              if (remoteInfo.position && globalLastModified <= remoteTimestamp) {
+                updateLocal.push({ local: item, remote: remoteItem, info: remoteInfo });
+              } else {
+                remoteInfo.position = position;
+                remoteChanged = true;
+              }
+            }
           }
           delete remoteItemMap[uri];
         } else if (firstSync || !outdated || lastModified > remoteTimestamp) {
@@ -348,14 +366,15 @@ export const BaseService = serviceFactory({
       });
       Object.keys(remoteItemMap).forEach(uri => {
         const item = remoteItemMap[uri];
+        const info = remoteMetaData.info[uri];
         if (outdated) {
-          getRemote.push({ remote: item });
+          putLocal.push({ remote: item, info });
         } else {
           delRemote.push({ remote: item });
         }
       });
       const promiseQueue = [
-        ...getRemote.map(({ remote }) => {
+        ...putLocal.map(({ remote, info }) => {
           this.log('Download script:', remote.uri);
           return this.get(remote)
           .then(raw => {
@@ -380,10 +399,9 @@ export const BaseService = serviceFactory({
             }
             // Invalid data
             if (!data.code) return;
-            const remoteInfo = remoteMetaData.info[remote.uri];
-            const { modified } = remoteInfo;
+            const { modified } = info;
             data.modified = modified;
-            const position = +remoteInfo.position;
+            const position = +info.position;
             if (position) data.position = position;
             if (!getOption('syncScriptStatus') && data.config) {
               delete data.config.enabled;
@@ -433,8 +451,15 @@ export const BaseService = serviceFactory({
           this.log('Remove local script:', local.props.uri);
           return removeScript(local.props.id);
         }),
+        ...updateLocal.map(({ local, info }) => {
+          const updates = {};
+          if (info.position) {
+            updates.props = { position: info.position };
+          }
+          return updateScriptInfo(local.props.id, updates);
+        }),
       ];
-      promiseQueue.push(Promise.all(promiseQueue).then(() => normalizePosition()).then(changed => {
+      promiseQueue.push(Promise.all(promiseQueue).then(() => sortScripts()).then(changed => {
         if (!changed) return;
         remoteChanged = true;
         return getScripts().then(scripts => {

+ 7 - 7
src/background/sync/dropbox.js

@@ -1,5 +1,5 @@
 import { loadQuery, dumpQuery } from '../utils';
-import { getURI, getFilename, BaseService, isScriptFile, register } from './base';
+import { getURI, getItemFilename, BaseService, isScriptFile, register } from './base';
 
 const config = {
   client_id: 'f0q12zup2uys5w8',
@@ -43,8 +43,8 @@ const Dropbox = BaseService.extend({
       data.entries.filter(item => item['.tag'] === 'file' && isScriptFile(item.name)).map(normalize)
     ));
   },
-  get({ uri }) {
-    const name = getFilename(uri);
+  get(item) {
+    const name = getItemFilename(item);
     return this.loadData({
       method: 'POST',
       url: 'https://content.dropboxapi.com/2/files/download',
@@ -55,8 +55,8 @@ const Dropbox = BaseService.extend({
       },
     });
   },
-  put({ uri }, data) {
-    const name = getFilename(uri);
+  put(item, data) {
+    const name = getItemFilename(item);
     return this.loadData({
       method: 'POST',
       url: 'https://content.dropboxapi.com/2/files/upload',
@@ -72,8 +72,8 @@ const Dropbox = BaseService.extend({
     })
     .then(normalize);
   },
-  remove({ uri }) {
-    const name = getFilename(uri);
+  remove(item) {
+    const name = getItemFilename(item);
     return this.loadData({
       method: 'POST',
       url: 'https://api.dropboxapi.com/2/files/delete',

+ 4 - 3
src/background/sync/googledrive.js

@@ -4,7 +4,7 @@
 import { getUniqId } from 'src/common';
 import { objectGet } from 'src/common/object';
 import { loadQuery, dumpQuery } from '../utils';
-import { getURI, getFilename, BaseService, register, isScriptFile } from './base';
+import { getURI, getItemFilename, BaseService, register, isScriptFile } from './base';
 
 const config = {
   client_id: '590447512361-05hjbhnf8ua3iha55e5pgqg15om0cpef.apps.googleusercontent.com',
@@ -118,8 +118,9 @@ const GoogleDrive = BaseService.extend({
       url: `/files/${id}?alt=media`,
     });
   },
-  put({ id, name: filename, uri }, data) {
-    const name = uri ? getFilename(uri) : filename;
+  put(item, data) {
+    const name = getItemFilename(item);
+    const { id } = item;
     const boundary = getUniqId('violentmonkey-is-great-');
     const headers = {
       'Content-Type': `multipart/related; boundary=${boundary}`,

+ 8 - 8
src/background/sync/onedrive.js

@@ -2,7 +2,7 @@
 import { noop } from 'src/common';
 import { objectGet } from 'src/common/object';
 import { dumpQuery } from '../utils';
-import { getURI, getFilename, BaseService, isScriptFile, register } from './base';
+import { getURI, getItemFilename, BaseService, isScriptFile, register } from './base';
 
 const SECRET_KEY = JSON.parse(window.atob('eyJjbGllbnRfc2VjcmV0Ijoiajl4M09WRXRIdmhpSEtEV09HcXV5TWZaS2s5NjA0MEgifQ=='));
 const config = Object.assign({
@@ -50,7 +50,7 @@ const OneDrive = BaseService.extend({
     if (res.status === 404) {
       const header = res.xhr.getResponseHeader('WWW-Authenticate') || '';
       if (/^Bearer realm="OneDriveAPI"/.test(header)) {
-        return this.refreshToken().then(this.getMeta);
+        return this.refreshToken().then(() => this.getMeta());
       }
       return {};
     }
@@ -63,8 +63,8 @@ const OneDrive = BaseService.extend({
     })
     .then(data => data.value.filter(item => item.file && isScriptFile(item.name)).map(normalize));
   },
-  get({ uri }) {
-    const name = getFilename(uri);
+  get(item) {
+    const name = getItemFilename(item);
     return this.loadData({
       url: `/drive/special/approot:/${encodeURIComponent(name)}`,
       responseType: 'json',
@@ -74,8 +74,8 @@ const OneDrive = BaseService.extend({
       delay: false,
     }));
   },
-  put({ uri }, data) {
-    const name = getFilename(uri);
+  put(item, data) {
+    const name = getItemFilename(item);
     return this.loadData({
       method: 'PUT',
       url: `/drive/special/approot:/${encodeURIComponent(name)}:/content`,
@@ -87,9 +87,9 @@ const OneDrive = BaseService.extend({
     })
     .then(normalize);
   },
-  remove({ uri }) {
+  remove(item) {
     // return 204
-    const name = getFilename(uri);
+    const name = getItemFilename(item);
     return this.loadData({
       method: 'DELETE',
       url: `/drive/special/approot:/${encodeURIComponent(name)}`,

+ 36 - 11
src/background/utils/db.js

@@ -4,6 +4,7 @@ import { getNameURI, parseMeta, newScript } from './script';
 import { testScript, testBlacklist } from './tester';
 import { register } from './init';
 import patchDB from './patch-db';
+import { setOption } from './options';
 
 function cacheOrFetch(handle) {
   const requests = {};
@@ -147,10 +148,6 @@ function initialize() {
         storeInfo.position = Math.max(storeInfo.position, getInt(objectGet(value, 'props.position')));
       }
     });
-    scripts.sort((a, b) => {
-      const [pos1, pos2] = [a, b].map(item => getInt(objectGet(item, 'props.position')));
-      return Math.sign(pos1 - pos2);
-    });
     Object.assign(store, {
       scripts,
       storeInfo,
@@ -162,7 +159,7 @@ function initialize() {
     if (process.env.DEBUG) {
       console.log('store:', store); // eslint-disable-line no-console
     }
-    return normalizePosition();
+    return sortScripts();
   });
 }
 
@@ -170,12 +167,17 @@ function getInt(val) {
   return +val || 0;
 }
 
+function updateLastModified() {
+  setOption('lastModified', Date.now());
+}
+
 export function normalizePosition() {
   const updates = [];
+  const positionKey = 'props.position';
   store.scripts.forEach((item, index) => {
     const position = index + 1;
-    if (objectGet(item, 'props.position') !== position) {
-      objectSet(item, 'props.position', position);
+    if (objectGet(item, positionKey) !== position) {
+      objectSet(item, positionKey, position);
       updates.push(item);
     }
     // XXX patch v2.8.0
@@ -191,7 +193,24 @@ export function normalizePosition() {
   });
   store.storeInfo.position = store.scripts.length;
   const { length } = updates;
-  return length ? storage.script.dump(updates).then(() => length) : Promise.resolve();
+  if (!length) return Promise.resolve();
+  return storage.script.dump(updates)
+  .then(() => {
+    updateLastModified();
+    return length;
+  });
+}
+
+export function sortScripts() {
+  store.scripts.sort((a, b) => {
+    const [pos1, pos2] = [a, b].map(item => getInt(objectGet(item, 'props.position')));
+    return Math.sign(pos1 - pos2);
+  });
+  return normalizePosition()
+  .then(changed => {
+    browser.runtime.sendMessage({ cmd: 'ScriptsUpdated' });
+    return changed;
+  });
 }
 
 export function getScript(where) {
@@ -340,6 +359,7 @@ export function removeScript(id) {
     cmd: 'RemoveScript',
     data: id,
   });
+  return Promise.resolve();
 }
 
 export function moveScript(id, offset) {
@@ -389,8 +409,12 @@ function saveScript(script, code) {
     const index = store.scripts.indexOf(oldScript);
     store.scripts[index] = script;
   } else {
-    store.storeInfo.position += 1;
-    props.position = store.storeInfo.position;
+    if (!props.position) {
+      store.storeInfo.position += 1;
+      props.position = store.storeInfo.position;
+    } else if (store.storeInfo.position < props.position) {
+      store.storeInfo.position = props.position;
+    }
     script.config = config;
     script.props = props;
     store.scripts.push(script);
@@ -404,8 +428,9 @@ function saveScript(script, code) {
 export function updateScriptInfo(id, data) {
   const script = store.scriptMap[id];
   if (!script) return Promise.reject();
+  script.props = Object.assign({}, script.props, data.props);
   script.config = Object.assign({}, script.config, data.config);
-  script.custom = Object.assign({}, script.custom, data.custom);
+  // script.custom = Object.assign({}, script.custom, data.custom);
   return storage.script.dump(script);
 }
 

+ 11 - 0
src/background/utils/options.js

@@ -7,6 +7,7 @@ const defaults = {
   autoUpdate: true,
   // ignoreGrant: false,
   lastUpdate: 0,
+  lastModified: 0,
   showBadge: true,
   exportValues: true,
   closeAfterInstall: false,
@@ -29,6 +30,7 @@ const hooks = initHooks();
 const callHooksLater = debounce(callHooks, 100);
 
 let options = {};
+let ready = false;
 const init = browser.storage.local.get('options')
 .then(({ options: data }) => {
   if (data && typeof data === 'object') options = data;
@@ -66,6 +68,9 @@ const init = browser.storage.local.get('options')
     }
     setOption('version', 1);
   }
+})
+.then(() => {
+  ready = true;
 });
 register(init);
 
@@ -89,6 +94,12 @@ export function getOption(key, def) {
 }
 
 export function setOption(key, value) {
+  if (!ready) {
+    init.then(() => {
+      setOption(key, value);
+    });
+    return;
+  }
   const keys = normalizeKeys(key);
   const optionKey = keys.join('.');
   let optionValue = value;