From 3aa47a198f2424ba91bb7a8ac07d6b2c53da9698 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Nov 2012 13:10:33 +0000 Subject: Initial synchronization commit. Synchronization is via `chrome.storage.sync.*`; data is cached in `localStorage`. --- background_scripts/settings.coffee | 44 ++++++++++-- background_scripts/sync.coffee | 136 +++++++++++++++++++++++++++++++++++++ manifest.json | 2 + pages/options.coffee | 8 +-- 4 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 background_scripts/sync.coffee 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", "" ], "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 -- cgit v1.2.3 From 75234c78cf13be08608ccd2b6bcecf2757b199ff Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 18 Apr 2014 23:09:39 +0100 Subject: Code review of Sync(). --- background_scripts/settings.coffee | 37 ++++------ background_scripts/sync.coffee | 139 +++++++++++++++++-------------------- 2 files changed, 78 insertions(+), 98 deletions(-) diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index 64fce308..063287db 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -7,47 +7,36 @@ root.Settings = Settings = get: (key) -> if (key of localStorage) then JSON.parse(localStorage[key]) else @defaults[key] - # 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 - # 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 + if (value == @defaults[key]) + @clear(key) + else + jsonified = JSON.stringify value + localStorage[key] = jsonified + root.Sync.set key, jsonified - # The doNotSync argument suppresses calls to chrome.storage.sync.* while running unit tests - clear: (key, doNotSync) -> + clear: (key) -> if @has key - root.Sync.clear key if root?.Sync?.clear delete localStorage[key] + root.Sync.clear 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 are 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) # postUpdateHooks: keyMappings: (value) -> root.Commands.clearKeyMappingsAndSetDefaults() root.Commands.parseCustomKeyMappings value root.refreshCompletionKeysAfterMappingSave() - + # postUpdateHooks convenience wrapper doPostUpdateHook: (key, value) -> if @postUpdateHooks[key] - @postUpdateHooks[key] value + @postUpdateHooks[key] value # options/options.(coffee|html) only handle booleans and strings; therefore diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index d3a18b85..7cb77518 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -1,30 +1,25 @@ # -# * Sync.set() and Sync.clear() propagate local changes to chrome.storage. -# * Sync.listener() listens for changes to chrome.storage and propagates those +# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync. +# * Sync.listener() listens for changes to chrome.storage.sync and propagates those +# changes to localStorage and into vimium's internal state. +# * Sync.pull() polls chrome.storage.sync at startup, similarly propagating # 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. +# Changes are propagated into vimium's state using the same mechanism +# (Settings.doPostUpdateHook) 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. +# chrome/vimium instances. # # 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 +root.Sync = Sync = debug: true storage: chrome.storage.sync @@ -33,104 +28,100 @@ root.Sync = Sync = init: -> chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area @pull() - @log "Sync.init()" - # asynchronous fetch from synced storage, called at startup + # Asynchronous fetch from synced storage, called only at startup. pull: -> @storage.get null, (items) -> - Sync.log "pull callback: #{Sync.callbackStatus()}" - if not chrome.runtime.lastError + if chrome.runtime.lastError is undefined for own key, value of items - Sync.storeAndPropagate key, value + @storeAndPropagate key, value + else + @log "chrome sync callback for Sync.pull() indicates error" + @log chrome.runtime.lastError - # asynchronous message from synced storage + # 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) + # 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}" + # Value must be JSON.stringifed or undefined. + if not @checkHaveStringOrUndefined value + return + # Ignore, we're not accepting this key. + if not @isSyncKey key + @log "ignoring: #{key}" return - # ignore, if unchanged - if localStorage[key] == value - @log "callback unchanged: #{key}" + # Ignore, it's unchanged + if localStorage[key] is value + @log "unchanged: #{key}" return - # ok: accept, store and propagate update + # Ok: accept, store and propagate this update. defaultValue = root.Settings.defaults[key] - defaultValueJSON = JSON.stringify(defaultValue) # could cache this to avoid repeated recalculation + defaultValueJSON = JSON.stringify(defaultValue) if value && value != defaultValueJSON - # key/value has been changed to non-default value at remote instance - @log "callback set: #{key}=#{value}" + # Key/value has been changed to non-default value at remote instance. + @log "update: #{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}" + # Key has been reset to default value at remote instance. + @log "clear: #{key}" 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) + # Only called synchronously from within vimium, never on a callback. + # No need to propagate updates into the rest of vimium. 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) + # value has already been JSON.stringifed + if not @checkHaveString value + return + # + if @isSyncKey key + @storage.set @mkKeyValue(key,value), -> + if chrome.runtime.lastError + @log "chrome sync callback for Sync.set() indicates error: " + key + @log chrome.runtime.lastError + @log "set scheduled: #{key}=#{value}" + + # Only called synchronously from within vimium, never on a callback. clear: (key) -> - if @syncKey key - @storage.remove key, -> Sync.logCallback "DONE clear", key - @log "clear scheduled: #{key}" - - # ################## - # utilities - - syncKey: (key) -> + if @isSyncKey key + @storage.remove key, -> + if chrome.runtime.lastError + @log "chrome sync callback for Sync.clear() indicates error: " + key + @log chrome.runtime.lastError + + # Should we synchronize this key? + isSyncKey: (key) -> key not in @doNotSync - # there has to be a more elegant way to do this! + # 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 + # Debugging messages. + # Disable debugginf by setting root.Sync.debug to anything falsy. + # Enabled for the time being (18/4/14) -- smblott. 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)" + console.log "Sync: #{msg}" if @debug checkHaveString: (thing) -> if typeof(thing) != "string" or not thing - @log "sync.coffee: Yikes! this should be a non-empty string: #{typeof(thing)} #{thing}" + @log "Sync: Yikes! this should be a non-empty string: #{typeof(thing)} #{thing}" + return false + return true 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}" + @log "Sync: Yikes! this should be a non-empty string or undefined: #{typeof(thing)} #{thing}" + return false + return true - # end of Sync object - # ################## - Sync.init() -- cgit v1.2.3 From 654738cb4d566c5e0b2d1e229dd9055403af35f6 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 00:13:36 +0100 Subject: Code review of Sync(). --- background_scripts/settings.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index 063287db..ad89888e 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -7,14 +7,14 @@ root.Settings = Settings = get: (key) -> if (key of localStorage) then JSON.parse(localStorage[key]) else @defaults[key] - set: (key, value, doNotSync) -> + set: (key, value) -> # 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 - jsonified = JSON.stringify value - localStorage[key] = jsonified - root.Sync.set key, jsonified + jsonValue = JSON.stringify value + localStorage[key] = jsonValue + root.Sync.set key, jsonValue clear: (key) -> if @has key -- cgit v1.2.3 From 033d353b265fcd4e9963ef232c17d82d7bea43f1 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 07:41:31 +0100 Subject: Settings tests pass, Sync still has an error. --- background_scripts/settings.coffee | 4 ++-- background_scripts/sync.coffee | 6 +++--- tests/unit_tests/settings_test.coffee | 29 ++++++++++++++++------------- tests/unit_tests/test_chrome_stubs.coffee | 11 +++++++++++ tests/unit_tests/utils_test.coffee | 3 +++ 5 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 tests/unit_tests/test_chrome_stubs.coffee diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index ad89888e..f75c1db3 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -14,12 +14,12 @@ root.Settings = Settings = else jsonValue = JSON.stringify value localStorage[key] = jsonValue - root.Sync.set key, jsonValue + Sync.set key, jsonValue clear: (key) -> if @has key delete localStorage[key] - root.Sync.clear key + Sync.clear key has: (key) -> key of localStorage diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index 7cb77518..40e0c5c3 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -59,19 +59,19 @@ root.Sync = Sync = return # Ok: accept, store and propagate this update. - defaultValue = root.Settings.defaults[key] + defaultValue = Settings.defaults[key] defaultValueJSON = JSON.stringify(defaultValue) if value && value != defaultValueJSON # Key/value has been changed to non-default value at remote instance. @log "update: #{key}=#{value}" localStorage[key] = value - root.Settings.doPostUpdateHook key, JSON.parse(value) + Settings.doPostUpdateHook key, JSON.parse(value) else # Key has been reset to default value at remote instance. @log "clear: #{key}" delete localStorage[key] - root.Settings.doPostUpdateHook key, defaultValue + Settings.doPostUpdateHook key, defaultValue # Only called synchronously from within vimium, never on a callback. # No need to propagate updates into the rest of vimium. diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index b2c5484b..040de285 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -1,30 +1,33 @@ require "./test_helper.js" +require "./test_chrome_stubs.js" extend(global, require "../../lib/utils.js") Utils.getCurrentVersion = -> '1.44' global.localStorage = {} -{Settings} = require "../../background_scripts/settings.js" + +extend(global,require "../../background_scripts/sync.js") +extend(global,require "../../background_scripts/settings.js") context "settings", setup -> stub global, 'localStorage', {} - should "obtain defaults if no key is stored", -> - assert.isFalse Settings.has 'scrollStepSize' - assert.equal Settings.get('scrollStepSize'), 60 +should "obtain defaults if no key is stored", -> + assert.isFalse Settings.has 'scrollStepSize' + assert.equal Settings.get('scrollStepSize'), 60 should "store values", -> Settings.set 'scrollStepSize', 20 assert.equal Settings.get('scrollStepSize'), 20 - should "not store values equal to the default", -> - Settings.set 'scrollStepSize', 20 - assert.isTrue Settings.has 'scrollStepSize' - Settings.set 'scrollStepSize', 60 - assert.isFalse Settings.has 'scrollStepSize' +should "not store values equal to the default", -> + Settings.set 'scrollStepSize', 20 + assert.isTrue Settings.has 'scrollStepSize' + Settings.set 'scrollStepSize', 60 + assert.isFalse Settings.has 'scrollStepSize' - should "revert to defaults if no key is stored", -> - Settings.set 'scrollStepSize', 20 - Settings.clear 'scrollStepSize' - assert.equal Settings.get('scrollStepSize'), 60 +should "revert to defaults if no key is stored", -> + Settings.set 'scrollStepSize', 20 + Settings.clear 'scrollStepSize' + assert.equal Settings.get('scrollStepSize'), 60 diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee new file mode 100644 index 00000000..86eef1a1 --- /dev/null +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -0,0 +1,11 @@ +global.chrome ||= {} +global.chrome.storage ||= {} + +global.chrome.storage.onChanged ||= + addListener: (changes,area) -> + +global.chrome.storage.sync ||= + set: (key,value,callback) -> + get: (keys,callback) -> + remove: (key,callback) -> + diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee index e1aa32c7..8e337d9e 100644 --- a/tests/unit_tests/utils_test.coffee +++ b/tests/unit_tests/utils_test.coffee @@ -1,4 +1,7 @@ require "./test_helper.js" +require "./test_chrome_stubs.js" + +extend(global, require "../../background_scripts/sync.js") extend(global, require "../../lib/utils.js") extend(global, require "../../background_scripts/settings.js") -- cgit v1.2.3 From 91a32f9ebe577aace222f615e17e924018b232e5 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 07:54:09 +0100 Subject: Functional sync. --- background_scripts/sync.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index 40e0c5c3..e498172e 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -31,9 +31,10 @@ root.Sync = Sync = # Asynchronous fetch from synced storage, called only at startup. pull: -> - @storage.get null, (items) -> + @storage.get null, (items) => if chrome.runtime.lastError is undefined for own key, value of items + @log "pull: #{key} <- #{value}" @storeAndPropagate key, value else @log "chrome sync callback for Sync.pull() indicates error" @@ -42,6 +43,7 @@ root.Sync = Sync = # Asynchronous message from synced storage. listener: (changes, area) -> for own key, change of changes + @log "listener: #{key} <- #{change.newValue}" @storeAndPropagate key, change.newValue # Only ever called from asynchronous synced-storage callbacks (pull and listener). -- cgit v1.2.3 From 8e0bb0a5c749cfae9586dd797d96a0f6233f828c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 08:01:29 +0100 Subject: Fix indentation and disable sync.log() messages. --- background_scripts/sync.coffee | 4 ++-- tests/unit_tests/settings_test.coffee | 25 ++++++++++++------------- tests/unit_tests/utils_test.coffee | 3 +-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index e498172e..afde3e70 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -21,7 +21,7 @@ root = exports ? window root.Sync = Sync = - debug: true + debug: false storage: chrome.storage.sync doNotSync: [ "settingsVersion", "previousVersion" ] @@ -124,6 +124,6 @@ root.Sync = Sync = @log "Sync: Yikes! this should be a non-empty string or undefined: #{typeof(thing)} #{thing}" return false return true - + Sync.init() diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 040de285..36a54f11 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -4,7 +4,6 @@ require "./test_chrome_stubs.js" extend(global, require "../../lib/utils.js") Utils.getCurrentVersion = -> '1.44' global.localStorage = {} - extend(global,require "../../background_scripts/sync.js") extend(global,require "../../background_scripts/settings.js") @@ -13,21 +12,21 @@ context "settings", setup -> stub global, 'localStorage', {} -should "obtain defaults if no key is stored", -> - assert.isFalse Settings.has 'scrollStepSize' - assert.equal Settings.get('scrollStepSize'), 60 + should "obtain defaults if no key is stored", -> + assert.isFalse Settings.has 'scrollStepSize' + assert.equal Settings.get('scrollStepSize'), 60 should "store values", -> Settings.set 'scrollStepSize', 20 assert.equal Settings.get('scrollStepSize'), 20 -should "not store values equal to the default", -> - Settings.set 'scrollStepSize', 20 - assert.isTrue Settings.has 'scrollStepSize' - Settings.set 'scrollStepSize', 60 - assert.isFalse Settings.has 'scrollStepSize' + should "not store values equal to the default", -> + Settings.set 'scrollStepSize', 20 + assert.isTrue Settings.has 'scrollStepSize' + Settings.set 'scrollStepSize', 60 + assert.isFalse Settings.has 'scrollStepSize' -should "revert to defaults if no key is stored", -> - Settings.set 'scrollStepSize', 20 - Settings.clear 'scrollStepSize' - assert.equal Settings.get('scrollStepSize'), 60 + should "revert to defaults if no key is stored", -> + Settings.set 'scrollStepSize', 20 + Settings.clear 'scrollStepSize' + assert.equal Settings.get('scrollStepSize'), 60 diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee index 8e337d9e..09e15353 100644 --- a/tests/unit_tests/utils_test.coffee +++ b/tests/unit_tests/utils_test.coffee @@ -1,8 +1,7 @@ require "./test_helper.js" require "./test_chrome_stubs.js" - -extend(global, require "../../background_scripts/sync.js") extend(global, require "../../lib/utils.js") +extend(global, require "../../background_scripts/sync.js") extend(global, require "../../background_scripts/settings.js") context "isUrl", -- cgit v1.2.3 From 64f290465aa14478200dc266ccc68b3475352e69 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 08:08:03 +0100 Subject: Add tests for sync. --- tests/unit_tests/settings_test.coffee | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 36a54f11..57766c12 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -30,3 +30,15 @@ context "settings", Settings.set 'scrollStepSize', 20 Settings.clear 'scrollStepSize' assert.equal Settings.get('scrollStepSize'), 60 + + should "remote changes take effect locally, non-default value", -> + Settings.set 'scrollStepSize', 20 + assert.equal Settings.get('scrollStepSize'), 20 + Sync.listener { scrollStepSize: { newValue: "40" } } + assert.equal Settings.get('scrollStepSize'), 40 + + should "remote changes take effect locally, default value", -> + Settings.set 'scrollStepSize', 20 + assert.equal Settings.get('scrollStepSize'), 20 + Sync.listener { scrollStepSize: { newValue: "60" } } + assert.isFalse Settings.has 'scrollStepSize' -- cgit v1.2.3 From a2de5078f8d34c2b65b451556aff07a6a41399a5 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 09:44:50 +0100 Subject: Add test cases for "asynchronous" sync. --- background_scripts/sync.coffee | 2 +- tests/unit_tests/settings_test.coffee | 22 ++++++++++++++ tests/unit_tests/test_chrome_stubs.coffee | 49 +++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index afde3e70..3a829722 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -83,7 +83,7 @@ root.Sync = Sync = return # if @isSyncKey key - @storage.set @mkKeyValue(key,value), -> + @storage.set @mkKeyValue(key,value), => if chrome.runtime.lastError @log "chrome sync callback for Sync.set() indicates error: " + key @log chrome.runtime.lastError diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 57766c12..07b96a30 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -42,3 +42,25 @@ context "settings", assert.equal Settings.get('scrollStepSize'), 20 Sync.listener { scrollStepSize: { newValue: "60" } } assert.isFalse Settings.has 'scrollStepSize' + + should "remote changes are propagated, non-default value", -> + # Prime Sync. + Settings.set 'scrollStepSize', 20 + assert.equal Settings.get('scrollStepSize'), 20 + # Set a bogus value in localStorage, bypassing Settings and Sync. + localStorage['scrollStepSize'] = JSON.stringify(10) + assert.equal Settings.get('scrollStepSize'), 10 + # Pull Sync's version of scrollStepSize, this should reset it to the correct value (20). + Sync.pull() + assert.equal Settings.get('scrollStepSize'), 20 + + should "remote changes are propagated, default value", -> + # Prime Sync with a default value. + chrome.storage.sync.set { scrollStepSize: JSON.stringify(60) } + assert.isFalse Settings.has 'scrollStepSize' + # Set a bogus value in localStorage, bypassing Settings and Sync. + localStorage['scrollStepSize'] = JSON.stringify(10) + assert.equal Settings.get('scrollStepSize'), 10 + # Pull Sync's version of scrollStepSize, this should delete scrollStepSize in localStorage, because it's a default value. + Sync.pull() + assert.isFalse Settings.has 'scrollStepSize' diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index 86eef1a1..f32de24f 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -1,11 +1,56 @@ global.chrome ||= {} +global.runtime ||= {} global.chrome.storage ||= {} +# +# This is a stub for chrome.strorage.sync for testing. +# It does what chrome.storage.sync should do (roughly), but does so synchronously. +# + global.chrome.storage.onChanged ||= - addListener: (changes,area) -> + addListener: (func) -> @func = func + + # Fake a callback from chrome.storage.sync. + call: (key,value) -> + chrome.runtime = { lastError: undefined } + if @func + @func( @mkKeyValue(key,value), 'synced storage stub' ) + + mkKeyValue: (key, value) -> + obj = {} + obj[key] = { newValue: value } + obj global.chrome.storage.sync ||= - set: (key,value,callback) -> + store: {} + + set: (items,callback) -> + chrome.runtime = { lastError: undefined } + for own key, value of items + @store[key] = value + if callback + callback() + # Now, generate (supposedly asynchronous) notifications for listeners. + for own key, value of items + global.chrome.storage.onChanged.call(key,value) + get: (keys,callback) -> + chrome.runtime = { lastError: undefined } + if keys == null + keys = [] + for own key, value of @store + keys.push key + items = {} + for key in keys + items[key] = @store[key] + # Now, generate (supposedly asynchronous) callback + if callback + callback items + remove: (key,callback) -> + chrome.runtime = { lastError: undefined } + if key of @store + delete @store[key] + if callback + callback() -- cgit v1.2.3 From 5ea93cd18611d859867bdd80c44c886d29a7b958 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 10:02:14 +0100 Subject: Add remote sync test for setting cleared. --- background_scripts/sync.coffee | 7 ++++--- tests/unit_tests/settings_test.coffee | 9 +++++++++ tests/unit_tests/test_chrome_stubs.coffee | 9 +++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index 3a829722..3b34c5a6 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -56,7 +56,7 @@ root.Sync = Sync = @log "ignoring: #{key}" return # Ignore, it's unchanged - if localStorage[key] is value + if key of localStorage and localStorage[key] is value @log "unchanged: #{key}" return @@ -64,7 +64,7 @@ root.Sync = Sync = defaultValue = Settings.defaults[key] defaultValueJSON = JSON.stringify(defaultValue) - if value && value != defaultValueJSON + if value and value != defaultValueJSON # Key/value has been changed to non-default value at remote instance. @log "update: #{key}=#{value}" localStorage[key] = value @@ -72,7 +72,8 @@ root.Sync = Sync = else # Key has been reset to default value at remote instance. @log "clear: #{key}" - delete localStorage[key] + if key of localStorage + delete localStorage[key] Settings.doPostUpdateHook key, defaultValue # Only called synchronously from within vimium, never on a callback. diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 07b96a30..9aec95dd 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -64,3 +64,12 @@ context "settings", # Pull Sync's version of scrollStepSize, this should delete scrollStepSize in localStorage, because it's a default value. Sync.pull() assert.isFalse Settings.has 'scrollStepSize' + + should "remote setting cleared", -> + # Prime localStorage. + Settings.set 'scrollStepSize', 20 + assert.equal Settings.get('scrollStepSize'), 20 + # Prime Sync with a non-default value. + chrome.storage.sync.set { scrollStepSize: JSON.stringify(40) } + chrome.storage.sync.remove 'scrollStepSize' + assert.isFalse Settings.has 'scrollStepSize' diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index f32de24f..dd8dccf8 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -16,6 +16,13 @@ global.chrome.storage.onChanged ||= if @func @func( @mkKeyValue(key,value), 'synced storage stub' ) + callEmpty: (key) -> + chrome.runtime = { lastError: undefined } + if @func + items = {} + items[key] = {} + @func( items, 'synced storage stub' ) + mkKeyValue: (key, value) -> obj = {} obj[key] = { newValue: value } @@ -53,4 +60,6 @@ global.chrome.storage.sync ||= delete @store[key] if callback callback() + # Now, generate (supposedly asynchronous) notification for listeners. + global.chrome.storage.onChanged.callEmpty(key) -- cgit v1.2.3 From 222d53fe43e34d8d2fd7c2fd859de53cf5fee1f3 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 11:00:32 +0100 Subject: Delay initializing sync until other components have started. --- background_scripts/main.coffee | 3 +++ background_scripts/sync.coffee | 6 ++++-- tests/unit_tests/settings_test.coffee | 1 + tests/unit_tests/utils_test.coffee | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index f564f477..dc853803 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -596,3 +596,6 @@ chrome.windows.getAll { populate: true }, (windows) -> createScrollPositionHandler = -> (response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response? chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler()) + +# Start pulling changes from synchrized storage. +Sync.init() diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index 3b34c5a6..b1ccb696 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -25,9 +25,11 @@ root.Sync = Sync = storage: chrome.storage.sync doNotSync: [ "settingsVersion", "previousVersion" ] + register: -> + chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area + init: -> chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area - @pull() # Asynchronous fetch from synced storage, called only at startup. pull: -> @@ -126,5 +128,5 @@ root.Sync = Sync = return false return true -Sync.init() +Sync.register() diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 9aec95dd..4b6f3498 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -6,6 +6,7 @@ Utils.getCurrentVersion = -> '1.44' global.localStorage = {} extend(global,require "../../background_scripts/sync.js") extend(global,require "../../background_scripts/settings.js") +Sync.init() context "settings", diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee index 09e15353..356bc39f 100644 --- a/tests/unit_tests/utils_test.coffee +++ b/tests/unit_tests/utils_test.coffee @@ -3,6 +3,7 @@ require "./test_chrome_stubs.js" extend(global, require "../../lib/utils.js") extend(global, require "../../background_scripts/sync.js") extend(global, require "../../background_scripts/settings.js") +Sync.init() context "isUrl", should "accept valid URLs", -> -- cgit v1.2.3 From b8c501ee2e4f85ef8307ec27f39ea98fe0779a6c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 11:07:30 +0100 Subject: Test that all stored settings are JSONified. --- tests/unit_tests/settings_test.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 4b6f3498..5d24c7f2 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -13,6 +13,10 @@ context "settings", setup -> stub global, 'localStorage', {} + should "all settings stored in localStorage must be JSONified strings", -> + Settings.set 'dummy', "" + assert.equal localStorage.dummy, '""' + should "obtain defaults if no key is stored", -> assert.isFalse Settings.has 'scrollStepSize' assert.equal Settings.get('scrollStepSize'), 60 -- cgit v1.2.3 From 0ff5b7a8ead8ad9051f0a9ba4a9667f07c9c38ac Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 11:19:59 +0100 Subject: Comments for philc. --- background_scripts/sync.coffee | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index b1ccb696..dfa260b5 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -1,4 +1,3 @@ - # # * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync. # * Sync.listener() listens for changes to chrome.storage.sync and propagates those @@ -21,6 +20,12 @@ root = exports ? window root.Sync = Sync = + # 19/4/14: + # Leave logging statements in, but disable debugging. + # We may need to come back to this, so removing logging now would be premature. + # However, if users have problems, they are unlikely to notice and make sense of console logs on + # background pages. So disable it, by default. + # For genuine errors, we call console.log directly. debug: false storage: chrome.storage.sync doNotSync: [ "settingsVersion", "previousVersion" ] @@ -34,13 +39,14 @@ root.Sync = Sync = # Asynchronous fetch from synced storage, called only at startup. pull: -> @storage.get null, (items) => + # Chrome sets chrome.runtime.lastError if there is an error. if chrome.runtime.lastError is undefined for own key, value of items @log "pull: #{key} <- #{value}" @storeAndPropagate key, value else - @log "chrome sync callback for Sync.pull() indicates error" - @log chrome.runtime.lastError + console.log "chrome sync callback for Sync.pull() indicates error" + console.log chrome.runtime.lastError # Asynchronous message from synced storage. listener: (changes, area) -> @@ -87,18 +93,20 @@ root.Sync = Sync = # if @isSyncKey key @storage.set @mkKeyValue(key,value), => + # Chrome sets chrome.runtime.lastError if there is an error. if chrome.runtime.lastError - @log "chrome sync callback for Sync.set() indicates error: " + key - @log chrome.runtime.lastError + console.log "chrome sync callback for Sync.set() indicates error: " + key + console.log chrome.runtime.lastError @log "set scheduled: #{key}=#{value}" # Only called synchronously from within vimium, never on a callback. clear: (key) -> if @isSyncKey key - @storage.remove key, -> + @storage.remove key, => + # Chrome sets chrome.runtime.lastError if there is an error. if chrome.runtime.lastError - @log "chrome sync callback for Sync.clear() indicates error: " + key - @log chrome.runtime.lastError + console.log "chrome sync callback for Sync.clear() indicates error: " + key + console.log chrome.runtime.lastError # Should we synchronize this key? isSyncKey: (key) -> @@ -129,4 +137,3 @@ root.Sync = Sync = return true Sync.register() - -- cgit v1.2.3 From 5398abdb0af3b0d7938c8b34bfda762c5995d427 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 14:27:34 +0100 Subject: Fix sync.init. --- background_scripts/sync.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index dfa260b5..4413a748 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -34,7 +34,7 @@ root.Sync = Sync = chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area init: -> - chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area + @pull() # Asynchronous fetch from synced storage, called only at startup. pull: -> -- cgit v1.2.3 From 2f343bacd7567cb51901b7f8b54f224ea2f011b2 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 14:49:45 +0100 Subject: Code review, clean up. --- background_scripts/sync.coffee | 52 +++++++++++------------------------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index 4413a748..454aa6fd 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -45,67 +45,56 @@ root.Sync = Sync = @log "pull: #{key} <- #{value}" @storeAndPropagate key, value else - console.log "chrome sync callback for Sync.pull() indicates error" + console.log "callback for Sync.pull() indicates error" console.log chrome.runtime.lastError # Asynchronous message from synced storage. listener: (changes, area) -> for own key, change of changes @log "listener: #{key} <- #{change.newValue}" - @storeAndPropagate key, change.newValue + @storeAndPropagate key, change?.newValue # Only ever called from asynchronous synced-storage callbacks (pull and listener). storeAndPropagate: (key, value) -> - # Value must be JSON.stringifed or undefined. - if not @checkHaveStringOrUndefined value - return - # Ignore, we're not accepting this key. - if not @isSyncKey key - @log "ignoring: #{key}" - return - # Ignore, it's unchanged - if key of localStorage and localStorage[key] is value - @log "unchanged: #{key}" - return + return if not key of Settings.defaults + return if not @isSyncKey key + return if value and key of localStorage and localStorage[key] is value - # Ok: accept, store and propagate this update. + # Ok: store and propagate this update. defaultValue = Settings.defaults[key] defaultValueJSON = JSON.stringify(defaultValue) if value and value != defaultValueJSON # Key/value has been changed to non-default value at remote instance. - @log "update: #{key}=#{value}" + @log "storeAndPropagate update: #{key}=#{value}" localStorage[key] = value Settings.doPostUpdateHook key, JSON.parse(value) else # Key has been reset to default value at remote instance. - @log "clear: #{key}" + @log "storeAndPropagate clear: #{key}" if key of localStorage delete localStorage[key] Settings.doPostUpdateHook key, defaultValue # Only called synchronously from within vimium, never on a callback. - # No need to propagate updates into the rest of vimium. + # No need to propagate updates to the rest of vimium, that's already been done. set: (key, value) -> - # value has already been JSON.stringifed - if not @checkHaveString value - return - # if @isSyncKey key + @log "set scheduled: #{key}=#{value}" @storage.set @mkKeyValue(key,value), => # Chrome sets chrome.runtime.lastError if there is an error. if chrome.runtime.lastError - console.log "chrome sync callback for Sync.set() indicates error: " + key + console.log "callback for Sync.set() indicates error: #{key} <- #{value}" console.log chrome.runtime.lastError - @log "set scheduled: #{key}=#{value}" # Only called synchronously from within vimium, never on a callback. clear: (key) -> if @isSyncKey key + @log "clear scheduled: #{key}" @storage.remove key, => # Chrome sets chrome.runtime.lastError if there is an error. if chrome.runtime.lastError - console.log "chrome sync callback for Sync.clear() indicates error: " + key + console.log "for Sync.clear() indicates error: #{key}" console.log chrome.runtime.lastError # Should we synchronize this key? @@ -118,22 +107,7 @@ root.Sync = Sync = obj[key] = value obj - # Debugging messages. - # Disable debugginf by setting root.Sync.debug to anything falsy. - # Enabled for the time being (18/4/14) -- smblott. log: (msg) -> console.log "Sync: #{msg}" if @debug - checkHaveString: (thing) -> - if typeof(thing) != "string" or not thing - @log "Sync: Yikes! this should be a non-empty string: #{typeof(thing)} #{thing}" - return false - return true - - checkHaveStringOrUndefined: (thing) -> - if ( typeof(thing) != "string" and typeof(thing) != "undefined" ) or ( typeof(thing) == "string" and not thing ) - @log "Sync: Yikes! this should be a non-empty string or undefined: #{typeof(thing)} #{thing}" - return false - return true - Sync.register() -- cgit v1.2.3 From 4083ee6423c10c34c1d0a3fa48e8f4d79c8615df Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 14:52:38 +0100 Subject: Clean up initialization. --- background_scripts/sync.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index 454aa6fd..1c6c7fb6 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -30,10 +30,9 @@ root.Sync = Sync = storage: chrome.storage.sync doNotSync: [ "settingsVersion", "previousVersion" ] - register: -> - chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area - + # This is called in main(). init: -> + chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area @pull() # Asynchronous fetch from synced storage, called only at startup. @@ -110,4 +109,3 @@ root.Sync = Sync = log: (msg) -> console.log "Sync: #{msg}" if @debug -Sync.register() -- cgit v1.2.3 From a9cec9742115a098a031ae7f946eb7bb93648ecd Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 19 Apr 2014 15:17:43 +0100 Subject: Add a couple more test cases for Sync. --- tests/unit_tests/settings_test.coffee | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 5d24c7f2..dd2815f0 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -78,3 +78,16 @@ context "settings", chrome.storage.sync.set { scrollStepSize: JSON.stringify(40) } chrome.storage.sync.remove 'scrollStepSize' assert.isFalse Settings.has 'scrollStepSize' + + should "trigger a postUpdateHook", -> + message = "Hello World" + # Install a bogus update hook for an existing setting. + Settings.postUpdateHooks['scrollStepSize'] = (value) -> Sync.message = value + chrome.storage.sync.set { scrollStepSize: JSON.stringify(message) } + # Was the update hook triggered? + assert.equal message, Sync.message + + should "sync a key which is not a setting", -> + # There is nothing to test, here. It's purpose is just to ensure that, should additional settings be + # added in future, then the Sync won't cause unexpected crashes. + chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") } -- cgit v1.2.3 From db65721aa67b2de75b1e279f01e721676e83b448 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 27 Apr 2014 11:52:30 +0100 Subject: Response to @philc's comments regarding sync. --- background_scripts/main.coffee | 2 +- background_scripts/settings.coffee | 13 ++-- background_scripts/sync.coffee | 50 ++++++------- pages/options.coffee | 2 +- tests/unit_tests/settings_test.coffee | 45 ++++-------- tests/unit_tests/test_chrome_stubs.coffee | 114 ++++++++++++++---------------- 6 files changed, 97 insertions(+), 129 deletions(-) diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index dc853803..5147aec2 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -597,5 +597,5 @@ chrome.windows.getAll { populate: true }, (windows) -> (response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response? chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler()) -# Start pulling changes from synchrized storage. +# Start pulling changes from synchronized storage. Sync.init() diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index f75c1db3..73a7a04b 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -23,10 +23,9 @@ root.Settings = Settings = has: (key) -> key of localStorage - # postUpdateHooks are 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) - # + # for settings which require action when their value changes, add hooks here + # called from options/options.coffee (when the options page is saved), and from background_scripts/sync.coffee (when + # an update propagates from chrome.storage.sync). postUpdateHooks: keyMappings: (value) -> root.Commands.clearKeyMappingsAndSetDefaults() @@ -34,10 +33,8 @@ root.Settings = Settings = root.refreshCompletionKeysAfterMappingSave() # postUpdateHooks convenience wrapper - doPostUpdateHook: (key, value) -> - if @postUpdateHooks[key] - @postUpdateHooks[key] value - + performPostUpdateHook: (key, value) -> + @postUpdateHooks[key] value if @postUpdateHooks[key] # options/options.(coffee|html) only handle booleans and strings; therefore # all defaults must be booleans or strings diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index 1c6c7fb6..56c74b81 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -1,12 +1,12 @@ # # * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync. -# * Sync.listener() listens for changes to chrome.storage.sync and propagates those +# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those # changes to localStorage and into vimium's internal state. -# * Sync.pull() polls chrome.storage.sync at startup, similarly propagating +# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating # changes to localStorage and into vimium's internal state. # # Changes are propagated into vimium's state using the same mechanism -# (Settings.doPostUpdateHook) that is used when options are changed on +# (Settings.performPostUpdateHook) that is used when options are changed on # the options page. # # The effect is best-effort synchronization of vimium options/settings between @@ -26,40 +26,38 @@ root.Sync = Sync = # However, if users have problems, they are unlikely to notice and make sense of console logs on # background pages. So disable it, by default. # For genuine errors, we call console.log directly. - debug: false + debug: true storage: chrome.storage.sync doNotSync: [ "settingsVersion", "previousVersion" ] - # This is called in main(). + # This is called in main.coffee. init: -> - chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area - @pull() + chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area + @fetchAsync() # Asynchronous fetch from synced storage, called only at startup. - pull: -> + fetchAsync: -> @storage.get null, (items) => # Chrome sets chrome.runtime.lastError if there is an error. if chrome.runtime.lastError is undefined for own key, value of items - @log "pull: #{key} <- #{value}" + @log "fetchAsync: #{key} <- #{value}" @storeAndPropagate key, value else - console.log "callback for Sync.pull() indicates error" + console.log "callback for Sync.fetchAsync() indicates error" console.log chrome.runtime.lastError # Asynchronous message from synced storage. - listener: (changes, area) -> + handleStorageUpdate: (changes, area) -> for own key, change of changes - @log "listener: #{key} <- #{change.newValue}" + @log "handleStorageUpdate: #{key} <- #{change.newValue}" @storeAndPropagate key, change?.newValue - # Only ever called from asynchronous synced-storage callbacks (pull and listener). + # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate). storeAndPropagate: (key, value) -> return if not key of Settings.defaults - return if not @isSyncKey key + return if not @shouldSyncKey key return if value and key of localStorage and localStorage[key] is value - - # Ok: store and propagate this update. defaultValue = Settings.defaults[key] defaultValueJSON = JSON.stringify(defaultValue) @@ -67,20 +65,22 @@ root.Sync = Sync = # Key/value has been changed to non-default value at remote instance. @log "storeAndPropagate update: #{key}=#{value}" localStorage[key] = value - Settings.doPostUpdateHook key, JSON.parse(value) + Settings.performPostUpdateHook key, JSON.parse(value) else # Key has been reset to default value at remote instance. @log "storeAndPropagate clear: #{key}" if key of localStorage delete localStorage[key] - Settings.doPostUpdateHook key, defaultValue + Settings.performPostUpdateHook key, defaultValue # Only called synchronously from within vimium, never on a callback. # No need to propagate updates to the rest of vimium, that's already been done. set: (key, value) -> - if @isSyncKey key + if @shouldSyncKey key @log "set scheduled: #{key}=#{value}" - @storage.set @mkKeyValue(key,value), => + key_value = {} + key_value[key] = value + @storage.set key_value, => # Chrome sets chrome.runtime.lastError if there is an error. if chrome.runtime.lastError console.log "callback for Sync.set() indicates error: #{key} <- #{value}" @@ -88,7 +88,7 @@ root.Sync = Sync = # Only called synchronously from within vimium, never on a callback. clear: (key) -> - if @isSyncKey key + if @shouldSyncKey key @log "clear scheduled: #{key}" @storage.remove key, => # Chrome sets chrome.runtime.lastError if there is an error. @@ -97,15 +97,9 @@ root.Sync = Sync = console.log chrome.runtime.lastError # Should we synchronize this key? - isSyncKey: (key) -> + shouldSyncKey: (key) -> key not in @doNotSync - # There has to be a more elegant way to do this! - mkKeyValue: (key, value) -> - obj = {} - obj[key] = value - obj - log: (msg) -> console.log "Sync: #{msg}" if @debug diff --git a/pages/options.coffee b/pages/options.coffee index 0aeb8e2d..34696f68 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -67,7 +67,7 @@ saveOptions = -> bgSettings.set fieldName, fieldValue $(fieldName).value = fieldValue $(fieldName).setAttribute "savedValue", fieldValue - bgSettings.doPostUpdateHook fieldName, fieldValue + bgSettings.performPostUpdateHook fieldName, fieldValue $("saveOptions").disabled = true diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index dd2815f0..25bb3628 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -13,7 +13,7 @@ context "settings", setup -> stub global, 'localStorage', {} - should "all settings stored in localStorage must be JSONified strings", -> + should "save settings in localStorage as JSONified strings", -> Settings.set 'dummy', "" assert.equal localStorage.dummy, '""' @@ -36,58 +36,39 @@ context "settings", Settings.clear 'scrollStepSize' assert.equal Settings.get('scrollStepSize'), 60 - should "remote changes take effect locally, non-default value", -> + should "propagate non-default value via synced storage listener", -> Settings.set 'scrollStepSize', 20 assert.equal Settings.get('scrollStepSize'), 20 - Sync.listener { scrollStepSize: { newValue: "40" } } + Sync.handleStorageUpdate { scrollStepSize: { newValue: "40" } } assert.equal Settings.get('scrollStepSize'), 40 - should "remote changes take effect locally, default value", -> + should "propagate default value via synced storage listener", -> Settings.set 'scrollStepSize', 20 assert.equal Settings.get('scrollStepSize'), 20 - Sync.listener { scrollStepSize: { newValue: "60" } } + Sync.handleStorageUpdate { scrollStepSize: { newValue: "60" } } assert.isFalse Settings.has 'scrollStepSize' - should "remote changes are propagated, non-default value", -> - # Prime Sync. - Settings.set 'scrollStepSize', 20 - assert.equal Settings.get('scrollStepSize'), 20 - # Set a bogus value in localStorage, bypassing Settings and Sync. - localStorage['scrollStepSize'] = JSON.stringify(10) - assert.equal Settings.get('scrollStepSize'), 10 - # Pull Sync's version of scrollStepSize, this should reset it to the correct value (20). - Sync.pull() + should "propagate non-default values from synced storage", -> + chrome.storage.sync.set { scrollStepSize: JSON.stringify(20) } + Sync.fetchAsync() assert.equal Settings.get('scrollStepSize'), 20 - should "remote changes are propagated, default value", -> - # Prime Sync with a default value. + should "propagate default values from synced storage", -> + Settings.set 'scrollStepSize', 20 chrome.storage.sync.set { scrollStepSize: JSON.stringify(60) } - assert.isFalse Settings.has 'scrollStepSize' - # Set a bogus value in localStorage, bypassing Settings and Sync. - localStorage['scrollStepSize'] = JSON.stringify(10) - assert.equal Settings.get('scrollStepSize'), 10 - # Pull Sync's version of scrollStepSize, this should delete scrollStepSize in localStorage, because it's a default value. - Sync.pull() + Sync.fetchAsync() assert.isFalse Settings.has 'scrollStepSize' - should "remote setting cleared", -> - # Prime localStorage. + should "clear a setting from synced storage", -> Settings.set 'scrollStepSize', 20 - assert.equal Settings.get('scrollStepSize'), 20 - # Prime Sync with a non-default value. - chrome.storage.sync.set { scrollStepSize: JSON.stringify(40) } chrome.storage.sync.remove 'scrollStepSize' assert.isFalse Settings.has 'scrollStepSize' should "trigger a postUpdateHook", -> message = "Hello World" - # Install a bogus update hook for an existing setting. Settings.postUpdateHooks['scrollStepSize'] = (value) -> Sync.message = value chrome.storage.sync.set { scrollStepSize: JSON.stringify(message) } - # Was the update hook triggered? assert.equal message, Sync.message - should "sync a key which is not a setting", -> - # There is nothing to test, here. It's purpose is just to ensure that, should additional settings be - # added in future, then the Sync won't cause unexpected crashes. + should "sync a key which is not a known setting (without crashing)", -> chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") } diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index dd8dccf8..e9c48f31 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -1,65 +1,61 @@ -global.chrome ||= {} -global.runtime ||= {} -global.chrome.storage ||= {} # # This is a stub for chrome.strorage.sync for testing. # It does what chrome.storage.sync should do (roughly), but does so synchronously. # -global.chrome.storage.onChanged ||= - addListener: (func) -> @func = func - - # Fake a callback from chrome.storage.sync. - call: (key,value) -> - chrome.runtime = { lastError: undefined } - if @func - @func( @mkKeyValue(key,value), 'synced storage stub' ) - - callEmpty: (key) -> - chrome.runtime = { lastError: undefined } - if @func - items = {} - items[key] = {} - @func( items, 'synced storage stub' ) - - mkKeyValue: (key, value) -> - obj = {} - obj[key] = { newValue: value } - obj - -global.chrome.storage.sync ||= - store: {} - - set: (items,callback) -> - chrome.runtime = { lastError: undefined } - for own key, value of items - @store[key] = value - if callback - callback() - # Now, generate (supposedly asynchronous) notifications for listeners. - for own key, value of items - global.chrome.storage.onChanged.call(key,value) - - get: (keys,callback) -> - chrome.runtime = { lastError: undefined } - if keys == null - keys = [] - for own key, value of @store - keys.push key - items = {} - for key in keys - items[key] = @store[key] - # Now, generate (supposedly asynchronous) callback - if callback - callback items - - remove: (key,callback) -> - chrome.runtime = { lastError: undefined } - if key of @store - delete @store[key] - if callback - callback() - # Now, generate (supposedly asynchronous) notification for listeners. - global.chrome.storage.onChanged.callEmpty(key) - +global.chrome = + runtime: {} + + storage: + + # chrome.storage.onChanged + onChanged: + addListener: (func) -> @func = func + + # Fake a callback from chrome.storage.sync. + call: (key, value) -> + chrome.runtime = { lastError: undefined } + key_value = {} + key_value[key] = { newValue: value } + @func(key_value,'synced storage stub') if @func + + callEmpty: (key) -> + chrome.runtime = { lastError: undefined } + if @func + items = {} + items[key] = {} + @func(items,'synced storage stub') + + # chrome.storage.sync + sync: + store: {} + + set: (items, callback) -> + chrome.runtime = { lastError: undefined } + for own key, value of items + @store[key] = value + callback() if callback + # Now, generate (supposedly asynchronous) notifications for listeners. + for own key, value of items + global.chrome.storage.onChanged.call(key,value) + + get: (keys, callback) -> + chrome.runtime = { lastError: undefined } + if keys == null + keys = [] + for own key, value of @store + keys.push key + items = {} + for key in keys + items[key] = @store[key] + # Now, generate (supposedly asynchronous) callback + callback items if callback + + remove: (key, callback) -> + chrome. runtime = { lastError: undefined } + if key of @store + delete @store[key] + callback() if callback + # Now, generate (supposedly asynchronous) notification for listeners. + global.chrome.storage.onChanged.callEmpty(key) -- cgit v1.2.3