diff --git a/omega-target-chromium-extension/src/options.coffee b/omega-target-chromium-extension/src/options.coffee index d585a2c..056fe4e 100644 --- a/omega-target-chromium-extension/src/options.coffee +++ b/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 diff --git a/omega-target-chromium-extension/src/proxy_auth.coffee b/omega-target-chromium-extension/src/proxy_auth.coffee index 56d9255..cebdc2e 100644 --- a/omega-target-chromium-extension/src/proxy_auth.coffee +++ b/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] diff --git a/omega-target-chromium-extension/src/tabs.coffee b/omega-target-chromium-extension/src/tabs.coffee index d51b6d5..6f9456a 100644 --- a/omega-target-chromium-extension/src/tabs.coffee +++ b/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 diff --git a/omega-target/src/options.coffee b/omega-target/src/options.coffee index 9fc2c33..bf6c50a 100644 --- a/omega-target/src/options.coffee +++ b/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 diff --git a/omega-target/src/options_sync.coffee b/omega-target/src/options_sync.coffee index 8a8c113..febd408 100644 --- a/omega-target/src/options_sync.coffee +++ b/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) diff --git a/omega-target/src/storage.coffee b/omega-target/src/storage.coffee index 896ac1a..49ba2e5 100644 --- a/omega-target/src/storage.coffee +++ b/omega-target/src/storage.coffee @@ -35,31 +35,23 @@ class Storage # @param {?{}} args Extra arguments # @param {Object.?} 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} ###* diff --git a/omega-target/test/options_sync.coffee b/omega-target/test/options_sync.coffee index 77401b5..dbc7619 100644 --- a/omega-target/test/options_sync.coffee +++ b/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()