瀏覽代碼

fix: value change notifications + broadcast only changes

tophf 6 年之前
父節點
當前提交
f0d4dc68aa
共有 4 個文件被更改,包括 98 次插入111 次删除
  1. 61 61
      src/background/utils/values.js
  2. 3 6
      src/injected/web/gm-api.js
  3. 32 37
      src/injected/web/gm-values.js
  4. 2 7
      src/options/views/edit/values.vue

+ 61 - 61
src/background/utils/values.js

@@ -1,12 +1,10 @@
 import { isEmpty, sendTabCmd } from '#/common';
 import { isEmpty, sendTabCmd } from '#/common';
-import {
-  forEachEntry, forEachKey, objectPick, objectSet,
-} from '#/common/object';
+import { forEachEntry, forEachKey, objectSet } from '#/common/object';
 import { getScript, getValueStoresByIds, dumpValueStores } from './db';
 import { getScript, getValueStoresByIds, dumpValueStores } from './db';
 import { commands } from './message';
 import { commands } from './message';
 
 
 const openers = {}; // { scriptId: { tabId: { frameId: 1, ... }, ... } }
 const openers = {}; // { scriptId: { tabId: { frameId: 1, ... }, ... } }
-let cache; // { scriptId: { key: [{ value, src }, ... ], ... } }
+let cache = {}; // { scriptId: { key: { last: value, tabId: { frameId: value } } } }
 let updateScheduled;
 let updateScheduled;
 
 
 Object.assign(commands, {
 Object.assign(commands, {
@@ -24,15 +22,16 @@ Object.assign(commands, {
       if (id) res[id] = store;
       if (id) res[id] = store;
       return res;
       return res;
     }, {});
     }, {});
-    await dumpValueStores(stores);
-    broadcastUpdates(stores);
+    await Promise.all([
+      dumpValueStores(stores),
+      broadcastValueStores(groupStoresByFrame(stores)),
+    ]);
   },
   },
-  /** @return {Promise<void>} */
-  UpdateValue({ id, update: { key, value = null } }, src) {
-    // Value will be updated to store later.
-    updateLater();
-    cache = objectSet(cache, [id, key, 'last'], value);
+  /** @return {void} */
+  UpdateValue({ id, key, value = null }, src) {
+    objectSet(cache, [id, key, 'last'], value);
     objectSet(cache, [id, key, src.tab.id, src.frameId], value);
     objectSet(cache, [id, key, src.tab.id, src.frameId], value);
+    updateLater();
   },
   },
 });
 });
 
 
@@ -55,72 +54,73 @@ export function addValueOpener(tabId, frameId, scriptIds) {
 }
 }
 
 
 async function updateLater() {
 async function updateLater() {
-  if (!updateScheduled) {
+  while (!updateScheduled) {
     updateScheduled = true;
     updateScheduled = true;
     await 0;
     await 0;
-    doUpdate();
+    const currentCache = cache;
+    cache = {};
+    await doUpdate(currentCache);
     updateScheduled = false;
     updateScheduled = false;
-    if (cache) updateLater();
+    if (isEmpty(cache)) break;
   }
   }
 }
 }
 
 
-async function doUpdate() {
-  const ids = Object.keys(cache);
-  const currentCache = cache;
-  cache = null;
-  try {
-    const valueStores = await getValueStoresByIds(ids);
-    ids.forEach((id) => {
-      const valueStore = valueStores[id] || (valueStores[id] = {});
-      const updates = currentCache[id] || {};
-      updates::forEachEntry(([key, { last }]) => {
-        if (!last) delete valueStore[key];
-        else valueStore[key] = last;
-      });
+async function doUpdate(currentCache) {
+  const ids = Object.keys(currentCache);
+  const valueStores = await getValueStoresByIds(ids);
+  ids.forEach((id) => {
+    currentCache[id]::forEachEntry(([key, { last }]) => {
+      objectSet(valueStores, [id, key], last || undefined);
     });
     });
-    await dumpValueStores(valueStores);
-    await broadcastUpdates(valueStores, currentCache);
-  } catch (err) {
-    console.error('Values error:', err);
+  });
+  await Promise.all([
+    dumpValueStores(valueStores),
+    broadcastValueStores(groupCacheByFrame(currentCache), { partial: true }),
+  ]);
+}
+
+async function broadcastValueStores(tabFrameData, { partial } = {}) {
+  const tasks = [];
+  for (const [tabId, frames] of Object.entries(tabFrameData)) {
+    for (const [frameId, frameData] of Object.entries(frames)) {
+      if (!isEmpty(frameData)) {
+        if (partial) frameData.partial = true;
+        tasks.push(sendTabCmd(+tabId, 'UpdatedValues', frameData, { frameId: +frameId }));
+        if (tasks.length === 20) await Promise.all(tasks.splice(0)); // throttling
+      }
+    }
   }
   }
+  await Promise.all(tasks);
 }
 }
 
 
