Skip to content

Commit

Permalink
feat: options.cache and account.ready
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

As part of our effort to make Hoodie Client compatible with Service Worker and other environments that do not have access to localStorage, we have to make the Account Client compatible with async store APIs. That means we can’t load the current account state synchronously, so this is no longer be possible:

```js
var account = new Account({
  url: /api
})
if (account.isSignedIn()) {
  sayHi(account.username)
}
```

Starting with this release, you have to wrap the synchronous methods and properties into `account.ready.then()`

```js
account.ready.then(function () {
  if (account.isSignedIn()) {
    sayHi(account.username)
  }
})
```

By default, the account will still use localStorage (via [humble-localstorage](https://github.com/gr2m/humble-localstorage)) to persist its state, but it will now be made asynchronous. In order to use another storage a new `options.cache` argument can be passed to the Account constructor:

```js
var account = new Account({
  url: /api,
  cache: {
    set: writeAccountState
    get: getAccountState,
    unset: clearAccountState,
  }
})
```

All three `options.cache` methods must return promises. `options.cache.get` must resolve with the persisted account properties or an empty object.
  • Loading branch information
gr2m committed Dec 23, 2016
1 parent cdf00bb commit 962841a
Show file tree
Hide file tree
Showing 25 changed files with 294 additions and 221 deletions.
18 changes: 12 additions & 6 deletions admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = AccountAdmin

var EventEmitter = require('events').EventEmitter
var Hook = require('before-after-hook')
var LocalStorageStore = require('async-get-set-store')

var getUsername = require('../lib/username')
var signIn = require('../lib/sign-in')
Expand All @@ -17,7 +18,6 @@ var accountsRemove = require('./lib/accounts/remove')
var sessionsAdd = require('./lib/sessions/add')

var events = require('../lib/events')
var LocalStorageStore = require('../utils/localstorage-store')

function AccountAdmin (options) {
if (!(this instanceof AccountAdmin)) {
Expand All @@ -31,16 +31,19 @@ function AccountAdmin (options) {
var cacheKey = options.cacheKey || 'account_admin'
var emitter = options.emitter || new EventEmitter()
var accountsEmitter = new EventEmitter()
var store = new LocalStorageStore(cacheKey)
var cache = options.cache || new LocalStorageStore(cacheKey)

var state = {
accountsEmitter: accountsEmitter,
cacheKey: cacheKey,
emitter: emitter,
account: store.get(),
store: store,
account: undefined,
cache: cache,
url: options.url,
hook: new Hook()
hook: new Hook(),
ready: cache.get().then(function (storedAccount) {
state.account = storedAccount
})
}

var admin = {
Expand Down Expand Up @@ -73,7 +76,10 @@ function AccountAdmin (options) {
on: events.on.bind(null, state),
one: events.one.bind(null, state),
off: events.off.bind(null, state),
hook: state.hook.api
hook: state.hook.api,
ready: state.ready.then(function () {
return admin
})
}

// sessions.add can use accounts.find to lookup user id by username
Expand Down
18 changes: 11 additions & 7 deletions admin/lib/accounts/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ internals.deserialise = require('../../../utils/deserialise')
internals.serialise = require('../../../utils/serialise')

function add (state, account, options) {
return internals.request({
url: state.url + '/accounts' + query(options),
method: 'POST',
headers: {
authorization: 'Session ' + state.account.session.id
},
body: internals.serialise('account', account)
return state.ready

.then(function () {
return internals.request({
url: state.url + '/accounts' + query(options),
method: 'POST',
headers: {
authorization: 'Session ' + state.account.session.id
},
body: internals.serialise('account', account)
})
})

.then(function (response) {
Expand Down
16 changes: 10 additions & 6 deletions admin/lib/accounts/find-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ internals.request = require('../../../utils/request')
internals.deserialise = require('../../../utils/deserialise')

function findAll (state, options) {
return internals.request({
method: 'GET',
url: state.url + '/accounts' + query(options),
headers: {
authorization: 'Session ' + state.account.session.id
}
return state.ready

.then(function () {
return internals.request({
method: 'GET',
url: state.url + '/accounts' + query(options),
headers: {
authorization: 'Session ' + state.account.session.id
}
})
})

.then(function (response) {
Expand Down
16 changes: 10 additions & 6 deletions admin/lib/accounts/find.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ internals.request = require('../../../utils/request')
internals.deserialise = require('../../../utils/deserialise')

function find (state, id, options) {
return internals.request({
url: state.url + '/accounts/' + id + query(options),
method: 'GET',
headers: {
authorization: 'Session ' + state.account.session.id
}
return state.ready

.then(function () {
return internals.request({
url: state.url + '/accounts/' + id + query(options),
method: 'GET',
headers: {
authorization: 'Session ' + state.account.session.id
}
})
})

.then(function (response) {
Expand Down
6 changes: 5 additions & 1 deletion admin/lib/accounts/remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ internals.find = require('./find')
function remove (state, id, options) {
var account

return internals.find(state, id, options)
return state.ready

.then(function () {
return internals.find(state, id, options)
})

.then(function (_account) {
account = _account
Expand Down
6 changes: 5 additions & 1 deletion admin/lib/accounts/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ internals.serialise = require('../../../utils/serialise')
function update (state, id, change, options) {
var account

return internals.find(state, id, options)
return state.ready

.then(function () {
return internals.find(state, id, options)
})

.then(function (_account) {
account = _account
Expand Down
48 changes: 26 additions & 22 deletions admin/lib/sessions/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,33 @@ function add (state, options) {
return Promise.reject(new Error('options.username is required'))
}

// TODO: use accountsFind instead of accountsFindAll
// after updating accountsFind to match admin README doc
// return accountsFind(state, {username: options.username})
return accountsFindAll(state)
.then(function (response) {
var accountInfo = _.filter(
response, {username: options.username})[0]
if (!accountInfo) {
var notFoundErr = new Error('account not found')
notFoundErr.name = 'NotFoundError'
throw notFoundErr
}
return state.ready

return internals.request({
url: state.url + '/accounts/' + accountInfo.id + '/sessions',
method: 'POST',
headers: {
authorization: 'Session ' + state.account.session.id
.then(function () {
// TODO: use accountsFind instead of accountsFindAll
// after updating accountsFind to match admin README doc
// return accountsFind(state, {username: options.username})
return accountsFindAll(state)
.then(function (response) {
var accountInfo = _.filter(
response, {username: options.username})[0]
if (!accountInfo) {
var notFoundErr = new Error('account not found')
notFoundErr.name = 'NotFoundError'
throw notFoundErr
}

return internals.request({
url: state.url + '/accounts/' + accountInfo.id + '/sessions',
method: 'POST',
headers: {
authorization: 'Session ' + state.account.session.id
}
})
}).then(function (response) {
return internals.deserialise(response.body, {
include: 'account'
})
})
}).then(function (response) {
return internals.deserialise(response.body, {
include: 'account'
})
})
})
}
7 changes: 5 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function Account (options) {

var state = getState(options)

return {
var api = {
get username () {
return getUsername(state)
},
Expand All @@ -39,6 +39,9 @@ function Account (options) {
one: events.one.bind(null, state),
off: events.off.bind(null, state),
hook: state.hook.api,
validate: require('./lib/validate').bind(null, state)
validate: require('./lib/validate').bind(null, state),
ready: state.ready.then(function () { return api })
}

return api
}
19 changes: 10 additions & 9 deletions lib/destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,25 @@ internals.get = require('./get')
internals.isSignedIn = require('./is-signed-in')

function destroy (state) {
var accountProperties = internals.get(state)
var accountProperties
return state.ready

var promise = Promise.resolve()
.then(function () {
accountProperties = internals.get(state)

if (internals.isSignedIn(state)) {
promise = promise.then(function () {
internals.request({
if (internals.isSignedIn(state)) {
return internals.request({
method: 'DELETE',
url: state.url + '/session/account',
headers: {
authorization: 'Session ' + state.account.session.id
}
})
})
}
}
})

return promise.then(function () {
state.store.unset()
.then(function () {
state.cache.unset()

state.emitter.emit('signout', clone(state.account))
state.emitter.emit('destroy', clone(state.account))
Expand Down
28 changes: 17 additions & 11 deletions lib/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,23 @@ var set = require('lodash/set')

var internals = module.exports.internals = {}
internals.fetchProperties = require('../utils/fetch-properties')
internals.fetchProperties = require('../utils/fetch-properties')

function fetch (state, path) {
if (!state.account) {
var error = new Error('Not signed in')
error.name = 'UnauthenticatedError'
return Promise.reject(error)
}
return internals.fetchProperties({
url: state.url + '/session/account',
sessionId: get(state, 'account.session.id'),
path: path
return state.ready

.then(function () {
if (get(state, 'account.session') === undefined) {
var error = new Error('Not signed in')
error.name = 'UnauthenticatedError'
return Promise.reject(error)
}

return internals.fetchProperties({
url: state.url + '/session/account',
sessionId: get(state, 'account.session.id'),
path: path
})
})

.then(function (properties) {
Expand All @@ -27,7 +33,7 @@ function fetch (state, path) {
merge(state.account, properties)
}

state.store.set(state.account)
state.cache.set(state.account)

return properties
})
Expand All @@ -37,7 +43,7 @@ function fetch (state, path) {
state.account.session.invalid = true
state.emitter.emit('unauthenticate')

state.store.set(state.account)
state.cache.set(state.account)
}

throw error
Expand Down
4 changes: 4 additions & 0 deletions lib/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ var internals = module.exports.internals = {}
internals.getProperties = require('../utils/get-properties')

function accountGet (state, path) {
if (!state.account) {
throw new Error('account.get() not yet accessible, wait for account.ready to resolve')
}

return internals.getProperties(state.account, path)
}
4 changes: 4 additions & 0 deletions lib/has-invalid-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ module.exports = hasInvalidSession
var get = require('lodash/get')

function hasInvalidSession (state) {
if (!state.account) {
throw new Error('account.hasInvalidSession() not yet accessible, wait for account.ready to resolve')
}

return get(state, 'account.session.invalid')
}
7 changes: 1 addition & 6 deletions lib/id.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
module.exports = getId

var generateId = require('../utils/generate-id')

function getId (state) {
if (!state.account) {
state.account = {
id: generateId()
}
state.store.set(state.account)
throw new Error('account.id not yet accessible, wait for account.ready to resolve')
}
return state.account.id
}
4 changes: 4 additions & 0 deletions lib/is-signed-in.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ module.exports = isSignedIn
var get = require('lodash/get')

function isSignedIn (state) {
if (!state.account) {
throw new Error('account.isSignedIn() not yet accessible, wait for account.ready to resolve')
}

return get(state, 'account.session') !== undefined
}
16 changes: 10 additions & 6 deletions lib/profile-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ var internals = module.exports.internals = {}
internals.fetchProperties = require('../utils/fetch-properties')

function profileFetch (state, path) {
return internals.fetchProperties({
url: state.url + '/session/account/profile',
sessionId: get(state, 'account.session.id'),
path: path
return state.ready

.then(function () {
return internals.fetchProperties({
url: state.url + '/session/account/profile',
sessionId: get(state, 'account.session.id'),
path: path
})
})

.then(function (properties) {
Expand All @@ -20,7 +24,7 @@ function profileFetch (state, path) {
set(state.account, 'profile', properties)
}

state.store.set(state.account)
state.cache.set(state.account)

return properties
})
Expand All @@ -30,7 +34,7 @@ function profileFetch (state, path) {
state.account.session.invalid = true
state.emitter.emit('unauthenticate')

state.store.set(state.account)
state.cache.set(state.account)
}

throw error
Expand Down

0 comments on commit 962841a

Please sign in to comment.