浏览代码

Disable profile syncing if quota exceeded in sync storage. Fix #272.

FelisCatus 10 年之前
父节点
当前提交
2f6e9dc280

+ 2 - 1
omega-target-chromium-extension/src/options.coffee

@@ -107,9 +107,10 @@ class ChromeOptions extends OmegaTarget.Options
       config['rules'] = rules
     return config
 
-  _proxyChangeWatchers: []
+  _proxyChangeWatchers: null
   _proxyChangeListener: null
   watchProxyChange: (callback) ->
+    @_proxyChangeWatchers = []
     if not @_proxyChangeListener?
       @_proxyChangeListener = (details) =>
         for watcher in @_proxyChangeWatchers

+ 2 - 1
omega-target-chromium-extension/src/proxy_auth.coffee

@@ -4,6 +4,7 @@ Promise = OmegaTarget.Promise
 
 module.exports = class ProxyAuth
   constructor: (options) ->
+    @_requests = {}
     @options = options
 
   listening: false
@@ -64,7 +65,7 @@ module.exports = class ProxyAuth
 
   _proxies: {}
   _fallbacks: []
-  _requests: {}
+  _requests: null
   authHandler: (details) ->
     return {} unless details.isProxy
     req = @_requests[details.requestId]

+ 3 - 2
omega-target-chromium-extension/src/tabs.coffee

@@ -1,9 +1,10 @@
 class ChromeTabs
-  _dirtyTabs: {}
   _defaultAction: null
   _badgeTab: null
 
-  constructor: (@actionForUrl) -> return
+  constructor: (@actionForUrl) ->
+    @_dirtyTabs = {}
+    return
 
   ignoreError: ->
     chrome.runtime.lastError

+ 4 - 3
omega-target/src/options.coffee

@@ -16,15 +16,13 @@ class Options
   # All the options, in a map from key to value.
   # @type OmegaOptions
   ###
-  _options: {}
+  _options: null
   _storage: null
   _state: null
   _currentProfileName: null
   _revertToProfileName: null
   _watchingProfiles: {}
   _tempProfile: null
-  _tempProfileRules: {}
-  _tempProfileRulesByProfile: {}
   fallbackProfileName: 'system'
   _isSystem: false
   debugStr: 'Options'
@@ -56,6 +54,9 @@ class Options
     return value
 
   constructor: (options, @_storage, @_state, @log, @sync) ->
+    @_options = {}
+    @_tempProfileRules = {}
+    @_tempProfileRulesByProfile = {}
     @_storage ?= Storage()
     @_state ?= Storage()
     @log ?= Log

+ 25 - 11
omega-target/src/options_sync.coffee

@@ -12,7 +12,6 @@ class OptionsSync
   _timeout: null
   _bucket: null
   _waiting: false
-  _pending: {}
 
   ###*
   # The debounce timeout (ms) for requestPush scheduling. See requestPush.
@@ -33,6 +32,7 @@ class OptionsSync
   storage: null
 
   constructor: (@storage, @_bucket) ->
+    @_pending = {}
     @_bucket ?= new TokenBucket(10, 10, 'minute', null)
     @_bucket.clear ?= =>
       @_bucket.tryRemoveTokens(@_bucket.content)
@@ -50,9 +50,10 @@ class OptionsSync
   ###*
   # Merge newVal and oldVal of a given key. The default implementation choose
   # between newVal and oldVal based on the following rules:
-  # 1. Choose oldVal if it has a revision newer than or equal to that of newVal.
-  # 2. Choose oldVal if it deeply equals newVal.
-  # 3. Otherwise, choose newVal.
+  # 1. Choose oldVal if syncOptions is 'disabled' in either oldVal or newVal.
+  # 2. Choose oldVal if it has a revision newer than or equal to that of newVal.
+  # 3. Choose oldVal if it deeply equals newVal.
+  # 4. Otherwise, choose newVal.
   #
   # @param {string} key The key of the item
   # @param {} newVal The new value
@@ -66,6 +67,8 @@ class OptionsSync
     )
     return (key, newVal, oldVal) ->
       return oldVal if newVal == oldVal
+      if oldVal?.syncOptions == 'disabled' or newVal?.syncOptions == 'disabled'
+        return oldVal
       if oldVal?.revision? and newVal?.revision?
         result = Revision.compare(oldVal.revision, newVal.revision)
         return oldVal if result >= 0
@@ -87,7 +90,7 @@ class OptionsSync
   ###
   requestPush: (changes) ->
     clearTimeout(@_timeout) if @_timeout?
-    for key, value of changes
+    for own key, value of changes
       if typeof value != 'undefined'
         value = @transformValue(value, key)
         continue if typeof value == 'undefined'
@@ -128,7 +131,7 @@ class OptionsSync
               return Promise.reject('bucket')
         ).catch (e) =>
           # Re-submit the changes for syncing, but with lower priority.
-          for key, value of set
+          for own key, value of set
             if not (key of @_pending)
               @_pending[key] = value
           for key in remove
@@ -144,8 +147,19 @@ class OptionsSync
             @requestPush({})
             return
           else if e instanceof Storage.QuotaExceededError
