Browse Source

fix: throttle value update in the background

close #219
Gerald 8 years ago
parent
commit
62d804ecc0

+ 10 - 19
src/background/app.js

@@ -8,15 +8,16 @@ import {
   newScript, parseMeta,
   setClipboard, checkUpdate,
   getOption, setOption, hookOptions, getAllOptions,
-  initialize,
+  initialize, broadcast,
 } from './utils';
 import {
   getScripts, removeScript, getData, checkRemove, getScriptsByURL,
-  updateScriptInfo, setValues, getExportData, getScriptCode,
+  updateScriptInfo, getExportData, getScriptCode,
   getScriptByIds, moveScript, vacuum, parseScript, getScript,
   normalizePosition,
 } from './utils/db';
 import { resetBlacklist } from './utils/tester';
+import { setValueStore, updateValueStore } from './utils/values';
 
 const VM_VER = browser.runtime.getManifest().version;
 
@@ -28,15 +29,6 @@ hookOptions(changes => {
   });
 });
 
-function broadcast(data) {
-  browser.tabs.query({})
-  .then(tabs => {
-    tabs.forEach(tab => {
-      browser.tabs.sendMessage(tab.id, data);
-    });
-  });
-}
-
 function checkUpdateAll() {
   setOption('lastUpdate', Date.now());
   getScripts()
@@ -116,14 +108,13 @@ const commands = {
       });
     });
   },
-  SetValue({ where, values }) {
-    return setValues(where, values)
-    .then(data => {
-      broadcast({
-        cmd: 'UpdateValues',
-        data,
-      });
-    });
+  SetValueStore({ where, valueStore }) {
+    // Value store will be replaced soon.
+    return setValueStore(where, valueStore);
+  },
+  UpdateValue({ id, update }) {
+    // Value will be updated to store later.
+    return updateValueStore(id, update);
   },
   ExportZip({ ids, values }) {
     return getExportData(ids, values);

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

@@ -50,7 +50,7 @@ const storage = {
         return result;
       });
     },
