From d0b7f2b0dd67c779901bb3bceceef5a9433a11f7 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Thu, 6 Aug 2015 12:21:24 -0700 Subject: [PATCH] feat(database): Save and retrieve arbitrary json blobs to database This fixes T2233, which was caused by the main window trying to write config.cson very often as initial sync happened, and the parent process trying to observe those changes on disk to watch for the user's API key being removed. Further refactoring would be good but this will fix it. --- src/browser/application.coffee | 1 - src/flux/nylas-sync-worker.coffee | 17 +++-- src/flux/stores/contact-store.coffee | 70 +++++++------------ .../database-setup-query-builder.coffee | 4 ++ src/flux/stores/database-store.coffee | 11 +++ 5 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/browser/application.coffee b/src/browser/application.coffee index 885da706b..42b9f704d 100644 --- a/src/browser/application.coffee +++ b/src/browser/application.coffee @@ -158,7 +158,6 @@ class Application buttons: ['OK'] fs.unlink path.join(configDirPath,'edgehill.db'), (err) => @setDatabasePhase('setup') - @config.set("nylas.sync-state", {}) @windowManager.showMainWindow() # Registers basic application commands, non-idempotent. diff --git a/src/flux/nylas-sync-worker.coffee b/src/flux/nylas-sync-worker.coffee index f54037345..961c1edb3 100644 --- a/src/flux/nylas-sync-worker.coffee +++ b/src/flux/nylas-sync-worker.coffee @@ -1,6 +1,6 @@ _ = require 'underscore' NylasLongConnection = require './nylas-long-connection' - +DatabaseStore = require './stores/database-store' {Publisher} = require './modules/reflux-coffee' CoffeeHelpers = require './coffee-helpers' @@ -18,9 +18,13 @@ class NylasSyncWorker @_terminated = false @_connection = new NylasLongConnection(api, namespace.id) - @_state = atom.config.get("nylas.sync-state.#{namespace.id}") ? {} - for model, modelState of @_state - modelState.busy = false + + @_state = null + DatabaseStore.findJSONObject("NylasSyncWorker:#{@_namespace.id}").then (json) => + @_state = json ? {} + for model, modelState of @_state + modelState.busy = false + @resumeFetches() @ @@ -34,6 +38,7 @@ class NylasSyncWorker @_state busy: -> + return false unless @_state for key, state of @_state if state.busy return true @@ -51,6 +56,7 @@ class NylasSyncWorker @ resumeFetches: => + return unless @_state @fetchCollection('threads') @fetchCollection('calendars') @fetchCollection('contacts') @@ -61,6 +67,7 @@ class NylasSyncWorker @fetchCollection('folders') fetchCollection: (model, options = {}) -> + return unless @_state return if @_state[model]?.complete and not options.force? return if @_state[model]?.busy @@ -113,7 +120,7 @@ class NylasSyncWorker writeState: -> @_writeState ?= _.debounce => - atom.config.set("nylas.sync-state.#{@_namespace.id}", @_state) + DatabaseStore.persistJSONObject("NylasSyncWorker:#{@_namespace.id}", @_state) ,100 @_writeState() @trigger() diff --git a/src/flux/stores/contact-store.coffee b/src/flux/stores/contact-store.coffee index a9d44a225..72d431ff3 100644 --- a/src/flux/stores/contact-store.coffee +++ b/src/flux/stores/contact-store.coffee @@ -32,7 +32,7 @@ If you do not wish to refresh the value, do not call the callback. When you create an instance of a JSONCache, you need to provide several settings: -- `localPath`: path on disk to keep the cache +- `key`: A unique key identifying this object. - `version`: a version number. If the local cache has a different version number it will be thrown out. Useful if you want to change the format of the data @@ -43,63 +43,42 @@ When you create an instance of a JSONCache, you need to provide several settings ### class JSONCache @include: CoffeeHelpers.includeModule - @include Publisher - constructor: ({@localPath, @version, @maxAge}) -> + constructor: ({@key, @version, @maxAge}) -> @_value = null - @readLocal() + DatabaseStore.findJSONObject(@key).then (json) => + return @refresh() unless json + return @refresh() unless json.version is @version + @_value = json.value + @trigger() - detatch: => - clearInterval(@_interval) if @_interval + age = (new Date).getTime() - json.time + if age > @maxAge + @refresh() + else + setTimeout(@refresh, @maxAge - age) value: -> @_value reset: -> - fs.unlink @localPath, (err) -> - console.error(err) + DatabaseStore.persistJSONObject(@key, {}) + clearInterval(@_interval) if @_interval + @_interval = null @_value = null - readLocal: => - fs.exists @localPath, (exists) => - return @refresh() unless exists - fs.readFile @localPath, (err, contents) => - return @refresh() unless contents and not err - try - json = JSON.parse(contents) - if json.version isnt @version - throw new Error("Outdated schema") - if not json.time - throw new Error("No fetch time present") - @_value = json.value - @trigger() - - age = (new Date).getTime() - json.time - if age > @maxAge - @refresh() - else - setTimeout(@refresh, @maxAge - age) - - catch err - console.error(err) - @reset() - @refresh() - - writeLocal: => - json = - version: @version - time: (new Date).getTime() - value: @_value - fs.writeFile(@localPath, JSON.stringify(json)) - refresh: => clearInterval(@_interval) if @_interval @_interval = setInterval(@refresh, @maxAge) @refreshValue (newValue) => @_value = newValue - @writeLocal() + DatabaseStore.persistJSONObject(@key, { + version: @version + time: (new Date).getTime() + value: @_value + }) @trigger() refreshValue: (callback) => @@ -108,6 +87,9 @@ class JSONCache class RankingsJSONCache extends JSONCache + constructor: -> + super(key: 'RankingsJSONCache', version: 1, maxAge: 60 * 60 * 1000 * 24) + refreshValue: (callback) => return unless atom.isMainWindow() @@ -154,11 +136,7 @@ class ContactStore extends NylasStore @_contactCache = [] @_namespaceId = null - @_rankingsCache = new RankingsJSONCache - localPath: path.join(atom.getConfigDirPath(), 'contact-rankings.json') - maxAge: 60 * 60 * 1000 * 24 # one day - version: 1 - + @_rankingsCache = new RankingsJSONCache() @listenTo DatabaseStore, @_onDatabaseChanged @listenTo NamespaceStore, @_onNamespaceChanged @listenTo @_rankingsCache, @_sortContactsCacheWithRankings diff --git a/src/flux/stores/database-setup-query-builder.coffee b/src/flux/stores/database-setup-query-builder.coffee index 8e9d9da2e..e4ca0fee7 100644 --- a/src/flux/stores/database-setup-query-builder.coffee +++ b/src/flux/stores/database-setup-query-builder.coffee @@ -22,6 +22,10 @@ class DatabaseSetupQueryBuilder attributes = _.values(klass.attributes) queries = [] + # Add table for storing generic JSON blobs + queries.push("CREATE TABLE IF NOT EXISTS `JSONObject` (key TEXT PRIMARY KEY, data BLOB)") + queries.push("CREATE UNIQUE INDEX IF NOT EXISTS `JSONObject_id` ON `JSONObject` (`key`)") + # Identify attributes of this class that can be matched against. These # attributes need their own columns in the table columnAttributes = _.filter attributes, (attr) -> diff --git a/src/flux/stores/database-store.coffee b/src/flux/stores/database-store.coffee index ad5e19b15..1dc70ee4c 100644 --- a/src/flux/stores/database-store.coffee +++ b/src/flux/stores/database-store.coffee @@ -503,6 +503,17 @@ class DatabaseStore extends NylasStore }) @_triggerSoon({objectClass: newModel.constructor.name, objects: [oldModel, newModel], type: 'swap'}) + persistJSONObject: (key, json) -> + @_query(BEGIN_TRANSACTION) + @_query("REPLACE INTO `JSONObject` (`key`,`data`) VALUES (?,?)", [key, JSON.stringify(json)]) + @_query(COMMIT) + + findJSONObject: (key) -> + @_query("SELECT `data` FROM `JSONObject` WHERE key = ? LIMIT 1", [key]).then (results) => + return Promise.resolve(null) unless results[0] + data = JSON.parse(results[0].data) + Promise.resolve(data) + ######################################################################## ########################### PRIVATE METHODS ############################ ########################################################################