aboutsummaryrefslogtreecommitdiffstats
path: root/background_scripts/sync.coffee
blob: d3a18b85982656e5f53fa70de3a4c5eab723ac5a (plain)
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
#
# * 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()