diff options
| -rw-r--r-- | background_scripts/settings.coffee | 44 | ||||
| -rw-r--r-- | background_scripts/sync.coffee | 136 | ||||
| -rw-r--r-- | manifest.json | 2 | ||||
| -rw-r--r-- | pages/options.coffee | 8 | 
4 files changed, 177 insertions, 13 deletions
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index 0fe1e1bb..64fce308 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -7,17 +7,49 @@ root.Settings = Settings =    get: (key) ->      if (key of localStorage) then JSON.parse(localStorage[key]) else @defaults[key] -  set: (key, value) -> +  # The doNotSync argument suppresses calls to chrome.storage.sync.* while running unit tests +  set: (key, value, doNotSync) ->      # don't store the value if it is equal to the default, so we can change the defaults in the future -    if (value == @defaults[key]) -      @clear(key) -    else -      localStorage[key] = JSON.stringify(value) +    # warning: this test is always false for settings with numeric default values (such as scrollStepSize) +    if ( value == @defaults[key] ) +      return @clear(key,doNotSync) +    # don't update the key/value if it's unchanged; thereby suppressing unnecessary calls to chrome.storage +    valueJSON = JSON.stringify value +    if localStorage[key] == valueJSON +      return localStorage[key] +    # we have a new value: so update chrome.storage and localStorage +    root.Sync.set key, valueJSON if root?.Sync?.set +    localStorage[key] = valueJSON -  clear: (key) -> delete localStorage[key] +  # The doNotSync argument suppresses calls to chrome.storage.sync.* while running unit tests +  clear: (key, doNotSync) -> +    if @has key +      root.Sync.clear key if root?.Sync?.clear +      delete localStorage[key]    has: (key) -> key of localStorage +  # the postUpdateHooks handler below is called each time an option changes: +  #    either from options/options.coffee          (when the options page is saved) +  #        or from background_scripts/sync.coffee  (when an update propagates from chrome.storage) +  #  +  # NOTE: +  # this has been refactored and renamed from options.coffee(postSaveHooks): +  #   - refactored because it is now also called from sync.coffee +  #   - renamed because it is no longer associated only with "Save" operations +  # +  postUpdateHooks: +    keyMappings: (value) -> +      root.Commands.clearKeyMappingsAndSetDefaults() +      root.Commands.parseCustomKeyMappings value +      root.refreshCompletionKeysAfterMappingSave() +   +  # postUpdateHooks convenience wrapper +  doPostUpdateHook: (key, value) -> +    if @postUpdateHooks[key] +      @postUpdateHooks[key] value  + +    # options/options.(coffee|html) only handle booleans and strings; therefore    # all defaults must be booleans or strings    defaults: diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee new file mode 100644 index 00000000..d3a18b85 --- /dev/null +++ b/background_scripts/sync.coffee @@ -0,0 +1,136 @@ + +# +# * Sync.set() and Sync.clear() propagate local changes to chrome.storage. +# * Sync.listener() listens for changes to chrome.storage and propagates those +#   changes to localStorage and into vimium's internal state. +# * Sync.pull() polls chrome.storage at startup, similarly propagating changes +#   to localStorage and into vimium's internal state. +# +# Changes are propagated into vimium's state using the same mechanism that is +# used when options are changed on the options page. +# +# The effect is best-effort synchronization of vimium options/settings between +# chrome/vimium instances, whenever: +#   - chrome is logged in to the user's Google account, and +#   - chrome synchronization is enabled. +# +# NOTE: +#   Values handled within this module are ALWAYS already JSON.stringifed, so +#   they're always non-empty strings. +# + +console.log ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" +root = exports ? window +root.Sync = Sync =  + +  # ################## +  # constants + +  debug: true +  storage: chrome.storage.sync +  doNotSync: [ "settingsVersion", "previousVersion" ] + +  init: -> +    chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area +    @pull() +    @log "Sync.init()" + +  # asynchronous fetch from synced storage, called at startup +  pull: -> +    @storage.get null, (items) -> +      Sync.log "pull callback: #{Sync.callbackStatus()}" +      if not chrome.runtime.lastError +        for own key, value of items +          Sync.storeAndPropagate key, value + +  # asynchronous message from synced storage +  listener: (changes, area) -> +    @log "listener: #{area}" +    for own key, change of changes +      @storeAndPropagate key, change.newValue +   +  # only ever called from asynchronous synced-storage callbacks (pull and listener) +  storeAndPropagate: (key, value) -> +    # must be JSON.stringifed or undefined +    @checkHaveStringOrUndefined value +    # ignore, if not accepting this key +    if not @syncKey key +       @log "callback ignoring: #{key}" +       return +    # ignore, if unchanged +    if localStorage[key] == value +       @log "callback unchanged: #{key}" +       return + +    # ok: accept, store and propagate update +    defaultValue = root.Settings.defaults[key] +    defaultValueJSON = JSON.stringify(defaultValue) # could cache this to avoid repeated recalculation + +    if value && value != defaultValueJSON +      # key/value has been changed to non-default value at remote instance +      @log "callback set: #{key}=#{value}" +      localStorage[key] = value +      root.Settings.doPostUpdateHook key, JSON.parse(value) +    else +      # key has been reset to default value at remote instance +      @log "callback clear: #{key}=#{value}" +      delete localStorage[key] +      root.Settings.doPostUpdateHook key, defaultValue + +  # only called synchronously from within vimium, never on a callback +  # no need to propagate updates into the rest of vimium (because that will already have been handled externally) +  set: (key, value) -> +    # value must be JSON.stringifed +    @checkHaveString value +    if value +      if @syncKey key +        @storage.set @mkKeyValue(key,value), -> Sync.logCallback "DONE set", key +        @log "set scheduled: #{key}=#{value}" +    else +      # unreachable? (because value is a JSON string) +      @log "UNREACHABLE in Sync.set(): #{key}" +      @clear key + +  # only called synchronously from within vimium, never on a callback +  # no need to propagate updates into the rest of  vimium (because that will already have been handled by externally) +  clear: (key) -> +    if @syncKey key +      @storage.remove key, -> Sync.logCallback "DONE clear", key +      @log "clear scheduled: #{key}" + +  # ################## +  # utilities  + +  syncKey: (key) -> +    key not in @doNotSync + +  # there has to be a more elegant way to do this! +  mkKeyValue: (key, value) -> +    obj = {} +    obj[key] = value +    obj + +  # debugging messages +  # disable these by setting root.Sync.debug to anything falsy +  log: (msg) -> +    console.log "sync debug: #{msg}" if @debug + +  logCallback: (where, key) -> +    @log "#{where} callback: #{key} #{@callbackStatus()}" + +  callbackStatus: -> +    if chrome.runtime.lastError then "ERROR: #{chrome.runtime.lastError.message}" else "(OK)" + +  checkHaveString: (thing) -> +    if typeof(thing) != "string" or not thing +      @log "sync.coffee: Yikes! this should be a non-empty string: #{typeof(thing)} #{thing}" + +  checkHaveStringOrUndefined: (thing) -> +    if ( typeof(thing) != "string" and typeof(thing) != "undefined" ) or ( typeof(thing) == "string" and not thing ) +      @log "sync.coffee: Yikes! this should be a non-empty string or undefined: #{typeof(thing)} #{thing}" +   +  # end of Sync object +  # ################## + +Sync.init() + diff --git a/manifest.json b/manifest.json index 8de7f009..5692fe75 100644 --- a/manifest.json +++ b/manifest.json @@ -11,6 +11,7 @@        "lib/utils.js",        "background_scripts/commands.js",        "lib/clipboard.js", +      "background_scripts/sync.js",        "background_scripts/settings.js",        "background_scripts/completion.js",        "background_scripts/marks.js", @@ -23,6 +24,7 @@      "bookmarks",      "history",      "clipboardRead", +    "storage",      "<all_urls>"    ],    "content_scripts": [ diff --git a/pages/options.coffee b/pages/options.coffee index 117ce4a6..0aeb8e2d 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -8,12 +8,6 @@ editableFields = [ "scrollStepSize", "excludedUrls", "linkHintCharacters", "link  canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"] -postSaveHooks = keyMappings: (value) -> -  commands = chrome.extension.getBackgroundPage().Commands -  commands.clearKeyMappingsAndSetDefaults() -  commands.parseCustomKeyMappings value -  chrome.extension.getBackgroundPage().refreshCompletionKeysAfterMappingSave() -  document.addEventListener "DOMContentLoaded", ->    populateOptions() @@ -73,7 +67,7 @@ saveOptions = ->        bgSettings.set fieldName, fieldValue      $(fieldName).value = fieldValue      $(fieldName).setAttribute "savedValue", fieldValue -    postSaveHooks[fieldName] fieldValue if postSaveHooks[fieldName] +    bgSettings.doPostUpdateHook fieldName, fieldValue    $("saveOptions").disabled = true  | 
