UNPKG

@hoodie/client

Version:
1,916 lines (1,515 loc) 407 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Hoodie = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ module.exports = Hoodie var getState = require('./lib/get-state') var getApi = require('./lib/get-api') function Hoodie (options) { var state = getState(options) var api = getApi(state) return api } },{"./lib/get-api":3,"./lib/get-state":4}],2:[function(require,module,exports){ module.exports = { on: on, one: one, off: off, trigger: trigger } /** * add a listener to an event * * @param {String} eventName Name of event * @param {Function} handler callback for event */ function on (state, eventName, handler) { state.emitter.on(eventName, handler) return this } /** * adds a one time listener to an event * * @param {String} eventName Name of event * @param {Function} handler callback for event */ function one (state, eventName, handler) { state.emitter.once(eventName, handler) return this } /** * removes a listener for the specified event * * It will unsubscribe at most, one instance of a listener for a particular event. * If any single listener has subcribed multiple times to the same event, * then `off` must be called multiple times. * * @param {String} eventName Name of event * @param {Function} handler callback for event */ function off (state, eventName, handler) { state.emitter.removeListener(eventName, handler) return this } /** * trigger a specified event * * @param {String} eventName Name of event * @param {...*} [options] Options */ function trigger (state, eventName) { var args = [].slice.call(arguments, 1) state.emitter.emit.apply(state.emitter, args) return this } },{}],3:[function(require,module,exports){ module.exports = getApi var defaultsDeep = require('lodash/defaultsDeep') var internals = module.exports.internals = {} internals.Store = require('@hoodie/store-client') internals.Account = require('@hoodie/account-client') internals.ConnectionStatus = require('@hoodie/connection-status/client') internals.Log = require('@hoodie/log/client') internals.pouchdbDocApi = require('pouchdb-doc-api') internals.init = require('./init') function getApi (state) { var url = state.url + '/hoodie' state.PouchDB.plugin(internals.pouchdbDocApi) var hoodieDb = new state.PouchDB('hoodie') var hoodieAccount = mergeOptionsAndCreate(internals.Account, { cache: hoodieDb.doc('_local/account'), url: url + '/account/api' }, state.account) var hoodieConnectionStatus = mergeOptionsAndCreate(internals.ConnectionStatus, { cache: hoodieDb.doc('_local/connection-status'), url: url }, state.connectionStatus) var log = mergeOptionsAndCreate(internals.Log, { prefix: 'hoodie' }, state.log) var hoodieStore = new internals.Store('store', { PouchDB: state.PouchDB, get remote () { return hoodieAccount.get(['id', 'session.id']).then(function (properties) { var headers = { authorization: 'Session ' + properties.session.id } return new state.PouchDB(url + '/store/api/' + encodeURIComponent('user/' + properties.id), { headers: headers, // for PouchDB v7 ajax: { headers: headers } // for PouchDB v6 }) }) } }) var api = { get url () { return state.url + '/hoodie' }, // core modules account: hoodieAccount, store: hoodieStore, connectionStatus: hoodieConnectionStatus, // helpers request: require('./request').bind(null, state), log: log, // events on: require('./events').on.bind(null, state), one: require('./events').one.bind(null, state), off: require('./events').off.bind(null, state), trigger: require('./events').trigger.bind(null, state) } api.plugin = require('./plugin').bind(null, api, state) internals.init(api) return api } function mergeOptionsAndCreate (ObjectConstructor, defaultOptions, stateOptions) { var options = defaultsDeep(defaultOptions, stateOptions || {}) return new ObjectConstructor(options) } },{"./events":2,"./init":5,"./plugin":6,"./request":7,"@hoodie/account-client":9,"@hoodie/connection-status/client":28,"@hoodie/log/client":40,"@hoodie/store-client":49,"lodash/defaultsDeep":264,"pouchdb-doc-api":302}],4:[function(require,module,exports){ module.exports = getState var EventEmitter = require('events').EventEmitter var defaultsDeep = require('lodash/defaultsDeep') function getState (options) { options = options || {} if (typeof options.url !== 'string') { throw new TypeError('options.url is required (see https://github.com/hoodiehq/hoodie-client#constructor)') } if (typeof options.PouchDB !== 'function') { throw new TypeError('options.PouchDB is required (see https://github.com/hoodiehq/hoodie-client#constructor)') } var requiredProperties = { emitter: (options && options.emitter) || new EventEmitter() } return defaultsDeep(requiredProperties, options) } },{"events":101,"lodash/defaultsDeep":264}],5:[function(require,module,exports){ module.exports = init function init (hoodie) { // In order to prevent data loss, we want to move all data that has been // created without an account (e.g. while offline) to the user’s account // on signin. So before the signin happens, we store the user account’s id // and data and store it again after the signin hoodie.account.hook.before('signin', function (options) { return Promise.all([ hoodie.account.get('id'), hoodie.store.findAll() ]).then(function (results) { options.beforeSignin = { accountId: results[0], docs: results[1] } }) }) hoodie.account.hook.after('signin', function (account, options) { // when signing in to a newly created account, the account.id does not // change. The same is true when the user changed their username. In both // cases there is no need to migrate local data if (options.beforeSignin.accountId === account.id) { return hoodie.store.connect() } return hoodie.store.reset() .then(function () { function migrate (doc) { doc.createdBy = account.id delete doc._rev return doc } return hoodie.store.add(options.beforeSignin.docs.map(migrate)) }) .then(function () { return hoodie.store.connect() }) }) hoodie.account.hook.before('signout', function () { return hoodie.store.push() .catch(function (error) { if (error.status !== 401) { throw error } error.message = 'Local changes could not be synced, sign in first' throw error }) }) hoodie.account.hook.after('signout', function (options) { return hoodie.store.reset() }) hoodie.account.on('unauthenticate', hoodie.store.disconnect) hoodie.account.on('reauthenticate', hoodie.store.connect) // handle connection status changes hoodie.connectionStatus.on('disconnect', function () { hoodie.account.get('session') .then(function (session) { if (session) { hoodie.store.disconnect() } }) }) hoodie.connectionStatus.on('reconnect', function () { hoodie.account.get('session') .then(function (session) { if (session) { hoodie.store.connect() } }) }) hoodie.account.get('session') .then(function (session) { // signed out if (!session) { return } // signed in, but session was invalid if (session.invalid) { return } // hoodie.connectionStatus.ok is false if there is a connection issue if (hoodie.connectionStatus.ok === false) { return } hoodie.store.connect() }) } },{}],6:[function(require,module,exports){ module.exports = function pluginMethod (hoodie, options, plugin) { if (typeof plugin === 'function') { plugin(hoodie, options) } if (typeof plugin === 'object') { Object.keys(plugin).forEach(function (key) { hoodie[key] = plugin[key] }) } return hoodie } },{}],7:[function(require,module,exports){ module.exports = request var Promise = require('lie') var internals = module.exports.internals = {} internals.request = require('./utils/request') function request (state, options) { if (!state) { return Promise.reject(new Error('hoodie.request: state must be defined')) } if (!options) { return Promise.reject(new Error('hoodie.request: URL or options argument must be defined')) } var requestOptions = { url: options.url || options } if ((/^\/([^/]|$)/).test(requestOptions.url)) { requestOptions.url = state.url + requestOptions.url } if (options.data) { requestOptions.body = JSON.stringify(options.data) } requestOptions.method = options.method requestOptions.headers = options.headers return internals.request(requestOptions) } },{"./utils/request":8,"lie":110}],8:[function(require,module,exports){ module.exports = request var nets = require('nets') var Promise = require('lie') function request (options) { options.encoding = undefined return new Promise(function (resolve, reject) { nets(options, function (error, response) { if (error) { return reject(error) } if (response.statusCode >= 400) { error = new Error('HTTP error ' + response.statusCode) error.name = 'RequestError' error.code = response.statusCode error.body = response.body return reject(error) } resolve(response) }) }) } },{"lie":110,"nets":298}],9:[function(require,module,exports){ module.exports = Account var events = require('./lib/events') var getState = require('./utils/get-state') function Account (options) { if (!(this instanceof Account)) { return new Account(options) } var state = getState(options) var api = { signUp: require('./lib/sign-up').bind(null, state), signIn: require('./lib/sign-in').bind(null, state), signOut: require('./lib/sign-out').bind(null, state), destroy: require('./lib/destroy').bind(null, state), get: require('./lib/get').bind(null, state), update: require('./lib/update').bind(null, state), profile: { get: require('./lib/profile-get').bind(null, state), update: require('./lib/profile-update').bind(null, state) }, request: require('./lib/request').bind(null, state), on: events.on.bind(null, state), one: events.one.bind(null, state), off: events.off.bind(null, state), hook: state.hook.api, validate: require('./lib/validate').bind(null, state) } return api } },{"./lib/destroy":10,"./lib/events":11,"./lib/get":12,"./lib/profile-get":13,"./lib/profile-update":14,"./lib/request":15,"./lib/sign-in":16,"./lib/sign-out":17,"./lib/sign-up":18,"./lib/update":19,"./lib/validate":20,"./utils/get-state":25}],10:[function(require,module,exports){ module.exports = destroy var omit = require('lodash/omit') var internals = module.exports.internals = {} internals.request = require('../utils/request') internals.get = require('./get') function destroy (state) { return state.setup .then(function () { return state.cache.get() }) .then(function (cache) { var promise = Promise.resolve() if (internals.get(state, 'session')) { promise = promise.then(function () { internals.request({ method: 'DELETE', url: state.url + '/session/account', headers: { authorization: 'Session ' + cache.session.id } }) }) } return promise.then(function () { state.cache.unset() var account = omit(cache, 'session') state.emitter.emit('signout', account) state.emitter.emit('destroy', account) return account }) }) } },{"../utils/request":26,"./get":12,"lodash/omit":291}],11:[function(require,module,exports){ module.exports = { on: on, one: one, off: off } /** * add a listener to an event * * @param {String} eventName Name of event * @param {Function} handler callback for event */ function on (state, eventName, handler) { state.emitter.on(eventName, handler) return this } /** * adds a one time listener to an event * * @param {String} eventName Name of event * @param {Function} handler callback for event */ function one (state, eventName, handler) { state.emitter.once(eventName, handler) return this } /** * removes a listener for the specified event * * It will unsubscribe at most, one instance of a listener for a particular event. * If any single listener has subcribed multiple times to the same event, * then `off` must be called multiple times. * * @param {String} eventName Name of event * @param {Function} handler callback for event */ function off (state, eventName, handler) { state.emitter.removeListener(eventName, handler) return this } },{}],12:[function(require,module,exports){ module.exports = accountGet var set = require('lodash/set') var merge = require('lodash/merge') var internals = module.exports.internals = {} internals.getProperties = require('../utils/get-properties') internals.fetchProperties = require('../utils/fetch-properties') function accountGet (state, path, options) { if (typeof path === 'object' && !Array.isArray(path)) { options = path path = undefined } return state.setup .then(state.cache.get) .then(function (cachedProperties) { if ((options && options.local) || !cachedProperties.session || pathIsLocalOnly(path)) { return internals.getProperties(cachedProperties, path) } return internals.fetchProperties({ url: state.url + '/session/account', sessionId: cachedProperties.session.id, path: path }) .then(function (result) { if (typeof path === 'string') { set(cachedProperties, path, result) } else if (Array.isArray(path)) { merge(cachedProperties, result, { session: cachedProperties.session }) } else { result.session = cachedProperties.session cachedProperties = result } // reauthenticate an expired session if (cachedProperties.session.invalid) { delete cachedProperties.session.invalid state.emitter.emit('reauthenticate') } return state.cache.set(cachedProperties) .then(function () { return internals.getProperties(cachedProperties, path) }) }) .catch(function (error) { if (error.statusCode !== 401) { throw error } cachedProperties.session.invalid = true state.emitter.emit('unauthenticate') return state.cache.set(cachedProperties) .then(function () { throw error }) }) }) } function pathIsLocalOnly (path) { if (!path) { return false } if (typeof path === 'string') { return isLocalPath(path) } return path.filter(isLocalPath).length === path.length } function isLocalPath (path) { return /^(id|session)\b/.test(path) } },{"../utils/fetch-properties":22,"../utils/get-properties":24,"lodash/merge":289,"lodash/set":293}],13:[function(require,module,exports){ module.exports = profileGet var set = require('lodash/set') var internals = module.exports.internals = {} internals.getProperties = require('../utils/get-properties') internals.fetchProperties = require('../utils/fetch-properties') function profileGet (state, path, options) { if (typeof path === 'object' && !Array.isArray(path)) { options = path path = undefined } return state.setup .then(state.cache.get) .then(function (cachedProperties) { if (!cachedProperties.session) { return internals.getProperties(cachedProperties.profile || {}, path) } if (options && options.local) { return internals.getProperties(cachedProperties.profile || {}, path) } return internals.fetchProperties({ url: state.url + '/session/account/profile', sessionId: cachedProperties.session.id, path: path }) .then(function (result) { if (typeof path === 'string') { set(cachedProperties.profile, path, result) } else { cachedProperties.profile = result } // reauthenticate an expired session if (cachedProperties.session.invalid) { delete cachedProperties.session.invalid state.emitter.emit('reauthenticate') } return state.cache.set(cachedProperties) .then(function () { return internals.getProperties(cachedProperties.profile || {}, path) }) }) .catch(function (error) { if (error.statusCode !== 401) { throw error } cachedProperties.session.invalid = true state.emitter.emit('unauthenticate') return state.cache.set(cachedProperties) .then(function () { throw error }) }) }) } },{"../utils/fetch-properties":22,"../utils/get-properties":24,"lodash/set":293}],14:[function(require,module,exports){ module.exports = updateProfile var clone = require('lodash/clone') var merge = require('lodash/merge') var Promise = require('lie') var internals = module.exports.internals = {} internals.request = require('../utils/request') internals.serialise = require('../utils/serialise') function updateProfile (state, options) { if (!options) { return Promise.reject(new Error('Please specify a profile property to update or add.')) } return state.setup .then(function () { return state.cache.get() }) .then(function (cache) { return internals.request({ method: 'PATCH', url: state.url + '/session/account/profile', headers: { authorization: 'Session ' + cache.session.id }, body: internals.serialise('profile', options, cache.id + '-profile') }) .then(function () { if (!cache.profile) { cache.profile = {} } merge(cache.profile, options) state.cache.set(cache) state.emitter.emit('update', clone(cache)) return cache.profile }) .catch(function (error) { if (error.statusCode === 401) { cache.session.invalid = true state.emitter.emit('unauthenticate') state.cache.set(cache) } throw error }) }) } },{"../utils/request":26,"../utils/serialise":27,"lie":110,"lodash/clone":262,"lodash/merge":289}],15:[function(require,module,exports){ module.exports = request var Promise = require('lie') var internals = module.exports.internals = {} internals.deserialise = require('../utils/deserialise') internals.request = require('../utils/request') internals.serialise = require('../utils/serialise') function request (state, options) { if (!options || !options.type) { return Promise.reject(new Error('account.request: options.type must be passed')) } return state.setup .then(function () { return internals.request({ url: state.url + '/requests', method: 'POST', body: internals.serialise('request', options) }) }) .then(function (response) { var data = internals.deserialise(response.body) state.emitter.emit(options.type, data) return data }) } },{"../utils/deserialise":21,"../utils/request":26,"../utils/serialise":27,"lie":110}],16:[function(require,module,exports){ module.exports = signIn var Promise = require('lie') var omit = require('lodash/omit') var internals = module.exports.internals = {} internals.deserialise = require('../utils/deserialise') internals.request = require('../utils/request') internals.serialise = require('../utils/serialise') function signIn (state, options) { if (!options) { options = {} } var usernameOrPasswordUnset = !options.username || !options.password if (usernameOrPasswordUnset && !options.token) { return Promise.reject(new Error('options.username/options.password or options.token required')) } return state.setup .then(function () { return state.cache.get() }) .then(function (cache) { // If a different user is signed in than the one trying to signIn, throw an error if (cache.session && cache.username !== options.username) { return Promise.reject(new Error('You must sign out before signing in')) } return state.hook('signin', options, function (options) { return internals.request({ url: state.url + '/session', method: 'PUT', body: internals.serialise('session', options) }) .then(function (response) { var data = internals.deserialise(response.body, { include: 'account' }) // admins don’t have an account if (!data.account) { data.account = { username: options.username } } // If the username hasn’t changed, emit 'reauthenticate' instead of 'signin' var emitEvent = cache.username === data.account.username ? 'reauthenticate' : 'signin' cache = { username: data.account.username, session: { id: data.id } } if (data.account.id) { cache.id = data.account.id } state.cache.set(cache) state.emitter.emit(emitEvent, cache) return omit(cache, 'session') }) }) }) } },{"../utils/deserialise":21,"../utils/request":26,"../utils/serialise":27,"lie":110,"lodash/omit":291}],17:[function(require,module,exports){ module.exports = signOut var omit = require('lodash/omit') var internals = module.exports.internals = {} internals.request = require('../utils/request') internals.get = require('./get') internals.generateId = require('../utils/generate-id') function signOut (state) { return state.setup .then(function () { return state.cache.get() }) .then(function (cache) { if (!cache.session) { throw new Error('UnauthenticatedError: Not signed in') } return state.hook('signout', function () { return internals.request({ method: 'DELETE', url: state.url + '/session', headers: { authorization: 'Session ' + cache.session.id } }) .then(function () { return state.cache.unset() }) .then(function () { return state.cache.set({ id: internals.generateId() }) .then(function () { var account = omit(cache, 'session') state.emitter.emit('signout', account) return account }) }) }) }) } },{"../utils/generate-id":23,"../utils/request":26,"./get":12,"lodash/omit":291}],18:[function(require,module,exports){ module.exports = signUp var Promise = require('lie') var internals = module.exports.internals = {} internals.request = require('../utils/request') internals.deserialise = require('../utils/deserialise') internals.serialise = require('../utils/serialise') function signUp (state, options) { if (!options || !options.username || !options.password) { return Promise.reject(new Error('options.username and options.password is required')) } return state.setup .then(function () { return state.cache.get() }) .then(function (cache) { state.validate(options) if (options.profile) { throw new Error('SignUp with profile data not yet implemented. Please see https://github.com/hoodiehq/hoodie-account-client/issues/11.') } options.createdAt = cache.createdAt return internals.request({ url: state.url + '/session/account', method: 'PUT', body: internals.serialise('account', options, cache.id) }) .then(function (response) { var account = internals.deserialise(response.body, { include: 'profile' }) state.emitter.emit('signup', account) return account }) }) } },{"../utils/deserialise":21,"../utils/request":26,"../utils/serialise":27,"lie":110}],19:[function(require,module,exports){ module.exports = update var merge = require('lodash/merge') var omit = require('lodash/omit') var Promise = require('lie') var internals = module.exports.internals = {} internals.deserialise = require('../utils/deserialise') internals.request = require('../utils/request') internals.serialise = require('../utils/serialise') function update (state, options) { if (!options) { return Promise.reject(new Error('Specify an account property to update')) } return state.setup .then(function () { return state.cache.get() }) .then(function (cache) { return internals.request({ method: 'PATCH', url: state.url + '/session/account', headers: { authorization: 'Session ' + cache.session.id }, body: internals.serialise('account', options, cache.id) }) .then(function (response) { // when a username changes, the session ID gets recalculated, as it’s based // on the username, see npm.im/couchdb-calculate-session-id. In that case // the server sets x-set-session with the new session id. if (response.headers['x-set-session']) { cache.session.id = response.headers['x-set-session'] } merge(cache, omit(options, ['password'])) state.cache.set(cache) var account = omit(cache, 'session') state.emitter.emit('update', account) return account }) .catch(function (error) { if (error.statusCode === 401) { cache.session.invalid = true state.emitter.emit('unauthenticate') state.cache.set(cache) } throw error }) }) } },{"../utils/deserialise":21,"../utils/request":26,"../utils/serialise":27,"lie":110,"lodash/merge":289,"lodash/omit":291}],20:[function(require,module,exports){ module.exports = validate var Promise = require('lie') function validate (state, options) { var self = this return new Promise(function (resolve, reject) { var result try { result = state.validate.call(self, options) } catch (error) { reject(error) } if (result && result.then) { return result.then(resolve, reject) } resolve() }) } },{"lie":110}],21:[function(require,module,exports){ module.exports = deserialise var merge = require('lodash/merge') var filter = require('lodash/filter') function deserialise (response, options) { if (!response || !response.data) { throw new Error('Please include a JSON API response to deserialise.') } return Array.isArray(response.data) ? deserialiseMany(options || {}, response) : deserialiseOne(options || {}, response) } function deserialiseOne (options, response) { var resource = response.data var properties = {} options = merge({}, options) if (resource.type !== 'profile') { properties.id = resource.id } if (options.include) { var tmp = options.include.indexOf('.') var currentInclude = options.include.substr(0, tmp) var nextInclude = options.include.substr(tmp + 1) if (tmp === -1) { currentInclude = nextInclude nextInclude = '' } if (resource.relationships) { var relationship = resource.relationships[currentInclude].data var includedResource = filter(response.included, { type: relationship.type, id: relationship.id })[0] if (includedResource) { options.include = nextInclude properties[currentInclude] = deserialiseOne(options, { included: response.included, data: includedResource }) } } } if (resource.attributes) { Object.keys(resource.attributes).forEach(function (attribute) { properties[attribute] = resource.attributes[attribute] }) } return properties } function deserialiseMany (options, response) { return response.data.map(function (resource) { return deserialiseOne(options, { included: response.included, data: resource }) }) } },{"lodash/filter":266,"lodash/merge":289}],22:[function(require,module,exports){ module.exports = fetchProperties var deserialise = require('./deserialise') var getProperties = require('./get-properties') var request = require('./request') function fetchProperties (options) { return request({ url: options.url, method: 'GET', headers: { authorization: 'Session ' + options.sessionId } }) .then(function (response) { var data = deserialise(response.body) return getProperties(data, options.path) }) } },{"./deserialise":21,"./get-properties":24,"./request":26}],23:[function(require,module,exports){ module.exports = generateId // uuids consist of numbers and lowercase letters only. // We stick to lowercase letters to prevent confusion // and to prevent issues with CouchDB, e.g. database // names only allow for lowercase letters. var CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'.split('') var LENGTH = 7 function generateId () { var id = '' var radix = CHARS.length for (var i = 0; i < LENGTH; i++) { var rand = Math.random() * radix var c = CHARS[Math.floor(rand)] id += String(c).charAt(0) } return id } },{}],24:[function(require,module,exports){ module.exports = getProperties var get = require('lodash/get') var set = require('lodash/set') function getProperties (baseObject, path) { if (path === undefined) { return baseObject } if (Array.isArray(path)) { return path.reduce(function (properties, path) { return set(properties, path, get(baseObject, path)) }, {}) } return get(baseObject, path) } },{"lodash/get":268,"lodash/set":293}],25:[function(require,module,exports){ module.exports = getState var Hook = require('before-after-hook') var EventEmitter = require('events').EventEmitter var LocalStorageStore = require('async-get-set-store') var generateId = require('./generate-id') function getState (options) { if (!options) { options = {} } if (typeof options === 'string') { options = {url: options} } if (!options.url) { throw new Error('options.url is required') } var cacheKey = options.cacheKey || 'account' var cache = options.cache || new LocalStorageStore(cacheKey) var setup = cache.get() .then(function (storedAccount) { if (storedAccount.id) { if (options.id && options.id !== storedAccount.id) { throw new Error('account.id conflict') } return } storedAccount = { id: options.id || generateId(), createdAt: new Date().toISOString() } return cache.set(storedAccount) }) .catch(function (error) { error.message = 'Error while initialising: ' + error.message throw error }) var state = { cacheKey: cacheKey, emitter: options.emitter || new EventEmitter(), hook: new Hook(), url: options.url, validate: options.validate || function () {}, cache: cache, setup: setup } return state } },{"./generate-id":23,"async-get-set-store":92,"before-after-hook":94,"events":101}],26:[function(require,module,exports){ module.exports = request var nets = require('nets') var Promise = require('lie') var set = require('lodash/set') function request (options) { options.encoding = undefined return new Promise(function (resolve, reject) { set(options, 'headers.accept', 'application/vnd.api+json') set(options, 'headers.content-type', 'application/vnd.api+json') options.json = true if (options.body) { // works around an issue where nets-xhr stringifies options.json // if it is truthy, which overides options.body options.json = options.body } nets(options, function (error, response) { if (error) { error.name = 'ConnectionError' return reject(error) } if (response.statusCode >= 400) { error = new Error(response.body.errors[0].detail) error.name = response.body.errors[0].title + 'Error' error.statusCode = parseInt(response.body.errors[0].status, 10) return reject(error) } resolve(response) }) }) } },{"lie":110,"lodash/set":293,"nets":298}],27:[function(require,module,exports){ module.exports = serialise function serialise (type, attributes, id) { if (!type || !attributes) { throw new Error('Serialisation must include a type and some attributes.') } var data = { type: type, attributes: attributes } if (id) { data.id = id } return { data: data } } },{}],28:[function(require,module,exports){ module.exports = Connection var EventEmitter = require('events').EventEmitter var check = require('../lib/check') var startChecking = require('../lib/start-checking') var stopChecking = require('../lib/stop-checking') var isChecking = require('../lib/is-checking') var getOk = require('../lib/get-ok') var on = require('../lib/on') var off = require('../lib/off') var reset = require('../lib/reset') var parseOptions = require('../lib/utils/parse-options') function Connection (options) { var state = parseOptions(options) state.emitter = new EventEmitter() var api = { get ready () { return state.ready.then(function () { return api }) }, get ok () { return getOk(state) }, get isChecking () { return isChecking(state) }, check: check.bind(null, state), stopChecking: stopChecking.bind(null, state), startChecking: startChecking.bind(null, state), on: on.bind(null, state), off: off.bind(null, state), reset: reset.bind(null, state) } return api } },{"../lib/check":29,"../lib/get-ok":30,"../lib/is-checking":31,"../lib/off":32,"../lib/on":33,"../lib/reset":34,"../lib/start-checking":35,"../lib/stop-checking":36,"../lib/utils/parse-options":38,"events":101}],29:[function(require,module,exports){ module.exports = check var nextTick = require('next-tick') var internals = module.exports.internals = {} internals.cache = require('./utils/cache') internals.request = require('./utils/request') function check (state) { return state.ready .then(function () { if (state.request) { state.request.abort() } state.request = internals.request({ method: state.method, url: state.url, timeout: state.timeout }) // once request finishes, remove it from state return state.request .then(function () { delete state.request state.timestamp = new Date().toISOString() if (state.error) { nextTick(function () { state.emitter.emit('reconnect') }) delete state.error } }) .catch(function (error) { delete state.request state.timestamp = new Date().toISOString() if (!state.error) { nextTick(function () { state.emitter.emit('disconnect') }) } state.error = { name: error.name, message: error.message, code: error.code } }) .then(function () { return internals.cache.set(state) }) .then(function () { if (state.error) { throw state.error } }) }) } },{"./utils/cache":37,"./utils/request":39,"next-tick":299}],30:[function(require,module,exports){ module.exports = getOk function getOk (state) { if (state.timestamp === undefined) { return undefined } return state.error === undefined } },{}],31:[function(require,module,exports){ module.exports = isChecking function isChecking (state) { return !!state.checkTimeout } },{}],32:[function(require,module,exports){ module.exports = off function off (state, eventName, handler) { state.emitter.removeListener(eventName, handler) return this } },{}],33:[function(require,module,exports){ module.exports = on function on (state, eventName, handler) { state.emitter.on(eventName, handler) return this } },{}],34:[function(require,module,exports){ module.exports = reset var cache = require('./utils/cache') function reset (state, o) { return state.ready .then(function () { var options = o || {} state.timestamp = undefined state.error = undefined if (typeof options.interval === 'number') { var intervalTimeValue = options.interval state.interval = { connected: intervalTimeValue, disconnected: intervalTimeValue } } if (state.request) { state.request.abort() } return cache.unset(state) }) .then(function () { state.emitter.emit('reset') }) } },{"./utils/cache":37}],35:[function(require,module,exports){ module.exports = startChecking var internals = module.exports.internals = {} internals.check = require('./check') internals.getOk = require('./get-ok') function startChecking (state, options) { return state.ready .then(function () { options = parse(options) if (!state.method || !state.url) { return } handleInterval(state, options) }) } function timeoutHandler (state, options) { var checkAgain = handleInterval.bind(null, state, options) internals.check(state, options).then(checkAgain, checkAgain) } function handleInterval (state, options) { var timeout var ok = internals.getOk(state) if (options.checkTimeout) { timeout = options.checkTimeout } if (options.interval) { if (typeof options.interval === 'number') { timeout = options.interval } else { // if ok is undefined, then we are unsure what the state is but it is most likely connected. So only when ok // is explicitly false do we want to use the disconnected interval timeout = (ok === false) ? options.interval.disconnected : options.interval.connected } } if (timeout) { // we use setTimeout on purpose, we don't want to send requests each // x seconds, but rather set a timeout for x seconds after each response // but we use `checkTimeout` as variable as the effect is the same state.checkTimeout = setTimeout(timeoutHandler, timeout, state, options) return } options.interval = 30000 state.checkTimeout = setTimeout(timeoutHandler, 0, state, options) } function parse (options) { if (!options) { return {} } return options } },{"./check":29,"./get-ok":30}],36:[function(require,module,exports){ module.exports = stopChecking function stopChecking (state) { return state.ready .then(function () { clearTimeout(state.checkTimeout) delete state.checkTimeout }) } },{}],37:[function(require,module,exports){ module.exports = { get: getCache, set: setCache, unset: clearCache } function setCache (state) { if (state.cache === false) { return Promise.resolve() } var data = { timestamp: state.timestamp, error: state.error } return state.cache.set(data) } function getCache (state) { if (state.cache === false) { return Promise.resolve({}) } return state.cache.get() .then(function (data) { return data }) } function clearCache (state) { if (state.cache === false) { return Promise.resolve() } return state.cache.unset() } },{}],38:[function(require,module,exports){ module.exports = parseOptions var Store = require('async-get-set-store') var nextTick = require('next-tick') var cache = require('./cache') var DEFAULTS = { cache: {}, cacheTimeout: 7200000, // 2h in milliseconds method: 'HEAD', checkTimeout: undefined, interval: { connected: undefined, disconnected: undefined } } function parseOptions (options) { var url = typeof options === 'string' ? options : options && options.url if (!url) { throw new TypeError('Connection: url must be set') } if (typeof options === 'string') { options = {} } if (options.cache === undefined) { options.cache = new Store('connection_' + url) } if (typeof options.interval === 'number') { var intervalTimeValue = options.interval options.interval = { connected: intervalTimeValue, disconnected: intervalTimeValue } } var state = { url: url, cache: options.cache, method: options.method || DEFAULTS.method, checkTimeout: options.checkTimeout || DEFAULTS.checkTimeout, interval: options.interval || DEFAULTS.interval, cacheTimeout: options.cacheTimeout || DEFAULTS.cacheTimeout, ready: cache.get({ cache: options.cache }) .then(function (cache) { if (cache.timestamp) { var cachedTime = +new Date(cache.timestamp) var currentTime = +new Date() if (state.cacheTimeout && currentTime >= cachedTime + state.cacheTimeout) { nextTick(function () { state.emitter.emit('reset', cache) }) } else { state.error = cache.error state.timestamp = cache.timestamp } } }) .catch(function (error) { error.name = 'SetupError' error.message = 'Error while initialising: ' + error.message throw error }) } return state } },{"./cache":37,"async-get-set-store":92,"next-tick":299}],39:[function(require,module,exports){ module.exports = request var internals = module.exports.internals = {} internals.nets = require('nets') function request (options) { var requestState var _reject var promise = new Promise(function (resolve, reject) { _reject = reject requestState = internals.nets({ method: options.method, url: options.url, timeout: options.timeout, // Turn off the use of Buffer in nets // in order to make this module compatible // for Webpack builds. // see: https://github.com/maxogden/nets encoding: undefined }, function (error, response, body) { if (error) { error.name = error.code === 'ETIMEDOUT' ? 'TimeoutError' : 'ConnectionError' error.code = undefined return reject(error) } if (response.statusCode >= 400) { error = new Error('Server error') error.name = 'ServerError' error.code = response.statusCode return reject(error) } resolve() }) }) promise.abort = function () { try { requestState.abort() } catch (error) {} var error = new Error('Aborted') error.name = 'AbortError' error.code = 0 _reject(error) } return promise } },{"nets":298}],40:[function(require,module,exports){ module.exports = Log Log.console = console var log = require('../lib/log') var debug = require('../lib/debug') var info = require('../lib/info') var warn = require('../lib/warn') var error = require('../lib/error') var parseOptions = require('../lib/utils/parse-options') function Log (options) { var state = parseOptions(options) state.console = Log.console var api = log.bind(null, state) api.debug = debug.bind(null, state) api.info = info.bind(null, state) api.warn = warn.bind(null, state) api.error = error.bind(null, state) api.scoped = function (name) { return Log({ prefix: state.prefix + ':' + name, level: state.level, styles: state.styles }) } Object.defineProperty(api, 'level', { get: function () { return state.level }, set: function (newValue) { if (['debug', 'error', 'info', 'warn'].indexOf(newValue) === -1) { throw new Error('Invalid value for log.level: ' + newValue) } state.level = newValue }, enumerable: true }) Object.defineProperty(api, 'prefix', { get: function () { return state.prefix }, set: function (newValue) { throw new Error('log.prefix is read-only') }, enumerable: true }) return api } },{"../lib/debug":41,"../lib/error":42,"../lib/info":43,"../lib/log":44,"../lib/utils/parse-options":46,"../lib/warn":48}],41:[function(require,module,exports){ module.exports = debug var logLevelIgnored = require('./utils/log-level-ignored') var prepareLogArguments = require('./utils/prepare-log-arguments') function debug (state) { if (logLevelIgnored(state, 'debug')) { return } var args = prepareLogArguments(state, 'debug', [].slice.call(arguments, 1)) state.console.log.apply(state.console, args) } },{"./utils/log-level-ignored":45,"./utils/prepare-log-arguments":47}],42:[function(require,module,exports){ module.exports = error var prepareLogArguments = require('./utils/prepare-log-arguments') function error (state) { var args = prepareLogArguments(state, 'error', [].slice.call(arguments, 1)) state.console.error.apply(state.console, args) } },{"./utils/prepare-log-arguments":47}],43:[function(require,module,exports){ module.exports = info var logLevelIgnored = require('./utils/log-level-ignored') var prepareLogArguments = require('./utils/prepare-log-arguments') function info (state) { if (logLevelIgnored(state, 'info')) { return } var args = prepareLogArguments(state, 'info', [].slice.call(arguments, 1)) state.console.info.apply(state.console, args) } },{"./utils/log-level-ignored":45,"./utils/prepare-log-arguments":47}],44:[function(require,module,exports){ module.exports = log var prepareLogArguments = require('./utils/prepare-log-arguments') function log (state) { var args = prepareLogArguments(state, 'log', [].slice.call(arguments, 1)) state.console.log.apply(state.console, args) } },{"./utils/prepare-log-arguments":47}],45:[function(require,module,exports){ module.exports = logLevelIgnored function logLevelIgnored (state, target) { return LEVELS[target] < LEVELS[state.level] } var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 } },{}],46:[function(require,module,exports){ module.exports = parseOptions parseOptions.browserSupportsLogStyles = require('browser-supports-log-styles') var DEFAULT_LEVEL = 'warn' var DEFAULT_STYLES = { default: 'color: white; padding: .2em .4em; border-radius: 1em', debug: 'background: green', log: 'background: gray', info: 'background: blue', warn: 'background: orange', error: 'background: red', reset: 'background: inherit; color: inherit' } function parseOptions (options) { if (typeof options === 'string') { options = { prefix: options } } if (!options || !options.prefix) { throw new TypeError('"prefix" required for new Log(options)') } if (options.styles === false) { options.styles = false } if (typeof options.styles === 'undefined') { options.styles = parseOptions.browserSupportsLogStyles() } if (options.styles === true) { options.styles = { default: DEFAULT_STYLES.default, log: DEFAULT_STYLES.log, debug: DEFAULT_STYLES.debug, info: DEFAULT_STYLES.info, warn: DEFAULT_STYLES.warn, error: DEFAULT_STYLES.error, reset: DEFAULT_STYLES.reset } } options.level = options.level || DEFAULT_LEVEL return options } },{"browser-supports-log-styles":98}],47:[function(require,module,exports){ module.exports = prepareLogArguments function prepareLogArguments (state, type, args) { if (state.styles) { return ['%c' + state.prefix + '%c', state.styles.default + '; ' + state.styles[type], state.styles.reset].concat(args) } return ['(' + state.prefix + ':' + type + ')'].concat(args) } },{}],48:[function(require,module,exports){ module.exports = warn var logLevelIgnored = require('./utils/log-level-ignored') var prepareLogArguments = require('./utils/prepare-log-arguments') function warn (state) { if (logLevelIgnored(state, 'warn')) { return } var args = prepareLogArguments(state, 'warn', [].slice.call(arguments, 1)) state.console.warn.apply(state.console, args) } },{"./utils/log-level-ignored":45,"./utils/prepare-log-arguments":47}],49:[function(require,module,exports){ module.exports = Store var EventEmitter = require('events').EventEmitter var assign = require('lodash/assign') var internals = Store.internals = {} internals.handleChanges = require('./lib/helpers/handle-changes') function Store (dbName, options) { if (!(this instanceof Store)) return new Store(dbName, options) if (typeof dbName !== 'string') throw new Error('Must be a valid string.') if (!options || (!('remote' in options) && !options.remoteBaseUrl)) { throw new Error('options.remote or options.remoteBaseUrl is required') } if (options.remoteBaseUrl) { options.remoteBaseUrl = options.remoteBaseUrl.replace(/\/$/, '') if (!options.remote) { options.remote = dbName } if (!/^https?:\/\//.test(options.remote)) { options.remote = (options.remoteBaseUrl + '/' + encodeURIComponent(options.remote)) } } var db = new options.PouchDB(dbName) var emitter = new EventEmitter() var state = { db: db, dbName: dbName, PouchDB: options.PouchDB, emitter: emitter, validate: options.validate, get remote () { return options.remote } } var api = { db: state.db, isPersistent: require('./lib/is-persistent').bind(null, state), add: require('./lib/add').bind(null, state, null), find: require('./lib/find').bind(null, state, null), findAll: require('./lib/find-all').bind(null, state, null), findOrAdd: require('./lib/find-or-add').bind(null, state, null), update: require('./lib/update').bind(null, state, null), updateOrAdd: require('./lib/update-or-add').bind(null, state, null),