-    dump(id, value) {
+    set(id, value) {
       if (!id) return Promise.resolve();
       return browser.storage.local.set({
         [this.getKey(id)]: value,
@@ -82,11 +82,20 @@ storage.code = Object.assign({}, storage.base, {
 });
 storage.value = Object.assign({}, storage.base, {
   prefix: 'val:',
+  dump(dict) {
+    const updates = {};
+    Object.keys(dict)
+    .forEach(id => {
+      const value = dict[id];
+      updates[this.getKey(id)] = value;
+    });
+    return browser.storage.local.set(updates);
+  },
 });
 storage.require = Object.assign({}, storage.base, {
   prefix: 'req:',
   fetch: cacheOrFetch(function fetch(uri) {
-    return request(uri).then(({ data }) => this.dump(uri, data));
+    return request(uri).then(({ data }) => this.set(uri, data));
   }),
 });
 storage.cache = Object.assign({}, storage.base, {
@@ -101,7 +110,7 @@ storage.cache = Object.assign({}, storage.base, {
         base64: () => window.btoa(data.string()),
       };
       return (check ? Promise.resolve(check(data)) : Promise.resolve())
-      .then(() => this.dump(uri, data.base64()));
+      .then(() => this.set(uri, data.base64()));
     });
   }),
 });
@@ -210,12 +219,31 @@ export function getScriptCode(id) {
   return storage.code.getOne(id);
 }
 
-export function setValues(where, values) {
+/**
+ * @desc Load values for batch updates.
+ * @param {Array} ids
+ */
+export function getValueStoresByIds(ids) {
+  return storage.value.getMulti(ids);
+}
+
+/**
+ * @desc Dump values for batch updates.
+ * @param {Object} valueDict { id1: value1, id2: value2, ... }
+ */
+export function dumpValueStores(valueDict) {
+  if (process.env.DEBUG) {
+    console.info('Update value stores', valueDict);
+  }
+  return storage.value.dump(valueDict).then(() => valueDict);
+}
+
+export function dumpValueStore(where, valueStore) {
   return (where.id
     ? Promise.resolve(where.id)
     : getScript(where).then(script => objectGet(script, 'props.id')))
   .then(id => {
-    if (id) return storage.value.dump(id, values).then(() => ({ id, values }));
+    if (id) return dumpValueStores({ [id]: valueStore });
   });
 }
 
@@ -369,7 +397,7 @@ function saveScript(script, code) {
   }
   return Promise.all([
     storage.script.dump(script),
-    storage.code.dump(props.id, code),
+    storage.code.set(props.id, code),
   ]);
 }
 
@@ -477,7 +505,7 @@ function fetchScriptResources(script, cache) {
     const fullUrl = pathMap[key] || key;
     const cached = objectGet(cache, ['require', fullUrl]);
     if (cached) {
-      storage.require.dump(fullUrl, cached);
+      storage.require.set(fullUrl, cached);
     } else {
       storage.require.fetch(fullUrl);
     }
@@ -487,7 +515,7 @@ function fetchScriptResources(script, cache) {
     const fullUrl = pathMap[url] || url;
     const cached = objectGet(cache, ['resources', fullUrl]);
     if (cached) {
-      storage.cache.dump(fullUrl, cached);
+      storage.cache.set(fullUrl, cached);
     } else {
       storage.cache.fetch(fullUrl);
     }

+ 9 - 0
src/background/utils/index.js

@@ -19,3 +19,12 @@ export function notify(options) {
     isClickable: options.isClickable,
   });
 }
+
+export function broadcast(data) {
+  browser.tabs.query({})
+  .then(tabs => {
+    tabs.forEach(tab => {
+      browser.tabs.sendMessage(tab.id, data);
+    });
+  });
+}

+ 66 - 0
src/background/utils/values.js

@@ -0,0 +1,66 @@
+import { broadcast } from '.';
+import { getValueStoresByIds, dumpValueStores, dumpValueStore } from './db';
+
+let cache;
+let timer;
+let updating;
+
+export function updateValueStore(id, update) {
+  updateLater();
+  const { key, value } = update;
+  if (!cache) cache = {};
+  let updates = cache[id];
+  if (!updates) {
+    updates = {};
+    cache[id] = updates;
+  }
+  updates[key] = value || null;
+}
+
+export function setValueStore(where, value) {
+  return dumpValueStore(where, value)
+  .then(broadcastUpdates);
+}
+
+function updateLater() {
+  if (!updating && !timer) {
+    timer = Promise.resolve().then(doUpdate);
+    // timer = setTimeout(doUpdate);
+  }
+}
+
+function doUpdate() {
+  const currentCache = cache;
+  cache = null;
+  timer = null;
+  const ids = Object.keys(currentCache);
+  updating = true;
+  getValueStoresByIds(ids)
+  .then(valueStores => {
+    ids.forEach(id => {
+      const valueStore = valueStores[id] || {};
+      valueStores[id] = valueStore;
+      const updates = currentCache[id] || {};
+      Object.keys(updates).forEach(key => {
+        const value = updates[key];
+        if (!value) delete valueStore[key];
+        else valueStore[key] = value;
+      });
+    });
+    return dumpValueStores(valueStores);
+  })
+  .then(broadcastUpdates)
+  .then(() => {
+    updating = false;
+    if (cache) updateLater();
+  });
+}
+
+function broadcastUpdates(updates) {
+  if (updates) {
+    broadcast({
+      cmd: 'UpdatedValues',
+      data: updates,
+    });
+  }
+}

+ 4 - 4
src/injected/content/index.js

@@ -37,8 +37,8 @@ const bgHandlers = {
   GetBadge: getBadge,
   HttpRequested: httpRequested,
   TabClosed: tabClosed,
-  UpdateValues(data) {
-    bridge.post({ cmd: 'UpdateValues', data });
+  UpdatedValues(data) {
+    bridge.post({ cmd: 'UpdatedValues', data });
   },
   NotificationClick: onNotificationClick,
   NotificationClose: onNotificationClose,
@@ -75,8 +75,8 @@ const handlers = {
   Inject: injectScript,
   TabOpen: tabOpen,
   TabClose: tabClose,
-  SetValue(data) {
-    sendMessage({ cmd: 'SetValue', data });
+  UpdateValue(data) {
+    sendMessage({ cmd: 'UpdateValue', data });
   },
   RegisterMenu(data) {
     if (IS_TOP) menus.push(data);

+ 13 - 11
src/injected/web/index.js

@@ -1,4 +1,4 @@
-import { getUniqId, bindEvents, Promise, attachFunction, console, throttle } from '../utils';
+import { getUniqId, bindEvents, Promise, attachFunction, console } from '../utils';
 import { includes, forEach, map, utf8decode } from './helpers';
 import bridge from './bridge';
 import { onRequestCreate, onRequestStart, onRequestCallback } from './requests';
@@ -35,8 +35,11 @@ const handlers = {
   GotRequestId: onRequestStart,
   HttpRequested: onRequestCallback,
   TabClosed: onTabClosed,
-  UpdateValues({ id, values }) {
-    if (store.values[id]) store.values[id] = values;
+  UpdatedValues(updates) {
+    Object.keys(updates)
+    .forEach(id => {
+      if (store.values[id]) store.values[id] = updates[id];
+    });
   },
   NotificationClicked: onNotificationClicked,
   NotificationClosed: onNotificationClosed,
@@ -154,7 +157,6 @@ function wrapGM(script, code, cache) {
     '': val => val,
   };
   const pathMap = script.custom.pathMap || {};
-  const throttledDumpValues = throttle(dumpValues, 200);
   const matches = code.match(/\/\/\s+==UserScript==\s+([\s\S]*?)\/\/\s+==\/UserScript==\s/);
   const metaStr = matches ? matches[1] : '';
   const gmFunctions = {
@@ -189,7 +191,7 @@ function wrapGM(script, code, cache) {
       value(key) {
         const value = loadValues();
         delete value[key];
-        throttledDumpValues();
+        dumpValue(key);
       },
     },
     GM_getValue: {
@@ -203,7 +205,7 @@ function wrapGM(script, code, cache) {
           try {
             val = handle(val);
           } catch (e) {
-            console.warn(e);
+            if (process.env.DEBUG) console.warn(e);
           }
           return val;
         }
@@ -222,7 +224,7 @@ function wrapGM(script, code, cache) {
         const raw = type + handle(val);
         const value = loadValues();
         value[key] = raw;
-        throttledDumpValues();
+        dumpValue(key, raw);
       },
     },
     GM_getResourceText: {
@@ -328,12 +330,12 @@ function wrapGM(script, code, cache) {
     Object.defineProperty(obj, name, prop);
     if (typeof obj[name] === 'function') obj[name].toString = propertyToString;
   }
-  function dumpValues() {
+  function dumpValue(key, value) {
     bridge.post({
-      cmd: 'SetValue',
+      cmd: 'UpdateValue',
       data: {
-        where: { id: script.props.id },
-        values: loadValues(),
+        id: script.props.id,
+        update: { key, value },
       },
     });
   }

+ 4 - 4
src/options/views/tab-settings/vm-import.vue

@@ -136,13 +136,13 @@ function importData(file) {
     return Promise.all(entries.map(entry => getVMFile(entry, vm)))
     .then(res => res.filter(Boolean).length)
     .then(count => {
-      forEachItem(vm.values, (value, key) => {
-        if (value) {
+      forEachItem(vm.values, (valueStore, key) => {
+        if (valueStore) {
           sendMessage({
-            cmd: 'SetValue',
+            cmd: 'SetValueStore',
             data: {
               where: { uri: key },
-              values: value,
+              valueStore,
             },
           });
         }