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

This commit is contained in:
FelisCatus 2015-02-11 15:29:47 +08:00
parent 718c569fc8
commit 2f6e9dc280
7 changed files with 78 additions and 35 deletions

View File

@ -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

View File

@ -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]

View File

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

View File

@ -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

View File

@ -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)

View File

@ -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}
###*

View File

@ -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()