-function broadcastUpdates(updates, oldCache = {}) {
-  // group updates by frame
+// Returns per tab/frame data with only the changed values
+function groupCacheByFrame(cacheData) {
   const toSend = {};
   const toSend = {};
-  updates::forEachEntry(([id, data]) => {
+  cacheData::forEachEntry(([id, scriptData]) => {
+    const dataEntries = Object.entries(scriptData);
     openers[id]::forEachEntry(([tabId, frames]) => {
     openers[id]::forEachEntry(([tabId, frames]) => {
-      frames::forEachKey(frameId => {
-        objectSet(toSend, [tabId, frameId, id],
-          avoidInitiator(data, oldCache[id], tabId, frameId));
+      frames::forEachKey((frameId) => {
+        dataEntries.forEach(([key, history]) => {
+          // Skipping this frame if its last recorded value is identical
+          if (history.last !== history[tabId]?.[frameId]) {
+            objectSet(toSend, [tabId, frameId, id, key], history.last);
+          }
+        });
       });
       });
     });
     });
   });
   });
-  // send the grouped updates
-  toSend::forEachEntry(([tabId, frames]) => {
-    frames::forEachEntry(([frameId, frameData]) => {
-      if (!isEmpty(frameData)) {
-        sendTabCmd(+tabId, 'UpdatedValues', frameData, { frameId: +frameId });
-      }
-    });
-  });
+  return toSend;
 }
 }
 
 
-function avoidInitiator(data, history, tabId, frameId) {
-  if (history) {
-    let toPick;
-    data::forEachKey((key, i, allKeys) => {
-      // Not sending `key` to this frame if its last recorded value is identical
-      const frameValue = history[key]?.[tabId]?.[frameId];
-      if (frameValue !== undefined && frameValue === data[key]) {
-        // ...sending the preceding different keys
-        if (!toPick) toPick = allKeys.slice(0, i);
-      } else {
-        // ...sending the subsequent different keys
-        if (toPick) toPick.push(key);
-      }
+// Returns per tab/frame data
+function groupStoresByFrame(stores) {
+  const toSend = {};
+  stores::forEachEntry(([id, store]) => {
+    openers[id]::forEachEntry(([tabId, frames]) => {
+      frames::forEachKey(frameId => {
+        objectSet(toSend, [tabId, frameId, id], store);
+      });
     });
     });
-    if (toPick) data = objectPick(data, toPick);
-  }
-  return !isEmpty(data) ? data : undefined; // undef will remove the key in objectSet
+  });
+  return toSend;
 }
 }

+ 3 - 6
src/injected/web/gm-api.js

