background - introduce ObservableStore

This commit is contained in:
kumavis 2017-01-11 19:04:19 -08:00
parent cc5e9aca4f
commit 8012ede126
11 changed files with 222 additions and 185 deletions

View File

@ -13,15 +13,18 @@ const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
var popupIsOpen = false
const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi,
// Persistence Methods:
setData,
loadData,
// initial state
initState: loadData(),
})
// setup state persistence
controller.store.subscribe(setData)
const txManager = controller.txManager
function triggerUi () {
if (!popupIsOpen) notification.show()
@ -112,13 +115,7 @@ function updateBadge () {
// data :: setters/getters
function loadData () {
var oldData = getOldStyleData()
var newData
try {
newData = JSON.parse(window.localStorage[STORAGE_KEY])
} catch (e) {}
var data = extend({
let defaultData = {
meta: {
version: 0,
},
@ -129,32 +126,16 @@ function loadData () {
},
},
},
}, oldData || null, newData || null)
return data
}
function getOldStyleData () {
var config, wallet, seedWords
var result = {
meta: { version: 0 },
data: {},
}
var persisted
try {
config = JSON.parse(window.localStorage['config'])
result.data.config = config
} catch (e) {}
try {
wallet = JSON.parse(window.localStorage['lightwallet'])
result.data.wallet = wallet
} catch (e) {}
try {
seedWords = window.localStorage['seedWords']
result.data.seedWords = seedWords
} catch (e) {}
persisted = JSON.parse(window.localStorage[STORAGE_KEY])
} catch (err) {
persisted = null
}
return result
return extend(defaultData, persisted)
}
function setData (data) {

View File

@ -19,6 +19,7 @@ module.exports = ConfigManager
function ConfigManager (opts) {
// ConfigManager is observable and will emit updates
this._subs = []
this.store = opts.store
/* The migrator exported on the config-manager
* has two methods the user should be concerned with:
@ -36,12 +37,9 @@ function ConfigManager (opts) {
// config data format, and returns the new one.
migrations: migrations,
// How to load initial config.
// Includes step on migrating pre-pojo-migrator data.
loadData: opts.loadData,
// How to persist migrated config.
setData: opts.setData,
// Data persistence methods
loadData: () => this.store.get(),
setData: (value) => this.store.put(value),
})
}

View File

@ -1,7 +1,7 @@
const Streams = require('mississippi')
const StreamProvider = require('web3-stream-provider')
const ObjectMultiplex = require('./obj-multiplex')
const RemoteStore = require('./remote-store.js').RemoteStore
const RemoteStore = require('./observable/remote')
const createRandomId = require('./random-id')
module.exports = MetamaskInpageProvider
@ -72,13 +72,13 @@ MetamaskInpageProvider.prototype.send = function (payload) {
case 'eth_accounts':
// read from localStorage
selectedAccount = self.publicConfigStore.get('selectedAccount')
selectedAccount = self.publicConfigStore.get().selectedAccount
result = selectedAccount ? [selectedAccount] : []
break
case 'eth_coinbase':
// read from localStorage
selectedAccount = self.publicConfigStore.get('selectedAccount')
selectedAccount = self.publicConfigStore.get().selectedAccount
result = selectedAccount || '0x0000000000000000000000000000000000000000'
break
@ -117,9 +117,15 @@ MetamaskInpageProvider.prototype.isMetaMask = true
function remoteStoreWithLocalStorageCache (storageKey) {
// read local cache
var initState = JSON.parse(localStorage[storageKey] || '{}')
var store = new RemoteStore(initState)
// cache the latest state locally
let initState
try {
initState = JSON.parse(localStorage[storageKey] || '{}')
} catch (err) {
initState = {}
}
// intialize store
const store = new RemoteStore(initState)
// write local cache
store.subscribe(function (state) {
localStorage[storageKey] = JSON.stringify(state)
})

View File

@ -0,0 +1,50 @@
const Dnode = require('dnode')
const ObservableStore = require('./index')
const endOfStream = require('end-of-stream')
//
// HostStore
//
// plays host to many RemoteStores and sends its state over a stream
//
class HostStore extends ObservableStore {
constructor (initState, opts) {
super(initState)
this.opts = opts || {}
}
createStream () {
const self = this
// setup remotely exposed api
let remoteApi = {}
if (!self.opts.readOnly) {
remoteApi.put = (newState) => self.put(newState)
}
// listen for connection to remote
const dnode = Dnode(remoteApi)
dnode.on('remote', (remote) => {
// setup update subscription lifecycle
const updateHandler = (state) => remote.put(state)
self._onConnect(updateHandler)
endOfStream(dnode, () => self._onDisconnect(updateHandler))
})
return dnode
}
_onConnect (updateHandler) {
// subscribe to updates
this.subscribe(updateHandler)
// send state immediately
updateHandler(this.get())
}
_onDisconnect (updateHandler) {
// unsubscribe to updates
this.unsubscribe(updateHandler)
}
}
module.exports = HostStore

View File

@ -0,0 +1,33 @@
const EventEmitter = require('events').EventEmitter
class ObservableStore extends EventEmitter {
constructor (initialState) {
super()
this._state = initialState
}
get () {
return this._state
}
put (newState) {
this._put(newState)
}
subscribe (handler) {
this.on('update', handler)
}
unsubscribe (handler) {
this.removeListener('update', handler)
}
_put (newState) {
this._state = newState
this.emit('update', newState)
}
}
module.exports = ObservableStore

View File

@ -0,0 +1,51 @@
const Dnode = require('dnode')
const ObservableStore = require('./index')
const endOfStream = require('end-of-stream')
//
// RemoteStore
//
// connects to a HostStore and receives its latest state
//
class RemoteStore extends ObservableStore {
constructor (initState, opts) {
super(initState)
this.opts = opts || {}
this._remote = null
}
put (newState) {
if (!this._remote) throw new Error('RemoteStore - "put" called before connection to HostStore')
this._put(newState)
this._remote.put(newState)
}
createStream () {
const self = this
const dnode = Dnode({
put: (newState) => self._put(newState),
})
// listen for connection to remote
dnode.once('remote', (remote) => {
// setup connection lifecycle
self._onConnect(remote)
endOfStream(dnode, () => self._onDisconnect())
})
return dnode
}
_onConnect (remote) {
this._remote = remote
this.emit('connected')
}
_onDisconnect () {
this._remote = null
this.emit('disconnected')
}
}
module.exports = RemoteStore

View File

@ -0,0 +1,13 @@
module.exports = transformStore
function transformStore(inStore, outStore, stateTransform) {
const initState = stateTransform(inStore.get())
outStore.put(initState)
inStore.subscribe((inState) => {
const outState = stateTransform(inState)
outStore.put(outState)
})
return outStore
}

View File

@ -1,97 +0,0 @@
const Dnode = require('dnode')
const inherits = require('util').inherits
module.exports = {
HostStore: HostStore,
RemoteStore: RemoteStore,
}
function BaseStore (initState) {
this._state = initState || {}
this._subs = []
}
BaseStore.prototype.set = function (key, value) {
throw Error('Not implemented.')
}
BaseStore.prototype.get = function (key) {
return this._state[key]
}
BaseStore.prototype.subscribe = function (fn) {
this._subs.push(fn)
var unsubscribe = this.unsubscribe.bind(this, fn)
return unsubscribe
}
BaseStore.prototype.unsubscribe = function (fn) {
var index = this._subs.indexOf(fn)
if (index !== -1) this._subs.splice(index, 1)
}
BaseStore.prototype._emitUpdates = function (state) {
this._subs.forEach(function (handler) {
handler(state)
})
}
//
// host
//
inherits(HostStore, BaseStore)
function HostStore (initState, opts) {
BaseStore.call(this, initState)
}
HostStore.prototype.set = function (key, value) {
this._state[key] = value
process.nextTick(this._emitUpdates.bind(this, this._state))
}
HostStore.prototype.createStream = function () {
var dnode = Dnode({
// update: this._didUpdate.bind(this),
})
dnode.on('remote', this._didConnect.bind(this))
return dnode
}
HostStore.prototype._didConnect = function (remote) {
this.subscribe(function (state) {
remote.update(state)
})
remote.update(this._state)
}
//
// remote
//
inherits(RemoteStore, BaseStore)
function RemoteStore (initState, opts) {
BaseStore.call(this, initState)
this._remote = null
}
RemoteStore.prototype.set = function (key, value) {
this._remote.set(key, value)
}
RemoteStore.prototype.createStream = function () {
var dnode = Dnode({
update: this._didUpdate.bind(this),
})
dnode.once('remote', this._didConnect.bind(this))
return dnode
}
RemoteStore.prototype._didConnect = function (remote) {
this._remote = remote
}
RemoteStore.prototype._didUpdate = function (state) {
this._state = state
this._emitUpdates(state)
}

View File

@ -6,26 +6,38 @@ const KeyringController = require('./keyring-controller')
const NoticeController = require('./notice-controller')
const messageManager = require('./lib/message-manager')
const TxManager = require('./transaction-manager')
const HostStore = require('./lib/remote-store.js').HostStore
const Web3 = require('web3')
const ConfigManager = require('./lib/config-manager')
const extension = require('./lib/extension')
const autoFaucet = require('./lib/auto-faucet')
const nodeify = require('./lib/nodeify')
const IdStoreMigrator = require('./lib/idStore-migrator')
const ObservableStore = require('./lib/observable/')
const HostStore = require('./lib/observable/host')
const transformStore = require('./lib/observable/util/transform')
const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter {
constructor (opts) {
super()
this.state = { network: 'loading' }
this.opts = opts
this.configManager = new ConfigManager(opts)
this.state = { network: 'loading' }
// observable state store
this.store = new ObservableStore(opts.initState)
// config manager
this.configManager = new ConfigManager({
store: this.store,
})
// key mgmt
this.keyringController = new KeyringController({
configManager: this.configManager,
getNetwork: this.getStateNetwork.bind(this),
})
this.keyringController.on('newAccount', (account) => {
autoFaucet(account)
})
// notices
this.noticeController = new NoticeController({
configManager: this.configManager,
@ -228,29 +240,20 @@ module.exports = class MetamaskController extends EventEmitter {
initPublicConfigStore () {
// get init state
var initPublicState = configToPublic(this.configManager.getConfig())
var publicConfigStore = new HostStore(initPublicState)
var initPublicState = this.store.get()
var publicConfigStore = new HostStore(initPublicState, { readOnly: true })
// subscribe to changes
this.configManager.subscribe(function (state) {
storeSetFromObj(publicConfigStore, configToPublic(state))
})
// sync publicConfigStore with transform
transformStore(this.store, publicConfigStore, selectPublicState)
this.keyringController.on('newAccount', (account) => {
autoFaucet(account)
})
// config substate
function configToPublic (state) {
return {
selectedAccount: state.selectedAccount,
function selectPublicState(state) {
let result = { selectedAccount: undefined }
try {
result.selectedAccount = state.data.config.selectedAccount
} catch (err) {
console.warn('Error in "selectPublicState": ' + err.message)
}
}
// dump obj into store
function storeSetFromObj (store, obj) {
Object.keys(obj).forEach(function (key) {
store.set(key, obj[key])
})
return result
}
return publicConfigStore

View File

@ -47,11 +47,13 @@ const controller = new MetamaskController({
showUnconfirmedMessage: noop,
unlockAccountMessage: noop,
showUnapprovedTx: noop,
// Persistence Methods:
setData,
loadData,
// initial state
initState: loadData(),
})
// setup state persistence
controller.store.subscribe(setData)
// Stub out localStorage for non-browser environments
if (!window.localStorage) {
window.localStorage = {}

View File

@ -1,25 +1,23 @@
var ConfigManager = require('../../../app/scripts/lib/config-manager')
var IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator')
var SimpleKeyring = require('../../../app/scripts/keyrings/simple')
var normalize = require('../../../app/scripts/lib/sig-util').normalize
const ObservableStore = require('../../../app/scripts/lib/observable/')
const ConfigManager = require('../../../app/scripts/lib/config-manager')
const IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator')
const SimpleKeyring = require('../../../app/scripts/keyrings/simple')
const normalize = require('../../../app/scripts/lib/sig-util').normalize
var oldStyleVault = require('../mocks/oldVault.json')
var badStyleVault = require('../mocks/badVault.json')
const oldStyleVault = require('../mocks/oldVault.json')
const badStyleVault = require('../mocks/badVault.json')
var STORAGE_KEY = 'metamask-config'
var PASSWORD = '12345678'
var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase()
var SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner'
var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9'
const PASSWORD = '12345678'
const FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase()
const BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9'
const SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner'
QUnit.module('Old Style Vaults', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(oldStyleVault)
let store = new ObservableStore(oldStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
store: store,
})
this.migrator = new IdStoreMigrator({
@ -46,11 +44,10 @@ QUnit.test('migrator:migratedVaultForPassword', function (assert) {
QUnit.module('Old Style Vaults with bad HD seed', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault)
let store = new ObservableStore(badStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
store: store,
})
this.migrator = new IdStoreMigrator({