1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
  | 
#
# * 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.
#
# 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.
#
# NOTE:
#   Values handled within this module are ALWAYS already JSON.stringifed, so
#   they're always non-empty strings.
#
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" ]
  register: ->
    chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area
  init: ->
    chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area
  # 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
        console.log "chrome sync 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
  
  # 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
    # Ok: accept, 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}"
      localStorage[key] = value
      Settings.doPostUpdateHook key, JSON.parse(value)
    else
      # Key has been reset to default value at remote instance.
      @log "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.
  set: (key, value) ->
    # value has already been JSON.stringifed
    if not @checkHaveString value
      return
    #
    if @isSyncKey key
      @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 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, =>
        # 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 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!
  mkKeyValue: (key, value) ->
    obj = {}
    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()
  |