@@ -24,9 +24,8 @@ export function makeGmApi() {
       const values = loadValues(id);
       const values = loadValues(id);
       const oldRaw = values[key];
       const oldRaw = values[key];
       delete values[key];
       delete values[key];
-      dumpValue({
-        id, key, oldRaw,
-      });
+      // using `undefined` to match the documentation and TM for GM_addValueChangeListener
+      dumpValue(id, key, undefined, null, oldRaw);
     },
     },
     GM_getValue(key, def) {
     GM_getValue(key, def) {
       const raw = loadValues(this.id)[key];
       const raw = loadValues(this.id)[key];
@@ -42,9 +41,7 @@ export function makeGmApi() {
       const values = loadValues(id);
       const values = loadValues(id);
       const oldRaw = values[key];
       const oldRaw = values[key];
       values[key] = raw;
       values[key] = raw;
-      dumpValue({
-        id, key, val, raw, oldRaw,
-      });
+      dumpValue(id, key, val, raw, oldRaw);
     },
     },
     /**
     /**
      * @callback GMValueChangeListener
      * @callback GMValueChangeListener

+ 32 - 37
src/injected/web/gm-values.js

@@ -1,7 +1,7 @@
 import bridge from './bridge';
 import bridge from './bridge';
 import store from './store';
 import store from './store';
 import {
 import {
-  jsonLoad, forEach, slice, objectKeys, objectValues, objectEntries, log,
+  jsonLoad, forEach, slice, objectValues, objectEntries, log,
 } from '../utils/helpers';
 } from '../utils/helpers';
 
 
 const { Number } = global;
 const { Number } = global;
@@ -18,11 +18,14 @@ const dataDecoders = {
 
 
 bridge.addHandlers({
 bridge.addHandlers({
   UpdatedValues(updates) {
   UpdatedValues(updates) {
-    objectKeys(updates)::forEach((id) => {
+    const { partial } = updates;
+    objectEntries(updates)::forEach(([id, update]) => {
       const oldData = store.values[id];
       const oldData = store.values[id];
       if (oldData) {
       if (oldData) {
-        store.values[id] = updates[id];
-        if (id in changeHooks) changedRemotely(id, oldData, updates);
+        const keyHooks = changeHooks[id];
+        if (keyHooks) changedRemotely(keyHooks, oldData, update);
+        if (partial) applyPartialUpdate(oldData, update);
+        else store.values[id] = update;
       }
       }
     });
     });
   },
   },
@@ -32,21 +35,11 @@ export function loadValues(id) {
   return store.values[id];
   return store.values[id];
 }
 }
 
 
-/** @type {function({ id, key, val, raw, oldRaw })} */
-export function dumpValue(change = {}) {
-  const {
-    id, key, raw, oldRaw,
-  } = change;
-  bridge.post('UpdateValue', {
-    id,
-    update: { key, value: raw },
-  });
+export function dumpValue(id, key, val, raw, oldRaw) {
+  bridge.post('UpdateValue', { id, key, value: raw });
   if (raw !== oldRaw) {
   if (raw !== oldRaw) {
     const hooks = changeHooks[id]?.[key];
     const hooks = changeHooks[id]?.[key];
-    if (hooks) {
-      change.hooks = hooks;
-      notifyChange(change);
-    }
+    if (hooks) notifyChange(hooks, key, val, raw, oldRaw);
   }
   }
 }
 }
 
 
@@ -62,29 +55,31 @@ export function decodeValue(raw) {
   return val;
   return val;
 }
 }
 
 
-// { id, key, val, raw, oldRaw }
-function changedRemotely(id, oldData, updates) {
-  const data = updates[id];
-  id = +id; // the remote id is a string, but all local data structures use a number
-  objectEntries(changeHooks[id])::forEach(([key, hooks]) => {
-    notifyChange({
-      id,
-      key,
-      hooks,
-      oldRaw: oldData[key],
-      raw: data[key],
-      remote: true,
-    });
+function applyPartialUpdate(data, update) {
+  objectEntries(update)::forEach(([key, val]) => {
+    if (val) data[key] = val;
+    else delete data[key];
+  });
+}
+
+function changedRemotely(keyHooks, data, update) {
+  objectEntries(update)::forEach(([key, raw]) => {
+    const hooks = keyHooks[key];
+    if (hooks) {
+      if (!raw) raw = undefined; // partial `update` currently uses null for deleted values
+      const oldRaw = data[key];
+      if (oldRaw !== raw) {
+        data[key] = raw; // will be deleted later in applyPartialUpdate if empty
+        notifyChange(hooks, key, undefined, raw, oldRaw, true);
+      }
+    }
   });
   });
 }
 }
 
 
-// { hooks, key, val, raw, oldRaw, remote }
-function notifyChange(change) {
-  const {
-    hooks, key, val, raw, oldRaw, remote = false,
-  } = change;
-  const oldVal = oldRaw && decodeValue(oldRaw);
-  const newVal = val == null && raw ? decodeValue(raw) : val;
+function notifyChange(hooks, key, val, raw, oldRaw, remote = false) {
+  // converting `null` from messaging to `undefined` to match the documentation and TM
+  const oldVal = (oldRaw || undefined) && decodeValue(oldRaw);
+  const newVal = val === undefined && raw ? decodeValue(raw) : val;
   objectValues(hooks)::forEach(fn => tryCall(fn, key, oldVal, newVal, remote));
   objectValues(hooks)::forEach(fn => tryCall(fn, key, oldVal, newVal, remote));
 }
 }
 
 

+ 2 - 7
src/options/views/edit/values.vue

@@ -124,13 +124,8 @@ export default {
     },
     },
     updateValue({ key, value, isNew }) {
     updateValue({ key, value, isNew }) {
       const rawValue = value ? `o${value}` : '';
       const rawValue = value ? `o${value}` : '';
-      return sendCmd('UpdateValue', {
-        id: this.script.props.id,
-        update: {
-          key,
-          value: rawValue,
-        },
-      })
+      const { id } = this.script.props;
+      return sendCmd('UpdateValue', { id, key, value: rawValue })
       .then(() => {
       .then(() => {
         if (value) {
         if (value) {
           this.$set(this.values, key, rawValue);
           this.$set(this.values, key, rawValue);