-            # TODO(catus): Remove profiles that are too large and retry.
-            @_pending = {}
+            # For now, we just disable syncing for all changed profiles.
+            # TODO(catus): Remove the largest profile each time and retry.
+            valuesAffected = 0
+            for own key, value of set
+              if key[0] == '+' and value.syncOptions != 'disabled'
+                value.syncOptions = 'disabled'
+                value.syncError = {reason: 'quotaPerItem'}
+                valuesAffected++
+            if valuesAffected > 0
+              @requestPush({})
+            else
+              @_pending = {}
+            return
           else
             Promise.reject(e)
 
@@ -162,8 +176,8 @@ class OptionsSync
   ###
   copyTo: (local) ->
     Promise.join local.get(null), @storage.get(null), (base, changes) =>
-      for key of base when not (key of changes)
-        if key[0] == '+'
+      for own key of base when not (key of changes)
+        if key[0] == '+' and not base[key]?.syncOptions == 'disabled'
           changes[key] = undefined
       local.apply(
         changes: changes
@@ -192,7 +206,7 @@ class OptionsSync
         local.apply(operations)
 
     @storage.watch null, (changes) =>
-      for key, value of changes
+      for own key, value of changes
         pull[key] = value
       return if pullScheduled?
       pullScheduled = setTimeout(doPull, @pullThrottle)

+ 9 - 17
omega-target/src/storage.coffee

@@ -35,31 +35,23 @@ class Storage
   # @param {?{}} args Extra arguments
   # @param {Object.<string, {}>?} args.base The original items in the storage.
   # @param {function(key, newVal, oldVal)} args.merge A function that merges
-  # the newVal and oldVal. oldVal is only provided if args.base is present.
+  # the newVal and oldVal. oldVal is provided only if args.base is present.
+  # Otherwise it will be equal to newVal (i.e. merge(key, newVal, newVal)).
   # @returns {WriteOperations} The operations that should be performed.
   ###
   @operationsForChanges: (changes, {base, merge} = {}) ->
     set = {}
     remove = []
     for key, newVal of changes
-      if not base?
-        newVal = if merge then merge(key, newVal) else newVal
-        if typeof newVal == 'undefined'
+      oldVal = if base? then base[key] else newVal
+      if merge
+        newVal = merge(key, newVal, oldVal)
+      continue if base? and newVal == oldVal
+      if typeof newVal == 'undefined'
+        if typeof oldVal != 'undefined' or not base?
           remove.push(key)
-        else
-          set[key] = newVal
       else
-        oldVal = base[key]
-        if typeof newVal == 'undefined'
-          if typeof oldVal != 'undefined'
-            remove.push(key)
-        else if newVal != oldVal
-          if merge
-            newVal = merge(key, newVal, oldVal)
-            if newVal != oldVal
-              set[key] = newVal
-          else
-            set[key] = newVal
+        set[key] = newVal
     return {set: set, remove: remove}
 
   ###*

+ 33 - 0
omega-target/test/options_sync.coffee

@@ -7,6 +7,7 @@ describe 'OptionsSync', ->
   OptionsSync = require '../src/options_sync'
   Storage = require '../src/storage'
   Log = require '../src/log'
+  Promise = require 'bluebird'
 
   before ->
     # Silence storage and sync logging.
@@ -36,6 +37,14 @@ describe 'OptionsSync', ->
       newVal = {revision: '2'}
       oldVal = {revision: '1'}
       sync.merge('example', newVal, oldVal).should.equal(newVal)
+    it 'should use oldVal when sync is disabled in newVal', ->
+      newVal = {revision: '2', is: 'newVal', syncOptions: 'disabled'}
+      oldVal = {revision: '1', is: 'oldVal'}
+      sync.merge('example', newVal, oldVal).should.equal(oldVal)
+    it 'should use oldVal when sync is disabled in oldVal', ->
+      newVal = {revision: '2', is: 'newVal'}
+      oldVal = {revision: '1', is: 'oldVal', syncOptions: 'disabled'}
+      sync.merge('example', newVal, oldVal).should.equal(oldVal)
     it 'should favor oldVal when revisions are equal', ->
       newVal = {revision: '1', is: 'newVal'}
       oldVal = {revision: '1', is: 'oldVal'}
@@ -101,6 +110,30 @@ describe 'OptionsSync', ->
       sync.requestPush({e: 1})
       sync.requestPush({e: undefined})
 
+    it 'should disable syncing for the profiles if quota is exceeded', (done) ->
+      options = {'+a': {is: 'a', oversized: true}, b: {is: 'b'}}
+
+      storage = new Storage()
+      storage.set = (changes) ->
+        for key, value of changes
+          if value.oversized
+            err = new Storage.QuotaExceededError()
+            err.perItem = true
+            return Promise.reject(err)
+        storage.set.should.have.been.calledTwice
+        storage.set.should.have.been.calledWith(options)
+        storage.set.should.have.been.calledWith({b: {is: 'b'}})
+        options['+a'].syncOptions.should.equal('disabled')
+        options['+a'].syncError.reason.should.equal('quotaPerItem')
+        done()
+        Promise.resolve()
+
+      sinon.spy(storage, 'set')
+
+      sync = new OptionsSync(storage, unlimited)
+      sync.debounce = 0
+      sync.requestPush(options)
+
   describe '#copyTo', ->
     it 'should fetch all items from remote storage', (done) ->
       remote = new Storage()